diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..bd04da86121bb2e8ed00e960cb1678650e184ac4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,22 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +backend/open_webui/static/fonts/NotoSans-Bold.ttf filter=lfs diff=lfs merge=lfs -text +backend/open_webui/static/fonts/NotoSans-Italic.ttf filter=lfs diff=lfs merge=lfs -text +backend/open_webui/static/fonts/NotoSans-Regular.ttf filter=lfs diff=lfs merge=lfs -text +backend/open_webui/static/fonts/NotoSans-Variable.ttf filter=lfs diff=lfs merge=lfs -text +backend/open_webui/static/fonts/NotoSansJP-Regular.ttf filter=lfs diff=lfs merge=lfs -text +backend/open_webui/static/fonts/NotoSansJP-Variable.ttf filter=lfs diff=lfs merge=lfs -text +backend/open_webui/static/fonts/NotoSansKR-Regular.ttf filter=lfs diff=lfs merge=lfs -text +backend/open_webui/static/fonts/NotoSansKR-Variable.ttf filter=lfs diff=lfs merge=lfs -text +backend/open_webui/static/fonts/NotoSansSC-Regular.ttf filter=lfs diff=lfs merge=lfs -text +backend/open_webui/static/fonts/NotoSansSC-Variable.ttf filter=lfs diff=lfs merge=lfs -text +backend/open_webui/static/fonts/Twemoji.ttf filter=lfs diff=lfs merge=lfs -text +static/assets/fonts/Archivo-Variable.ttf filter=lfs diff=lfs merge=lfs -text +static/assets/fonts/Inter-Variable.ttf filter=lfs diff=lfs merge=lfs -text +static/assets/fonts/Mona-Sans.woff2 filter=lfs diff=lfs merge=lfs -text +static/assets/fonts/Vazirmatn-Variable.ttf filter=lfs diff=lfs merge=lfs -text +static/assets/images/adam.jpg filter=lfs diff=lfs merge=lfs -text +static/assets/images/earth.jpg filter=lfs diff=lfs merge=lfs -text +static/assets/images/galaxy.jpg filter=lfs diff=lfs merge=lfs -text +static/assets/images/space.jpg filter=lfs diff=lfs merge=lfs -text diff --git a/Dockerfile b/Dockerfile index 769b8846f982ffc2b8cf444e6bae6f38e39a32c3..88ffd097529e4fbb4dd781c151b72416e77cb5fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,208 @@ -# Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker -# you will also find guides on how best to write your Dockerfile +# syntax=docker/dockerfile:1 +# Initialize device type args +# use build args in the docker build command with --build-arg="BUILDARG=true" +ARG USE_CUDA=false +ARG USE_OLLAMA=false +ARG USE_SLIM=false +ARG USE_PERMISSION_HARDENING=false +# Tested with cu117 for CUDA 11 and cu121 for CUDA 12 (default) +ARG USE_CUDA_VER=cu128 +# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers +# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard +# for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB) +# IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them. +ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2 +ARG USE_RERANKING_MODEL="" +ARG USE_AUXILIARY_EMBEDDING_MODEL=TaylorAI/bge-micro-v2 -FROM python:3.12 +# Tiktoken encoding name; models to use can be found at https://huggingface.co/models?library=tiktoken +ARG USE_TIKTOKEN_ENCODING_NAME="cl100k_base" -RUN useradd -m -u 1000 user -USER user -ENV PATH="/home/user/.local/bin:$PATH" +ARG BUILD_HASH=dev-build +# Override at your own risk - non-root configurations are untested +ARG UID=0 +ARG GID=0 + +######## WebUI frontend ######## +FROM --platform=$BUILDPLATFORM node:22-alpine3.20 AS build +ARG BUILD_HASH + +# Set Node.js options (heap limit Allocation failed - JavaScript heap out of memory) +# ENV NODE_OPTIONS="--max-old-space-size=4096" WORKDIR /app -COPY --chown=user ./requirements.txt requirements.txt -RUN pip install --no-cache-dir --upgrade -r requirements.txt +# to store git revision in build +RUN apk add --no-cache git + +COPY package.json package-lock.json ./ +RUN npm ci --force + +COPY . . +ENV APP_BUILD_HASH=${BUILD_HASH} +RUN npm run build + +######## WebUI backend ######## +FROM python:3.11.14-slim-bookworm AS base + +# Use args +ARG USE_CUDA +ARG USE_OLLAMA +ARG USE_CUDA_VER +ARG USE_SLIM +ARG USE_PERMISSION_HARDENING +ARG USE_EMBEDDING_MODEL +ARG USE_RERANKING_MODEL +ARG USE_AUXILIARY_EMBEDDING_MODEL +ARG UID +ARG GID + +# Python settings +ENV PYTHONUNBUFFERED=1 + +## Basis ## +ENV ENV=prod \ + PORT=8080 \ + # pass build args to the build + USE_OLLAMA_DOCKER=${USE_OLLAMA} \ + USE_CUDA_DOCKER=${USE_CUDA} \ + USE_SLIM_DOCKER=${USE_SLIM} \ + USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \ + USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} \ + USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL} \ + USE_AUXILIARY_EMBEDDING_MODEL_DOCKER=${USE_AUXILIARY_EMBEDDING_MODEL} + +## Basis URL Config ## +ENV OLLAMA_BASE_URL="/ollama" \ + OPENAI_API_BASE_URL="" + +## API Key and Security Config ## +ENV OPENAI_API_KEY="" \ + WEBUI_SECRET_KEY="" \ + SCARF_NO_ANALYTICS=true \ + DO_NOT_TRACK=true \ + ANONYMIZED_TELEMETRY=false + +#### Other models ######################################################### +## whisper TTS model settings ## +ENV WHISPER_MODEL="base" \ + WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models" + +## RAG Embedding model settings ## +ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \ + RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \ + AUXILIARY_EMBEDDING_MODEL="$USE_AUXILIARY_EMBEDDING_MODEL_DOCKER" \ + SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models" + +## Tiktoken model settings ## +ENV TIKTOKEN_ENCODING_NAME="cl100k_base" \ + TIKTOKEN_CACHE_DIR="/app/backend/data/cache/tiktoken" + +## Hugging Face download cache ## +ENV HF_HOME="/app/backend/data/cache/embedding/models" + +## Torch Extensions ## +# ENV TORCH_EXTENSIONS_DIR="/.cache/torch_extensions" + +#### Other models ########################################################## + +WORKDIR /app/backend + +ENV HOME=/root +# Create user and group if not root +RUN if [ $UID -ne 0 ]; then \ + if [ $GID -ne 0 ]; then \ + addgroup --gid $GID app; \ + fi; \ + adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \ + fi + +RUN mkdir -p $HOME/.cache/chroma +RUN echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id + +# Make sure the user has access to the app and root directory +RUN chown -R $UID:$GID /app $HOME + +# Install common system dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + git build-essential pandoc gcc netcat-openbsd curl jq \ + libmariadb-dev \ + python3-dev \ + ffmpeg libsm6 libxext6 zstd \ + && rm -rf /var/lib/apt/lists/* + +# install python dependencies +COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt + +# Set UV_LINK_MODE to copy to prevent 0-byte file corruption in QEMU arm64 cross-builds +ENV UV_LINK_MODE=copy + +RUN set -e; \ + pip3 install --no-cache-dir uv; \ + if [ "$USE_CUDA" = "true" ]; then \ + # If you use CUDA the whisper and embedding model will be downloaded on first use + # fix: pin torch<=2.9.1 - torch 2.10.0 aarch64 wheels cause SIGILL on ARM devices (RPi 4 Cortex-A72) #21349 + pip3 install 'torch<=2.9.1' torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir; \ + uv pip install --system -r requirements.txt --no-cache-dir; \ + python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')"; \ + python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')"; \ + python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \ + python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \ + python -c "import nltk; nltk.download('punkt_tab')"; \ + else \ + pip3 install 'torch<=2.9.1' torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir; \ + uv pip install --system -r requirements.txt --no-cache-dir; \ + if [ "$USE_SLIM" != "true" ]; then \ + python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')"; \ + python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')"; \ + python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \ + python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \ + python -c "import nltk; nltk.download('punkt_tab')"; \ + fi; \ + fi; \ + mkdir -p /app/backend/data; chown -R $UID:$GID /app/backend/data/; \ + rm -rf /var/lib/apt/lists/*; + +# Install Ollama if requested +RUN if [ "$USE_OLLAMA" = "true" ]; then \ + date +%s > /tmp/ollama_build_hash && \ + echo "Cache broken at timestamp: `cat /tmp/ollama_build_hash`" && \ + curl -fsSL https://ollama.com/install.sh | sh && \ + rm -rf /var/lib/apt/lists/*; \ + fi + +# copy embedding weight from build +# RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2 +# COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx + +# copy built frontend files +COPY --chown=$UID:$GID --from=build /app/build /app/build +COPY --chown=$UID:$GID --from=build /app/CHANGELOG.md /app/CHANGELOG.md +COPY --chown=$UID:$GID --from=build /app/package.json /app/package.json + +# copy backend files +COPY --chown=$UID:$GID ./backend . + +EXPOSE 8080 + +HEALTHCHECK CMD curl --silent --fail http://localhost:${PORT:-8080}/health | jq -ne 'input.status == true' || exit 1 + +# Minimal, atomic permission hardening for OpenShift (arbitrary UID): +# - Group 0 owns /app and /root +# - Directories are group-writable and have SGID so new files inherit GID 0 +RUN if [ "$USE_PERMISSION_HARDENING" = "true" ]; then \ + set -eux; \ + chgrp -R 0 /app /root || true; \ + chmod -R g+rwX /app /root || true; \ + find /app -type d -exec chmod g+s {} + || true; \ + find /root -type d -exec chmod g+s {} + || true; \ + fi + +USER $UID:$GID -COPY --chown=user . /app -CMD ["open-webui", "serve", "--host", "0.0.0.0", "--port", "7860"] +ARG BUILD_HASH +ENV WEBUI_BUILD_VERSION=${BUILD_HASH} +ENV DOCKER=true +CMD [ "bash", "start.sh"] diff --git a/README.md b/README.md index f893461294dc23f5aa9dd355f799632bbceb5b14..dcafd0728e99cffdc7853c9d356d7e13c9a34ae7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ ---- + title: Open Webui emoji: ♥︎ colorFrom: green @@ -6,6 +6,3 @@ colorTo: blue sdk: docker pinned: false license: mit ---- - -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..97ab32835d90e779180b09b866c7eecbdb1433ac --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,14 @@ +__pycache__ +.env +_old +uploads +.ipynb_checkpoints +*.db +_test +!/data +/data/* +!/data/litellm +/data/litellm/* +!data/litellm/config.yaml + +!data/config.json \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..614a5f7465676c7bdc5d90f117fa6c18c75c4476 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,12 @@ +__pycache__ +.env +_old +uploads +.ipynb_checkpoints +*.db +_test +Pipfile +!/data +/data/* +/open_webui/data/* +.webui_secret_key \ No newline at end of file diff --git a/backend/dev.sh b/backend/dev.sh new file mode 100644 index 0000000000000000000000000000000000000000..838b93f653656496297c47bc81cf4a0e22375d7d --- /dev/null +++ b/backend/dev.sh @@ -0,0 +1,3 @@ +export CORS_ALLOW_ORIGIN="http://localhost:5173;http://localhost:8080" +PORT="${PORT:-8080}" +uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips "${FORWARDED_ALLOW_IPS:-*}" --reload diff --git a/backend/open_webui/__init__.py b/backend/open_webui/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..be25227d368bba08b2b516a79fca90498c0fbd5d --- /dev/null +++ b/backend/open_webui/__init__.py @@ -0,0 +1,96 @@ +import base64 +import os +import random +from pathlib import Path +from typing import Annotated + +import typer +import uvicorn + +app = typer.Typer() + +KEY_FILE = Path.cwd() / '.webui_secret_key' + + +def version_callback(value: bool) -> None: + if value: + from open_webui.env import VERSION + + typer.echo(f'Open WebUI version: {VERSION}') + raise typer.Exit() + + +@app.command() +def main( + version: Annotated[bool | None, typer.Option('--version', callback=version_callback)] = None, +): + pass + + +@app.command() +def serve( + host: str = '0.0.0.0', + port: int = 8080, +): + os.environ['FROM_INIT_PY'] = 'true' + if os.getenv('WEBUI_SECRET_KEY') is None: + typer.echo('Loading WEBUI_SECRET_KEY from file, not provided as an environment variable.') + if not KEY_FILE.exists(): + typer.echo(f'Generating a new secret key and saving it to {KEY_FILE}') + KEY_FILE.write_bytes(base64.b64encode(random.randbytes(12))) + typer.echo(f'Loading WEBUI_SECRET_KEY from {KEY_FILE}') + os.environ['WEBUI_SECRET_KEY'] = KEY_FILE.read_text() + + if os.getenv('USE_CUDA_DOCKER', 'false') == 'true': + typer.echo('CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries.') + LD_LIBRARY_PATH = os.getenv('LD_LIBRARY_PATH', '').split(':') + os.environ['LD_LIBRARY_PATH'] = ':'.join( + LD_LIBRARY_PATH + + [ + '/usr/local/lib/python3.11/site-packages/torch/lib', + '/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib', + ] + ) + try: + import torch + + assert torch.cuda.is_available(), 'CUDA not available' + typer.echo('CUDA seems to be working') + except Exception as e: + typer.echo( + 'Error when testing CUDA but USE_CUDA_DOCKER is true. ' + 'Resetting USE_CUDA_DOCKER to false and removing ' + f'LD_LIBRARY_PATH modifications: {e}' + ) + os.environ['USE_CUDA_DOCKER'] = 'false' + os.environ['LD_LIBRARY_PATH'] = ':'.join(LD_LIBRARY_PATH) + + import open_webui.main # noqa: F401 + from open_webui.env import UVICORN_WORKERS # Import the workers setting + + uvicorn.run( + 'open_webui.main:app', + host=host, + port=port, + forwarded_allow_ips='*', + workers=UVICORN_WORKERS, + ) + + +@app.command() +def dev( + host: str = '0.0.0.0', + port: int = 8080, + reload: bool = True, +): + uvicorn.run( + 'open_webui.main:app', + host=host, + port=port, + reload=reload, + forwarded_allow_ips='*', + ) + + +if __name__ == '__main__': + app() diff --git a/backend/open_webui/alembic.ini b/backend/open_webui/alembic.ini new file mode 100644 index 0000000000000000000000000000000000000000..dccd8a3c123ddba7649da299f31c85f1d9a316ef --- /dev/null +++ b/backend/open_webui/alembic.ini @@ -0,0 +1,114 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = migrations + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = .. + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to migrations/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# sqlalchemy.url = REPLACE_WITH_DATABASE_URL + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py new file mode 100644 index 0000000000000000000000000000000000000000..06178d385c1d3d98afce10d56d72e0f132978070 --- /dev/null +++ b/backend/open_webui/config.py @@ -0,0 +1,4177 @@ +import asyncio +import json +import logging +import os +import shutil +import socket +import base64 +from concurrent.futures import ThreadPoolExecutor +import redis + +from datetime import datetime +from pathlib import Path +from typing import Generic, Union, Optional, TypeVar +from urllib.parse import urlparse + +import requests +from pydantic import BaseModel +from sqlalchemy import JSON, Column, DateTime, Integer, func +from authlib.integrations.starlette_client import OAuth + + +from open_webui.env import ( + DATA_DIR, + DATABASE_URL, + ENABLE_DB_MIGRATIONS, + ENV, + REDIS_URL, + REDIS_KEY_PREFIX, + REDIS_SENTINEL_HOSTS, + REDIS_SENTINEL_PORT, + FRONTEND_BUILD_DIR, + OFFLINE_MODE, + OPEN_WEBUI_DIR, + WEBUI_AUTH, + WEBUI_FAVICON_URL, + WEBUI_NAME, + log, +) +from open_webui.internal.db import Base, get_db, get_async_db +from open_webui.utils.redis import get_redis_connection + + +class EndpointFilter(logging.Filter): + def filter(self, record: logging.LogRecord) -> bool: + return record.getMessage().find('/health') == -1 + + +# Filter out /endpoint +logging.getLogger('uvicorn.access').addFilter(EndpointFilter()) + +#################################### +# Config helpers +#################################### + + +# Function to run the alembic migrations +def run_migrations(): + log.info('Running migrations') + try: + from alembic import command + from alembic.config import Config + + alembic_cfg = Config(OPEN_WEBUI_DIR / 'alembic.ini') + + # Set the script location dynamically + migrations_path = OPEN_WEBUI_DIR / 'migrations' + alembic_cfg.set_main_option('script_location', str(migrations_path)) + + command.upgrade(alembic_cfg, 'head') + except Exception as e: + log.exception(f'Error running migrations: {e}') + + +if ENABLE_DB_MIGRATIONS: + run_migrations() + + +class Config(Base): + __tablename__ = 'config' + + id = Column(Integer, primary_key=True) + data = Column(JSON, nullable=False) + version = Column(Integer, nullable=False, default=0) + created_at = Column(DateTime, nullable=False, server_default=func.now()) + updated_at = Column(DateTime, nullable=True, onupdate=func.now()) + + +def load_json_config(): + with open(f'{DATA_DIR}/config.json', 'r') as file: + return json.load(file) + + +def save_to_db(data): + """Sync save — used ONLY at startup/import time.""" + with get_db() as db: + existing_config = db.query(Config).first() + if not existing_config: + new_config = Config(data=data, version=0) + db.add(new_config) + else: + existing_config.data = data + existing_config.updated_at = datetime.now() + db.add(existing_config) + db.commit() + + +async def async_save_to_db(data): + """Async save — used for ALL runtime config persistence.""" + from sqlalchemy import select + + async with get_async_db() as db: + result = await db.execute(select(Config).limit(1)) + existing_config = result.scalars().first() + if not existing_config: + new_config = Config(data=data, version=0) + db.add(new_config) + else: + existing_config.data = data + existing_config.updated_at = datetime.now() + db.add(existing_config) + await db.commit() + + +def reset_config(): + """Sync reset — used ONLY at startup.""" + with get_db() as db: + db.query(Config).delete() + db.commit() + + +async def async_reset_config(): + """Async reset — used at runtime.""" + from sqlalchemy import delete as sa_delete + + async with get_async_db() as db: + await db.execute(sa_delete(Config)) + await db.commit() + + +# When initializing, check if config.json exists and migrate it to the database +if os.path.exists(f'{DATA_DIR}/config.json'): + data = load_json_config() + save_to_db(data) + os.rename(f'{DATA_DIR}/config.json', f'{DATA_DIR}/old_config.json') + +DEFAULT_CONFIG = { + 'version': 0, + 'ui': {}, +} + + +def get_config(): + with get_db() as db: + config_entry = db.query(Config).order_by(Config.id.desc()).first() + return config_entry.data if config_entry else DEFAULT_CONFIG + + +CONFIG_DATA = get_config() + + +def get_config_value(config_path: str): + path_parts = config_path.split('.') + cur_config = CONFIG_DATA + for key in path_parts: + if key in cur_config: + cur_config = cur_config[key] + else: + return None + return cur_config + + +PERSISTENT_CONFIG_REGISTRY = [] + + +def save_config(config): + """Sync save — used ONLY at startup/import time.""" + global CONFIG_DATA + global PERSISTENT_CONFIG_REGISTRY + try: + save_to_db(config) + CONFIG_DATA = config + + # Trigger updates on all registered PersistentConfig entries + for config_item in PERSISTENT_CONFIG_REGISTRY: + config_item.update() + except Exception as e: + log.exception(e) + return False + return True + + +async def async_save_config(config): + """Async save — used for ALL runtime config persistence.""" + global CONFIG_DATA + global PERSISTENT_CONFIG_REGISTRY + try: + await async_save_to_db(config) + CONFIG_DATA = config + + # Trigger updates on all registered PersistentConfig entries + for config_item in PERSISTENT_CONFIG_REGISTRY: + config_item.update() + except Exception as e: + log.exception(e) + return False + return True + + +T = TypeVar('T') + +ENABLE_PERSISTENT_CONFIG = os.environ.get('ENABLE_PERSISTENT_CONFIG', 'True').lower() == 'true' + + +class PersistentConfig(Generic[T]): + def __init__(self, env_name: str, config_path: str, env_value: T): + self.env_name = env_name + self.config_path = config_path + self.env_value = env_value + self.config_value = get_config_value(config_path) + + if self.config_value is not None and ENABLE_PERSISTENT_CONFIG: + if self.config_path.startswith('oauth.') and not ENABLE_OAUTH_PERSISTENT_CONFIG: + log.info(f"Skipping loading of '{env_name}' as OAuth persistent config is disabled") + self.value = env_value + else: + log.info(f"'{env_name}' loaded from the latest database entry") + self.value = self.config_value + else: + self.value = env_value + + PERSISTENT_CONFIG_REGISTRY.append(self) + + def __str__(self): + return str(self.value) + + @property + def __dict__(self): + raise TypeError('PersistentConfig object cannot be converted to dict, use config_get or .value instead.') + + def __getattribute__(self, item): + if item == '__dict__': + raise TypeError('PersistentConfig object cannot be converted to dict, use config_get or .value instead.') + return super().__getattribute__(item) + + def update(self): + new_value = get_config_value(self.config_path) + if new_value is not None: + self.value = new_value + log.info(f'Updated {self.env_name} to new value {self.value}') + + def save(self): + """Sync save — used ONLY at startup/import time.""" + log.info(f"Saving '{self.env_name}' to the database") + path_parts = self.config_path.split('.') + sub_config = CONFIG_DATA + for key in path_parts[:-1]: + if key not in sub_config: + sub_config[key] = {} + sub_config = sub_config[key] + sub_config[path_parts[-1]] = self.value + save_to_db(CONFIG_DATA) + self.config_value = self.value + + async def async_save(self): + """Async save — used for ALL runtime config persistence.""" + log.info(f"Saving '{self.env_name}' to the database") + path_parts = self.config_path.split('.') + sub_config = CONFIG_DATA + for key in path_parts[:-1]: + if key not in sub_config: + sub_config[key] = {} + sub_config = sub_config[key] + sub_config[path_parts[-1]] = self.value + await async_save_to_db(CONFIG_DATA) + self.config_value = self.value + + +class AppConfig: + _redis: Union[redis.Redis, redis.cluster.RedisCluster] = None + _redis_key_prefix: str + + _state: dict[str, PersistentConfig] + + def __init__( + self, + redis_url: Optional[str] = None, + redis_sentinels: Optional[list] = [], + redis_cluster: Optional[bool] = False, + redis_key_prefix: str = 'open-webui', + ): + if redis_url: + super().__setattr__('_redis_key_prefix', redis_key_prefix) + super().__setattr__( + '_redis', + get_redis_connection( + redis_url, + redis_sentinels, + redis_cluster, + decode_responses=True, + ), + ) + + super().__setattr__('_state', {}) + + def __setattr__(self, key, value): + if isinstance(value, PersistentConfig): + self._state[key] = value + else: + self._state[key].value = value + + # At runtime (inside the event loop) persist via the async engine + # to avoid blocking the loop and contending with the async DB pool. + # At startup/import time, fall back to sync. + try: + loop = asyncio.get_running_loop() + loop.create_task(self._async_persist(key)) + except RuntimeError: + self._state[key].save() + + if self._redis and ENABLE_PERSISTENT_CONFIG: + redis_key = f'{self._redis_key_prefix}:config:{key}' + self._redis.set(redis_key, json.dumps(self._state[key].value)) + + async def _async_persist(self, key): + """Persist a single config key via the async engine.""" + try: + await self._state[key].async_save() + except Exception as e: + log.error(f'Failed to async-persist config key {key}: {e}') + + def __getattr__(self, key): + if key not in self._state: + raise AttributeError(f"Config key '{key}' not found") + + # If Redis is available and persistent config is enabled, check for an updated value + if self._redis and ENABLE_PERSISTENT_CONFIG: + redis_key = f'{self._redis_key_prefix}:config:{key}' + redis_value = self._redis.get(redis_key) + + if redis_value is not None: + try: + decoded_value = json.loads(redis_value) + + # Update the in-memory value if different + if self._state[key].value != decoded_value: + self._state[key].value = decoded_value + log.info(f'Updated {key} from Redis: {decoded_value}') + + except json.JSONDecodeError: + log.error(f'Invalid JSON format in Redis for {key}: {redis_value}') + + return self._state[key].value + + +#################################### +# WEBUI_AUTH (Required for security) +#################################### + +ENABLE_API_KEYS = PersistentConfig( + 'ENABLE_API_KEYS', + 'auth.enable_api_keys', + os.environ.get('ENABLE_API_KEYS', 'False').lower() == 'true', +) + +ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS = PersistentConfig( + 'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS', + 'auth.api_key.endpoint_restrictions', + os.environ.get( + 'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS', + os.environ.get('ENABLE_API_KEY_ENDPOINT_RESTRICTIONS', 'False'), + ).lower() + == 'true', +) + +API_KEYS_ALLOWED_ENDPOINTS = PersistentConfig( + 'API_KEYS_ALLOWED_ENDPOINTS', + 'auth.api_key.allowed_endpoints', + os.environ.get('API_KEYS_ALLOWED_ENDPOINTS', os.environ.get('API_KEY_ALLOWED_ENDPOINTS', '')), +) + +JWT_EXPIRES_IN = PersistentConfig('JWT_EXPIRES_IN', 'auth.jwt_expiry', os.environ.get('JWT_EXPIRES_IN', '4w')) + +if JWT_EXPIRES_IN.value == '-1': + log.warning( + "⚠️ SECURITY WARNING: JWT_EXPIRES_IN is set to '-1'\n" + ' See: https://docs.openwebui.com/reference/env-configuration\n' + ) + +#################################### +# OAuth config +#################################### + +ENABLE_OAUTH_PERSISTENT_CONFIG = os.environ.get('ENABLE_OAUTH_PERSISTENT_CONFIG', 'False').lower() == 'true' + +ENABLE_OAUTH_SIGNUP = PersistentConfig( + 'ENABLE_OAUTH_SIGNUP', + 'oauth.enable_signup', + os.environ.get('ENABLE_OAUTH_SIGNUP', 'False').lower() == 'true', +) + +OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE = PersistentConfig( + 'OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE', + 'oauth.refresh_token_include_scope', + os.environ.get('OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE', 'False').lower() == 'true', +) + + +OAUTH_MERGE_ACCOUNTS_BY_EMAIL = PersistentConfig( + 'OAUTH_MERGE_ACCOUNTS_BY_EMAIL', + 'oauth.merge_accounts_by_email', + os.environ.get('OAUTH_MERGE_ACCOUNTS_BY_EMAIL', 'False').lower() == 'true', +) + +OAUTH_PROVIDERS = {} + +GOOGLE_CLIENT_ID = PersistentConfig( + 'GOOGLE_CLIENT_ID', + 'oauth.google.client_id', + os.environ.get('GOOGLE_CLIENT_ID', ''), +) + +GOOGLE_CLIENT_SECRET = PersistentConfig( + 'GOOGLE_CLIENT_SECRET', + 'oauth.google.client_secret', + os.environ.get('GOOGLE_CLIENT_SECRET', ''), +) + + +GOOGLE_OAUTH_SCOPE = PersistentConfig( + 'GOOGLE_OAUTH_SCOPE', + 'oauth.google.scope', + os.environ.get('GOOGLE_OAUTH_SCOPE', 'openid email profile'), +) + +GOOGLE_REDIRECT_URI = PersistentConfig( + 'GOOGLE_REDIRECT_URI', + 'oauth.google.redirect_uri', + os.environ.get('GOOGLE_REDIRECT_URI', ''), +) + +GOOGLE_OAUTH_AUTHORIZE_PARAMS = {} +_google_oauth_authorize_params = os.environ.get('GOOGLE_OAUTH_AUTHORIZE_PARAMS', '') +if _google_oauth_authorize_params: + try: + _parsed = json.loads(_google_oauth_authorize_params) + if isinstance(_parsed, dict): + GOOGLE_OAUTH_AUTHORIZE_PARAMS = _parsed + else: + log.warning('GOOGLE_OAUTH_AUTHORIZE_PARAMS must be a JSON object, ignoring') + except (json.JSONDecodeError, TypeError): + log.warning('GOOGLE_OAUTH_AUTHORIZE_PARAMS is not valid JSON, ignoring') + +MICROSOFT_CLIENT_ID = PersistentConfig( + 'MICROSOFT_CLIENT_ID', + 'oauth.microsoft.client_id', + os.environ.get('MICROSOFT_CLIENT_ID', ''), +) + +MICROSOFT_CLIENT_SECRET = PersistentConfig( + 'MICROSOFT_CLIENT_SECRET', + 'oauth.microsoft.client_secret', + os.environ.get('MICROSOFT_CLIENT_SECRET', ''), +) + +MICROSOFT_CLIENT_TENANT_ID = PersistentConfig( + 'MICROSOFT_CLIENT_TENANT_ID', + 'oauth.microsoft.tenant_id', + os.environ.get('MICROSOFT_CLIENT_TENANT_ID', ''), +) + +MICROSOFT_CLIENT_LOGIN_BASE_URL = PersistentConfig( + 'MICROSOFT_CLIENT_LOGIN_BASE_URL', + 'oauth.microsoft.login_base_url', + os.environ.get('MICROSOFT_CLIENT_LOGIN_BASE_URL', 'https://login.microsoftonline.com'), +) + +MICROSOFT_CLIENT_PICTURE_URL = PersistentConfig( + 'MICROSOFT_CLIENT_PICTURE_URL', + 'oauth.microsoft.picture_url', + os.environ.get( + 'MICROSOFT_CLIENT_PICTURE_URL', + 'https://graph.microsoft.com/v1.0/me/photo/$value', + ), +) + + +MICROSOFT_OAUTH_SCOPE = PersistentConfig( + 'MICROSOFT_OAUTH_SCOPE', + 'oauth.microsoft.scope', + os.environ.get('MICROSOFT_OAUTH_SCOPE', 'openid email profile'), +) + +MICROSOFT_REDIRECT_URI = PersistentConfig( + 'MICROSOFT_REDIRECT_URI', + 'oauth.microsoft.redirect_uri', + os.environ.get('MICROSOFT_REDIRECT_URI', ''), +) + +GITHUB_CLIENT_ID = PersistentConfig( + 'GITHUB_CLIENT_ID', + 'oauth.github.client_id', + os.environ.get('GITHUB_CLIENT_ID', ''), +) + +GITHUB_CLIENT_SECRET = PersistentConfig( + 'GITHUB_CLIENT_SECRET', + 'oauth.github.client_secret', + os.environ.get('GITHUB_CLIENT_SECRET', ''), +) + +GITHUB_CLIENT_SCOPE = PersistentConfig( + 'GITHUB_CLIENT_SCOPE', + 'oauth.github.scope', + os.environ.get('GITHUB_CLIENT_SCOPE', 'user:email'), +) + +GITHUB_CLIENT_REDIRECT_URI = PersistentConfig( + 'GITHUB_CLIENT_REDIRECT_URI', + 'oauth.github.redirect_uri', + os.environ.get('GITHUB_CLIENT_REDIRECT_URI', ''), +) + +OAUTH_CLIENT_ID = PersistentConfig( + 'OAUTH_CLIENT_ID', + 'oauth.oidc.client_id', + os.environ.get('OAUTH_CLIENT_ID', ''), +) + +OAUTH_CLIENT_SECRET = PersistentConfig( + 'OAUTH_CLIENT_SECRET', + 'oauth.oidc.client_secret', + os.environ.get('OAUTH_CLIENT_SECRET', ''), +) + +OPENID_PROVIDER_URL = PersistentConfig( + 'OPENID_PROVIDER_URL', + 'oauth.oidc.provider_url', + os.environ.get('OPENID_PROVIDER_URL', ''), +) + +OPENID_END_SESSION_ENDPOINT = PersistentConfig( + 'OPENID_END_SESSION_ENDPOINT', + 'oauth.oidc.end_session_endpoint', + os.environ.get('OPENID_END_SESSION_ENDPOINT', ''), +) + +OPENID_REDIRECT_URI = PersistentConfig( + 'OPENID_REDIRECT_URI', + 'oauth.oidc.redirect_uri', + os.environ.get('OPENID_REDIRECT_URI', ''), +) + +OAUTH_SCOPES = PersistentConfig( + 'OAUTH_SCOPES', + 'oauth.oidc.scopes', + os.environ.get('OAUTH_SCOPES', 'openid email profile'), +) + +OAUTH_TIMEOUT = PersistentConfig( + 'OAUTH_TIMEOUT', + 'oauth.oidc.oauth_timeout', + os.environ.get('OAUTH_TIMEOUT', ''), +) + +OAUTH_TOKEN_ENDPOINT_AUTH_METHOD = PersistentConfig( + 'OAUTH_TOKEN_ENDPOINT_AUTH_METHOD', + 'oauth.oidc.token_endpoint_auth_method', + os.environ.get('OAUTH_TOKEN_ENDPOINT_AUTH_METHOD', None), +) + +OAUTH_CODE_CHALLENGE_METHOD = PersistentConfig( + 'OAUTH_CODE_CHALLENGE_METHOD', + 'oauth.oidc.code_challenge_method', + os.environ.get('OAUTH_CODE_CHALLENGE_METHOD', None), +) + +OAUTH_PROVIDER_NAME = PersistentConfig( + 'OAUTH_PROVIDER_NAME', + 'oauth.oidc.provider_name', + os.environ.get('OAUTH_PROVIDER_NAME', 'SSO'), +) + +OAUTH_SUB_CLAIM = PersistentConfig( + 'OAUTH_SUB_CLAIM', + 'oauth.oidc.sub_claim', + os.environ.get('OAUTH_SUB_CLAIM', None), +) + +OAUTH_USERNAME_CLAIM = PersistentConfig( + 'OAUTH_USERNAME_CLAIM', + 'oauth.oidc.username_claim', + os.environ.get('OAUTH_USERNAME_CLAIM', 'name'), +) + + +OAUTH_PICTURE_CLAIM = PersistentConfig( + 'OAUTH_PICTURE_CLAIM', + 'oauth.oidc.avatar_claim', + os.environ.get('OAUTH_PICTURE_CLAIM', 'picture'), +) + +OAUTH_EMAIL_CLAIM = PersistentConfig( + 'OAUTH_EMAIL_CLAIM', + 'oauth.oidc.email_claim', + os.environ.get('OAUTH_EMAIL_CLAIM', 'email'), +) + +OAUTH_GROUPS_CLAIM = PersistentConfig( + 'OAUTH_GROUPS_CLAIM', + 'oauth.oidc.group_claim', + os.environ.get('OAUTH_GROUPS_CLAIM', os.environ.get('OAUTH_GROUP_CLAIM', 'groups')), +) + +FEISHU_CLIENT_ID = PersistentConfig( + 'FEISHU_CLIENT_ID', + 'oauth.feishu.client_id', + os.environ.get('FEISHU_CLIENT_ID', ''), +) + +FEISHU_CLIENT_SECRET = PersistentConfig( + 'FEISHU_CLIENT_SECRET', + 'oauth.feishu.client_secret', + os.environ.get('FEISHU_CLIENT_SECRET', ''), +) + +FEISHU_OAUTH_SCOPE = PersistentConfig( + 'FEISHU_OAUTH_SCOPE', + 'oauth.feishu.scope', + os.environ.get('FEISHU_OAUTH_SCOPE', 'contact:user.base:readonly'), +) + +FEISHU_REDIRECT_URI = PersistentConfig( + 'FEISHU_REDIRECT_URI', + 'oauth.feishu.redirect_uri', + os.environ.get('FEISHU_REDIRECT_URI', ''), +) + +ENABLE_OAUTH_ROLE_MANAGEMENT = PersistentConfig( + 'ENABLE_OAUTH_ROLE_MANAGEMENT', + 'oauth.enable_role_mapping', + os.environ.get('ENABLE_OAUTH_ROLE_MANAGEMENT', 'False').lower() == 'true', +) + +ENABLE_OAUTH_GROUP_MANAGEMENT = PersistentConfig( + 'ENABLE_OAUTH_GROUP_MANAGEMENT', + 'oauth.enable_group_mapping', + os.environ.get('ENABLE_OAUTH_GROUP_MANAGEMENT', 'False').lower() == 'true', +) + +ENABLE_OAUTH_GROUP_CREATION = PersistentConfig( + 'ENABLE_OAUTH_GROUP_CREATION', + 'oauth.enable_group_creation', + os.environ.get('ENABLE_OAUTH_GROUP_CREATION', 'False').lower() == 'true', +) + + +oauth_group_default_share = os.environ.get('OAUTH_GROUP_DEFAULT_SHARE', 'true').strip().lower() +OAUTH_GROUP_DEFAULT_SHARE = PersistentConfig( + 'OAUTH_GROUP_DEFAULT_SHARE', + 'oauth.group_default_share', + ('members' if oauth_group_default_share == 'members' else oauth_group_default_share == 'true'), +) + + +OAUTH_BLOCKED_GROUPS = PersistentConfig( + 'OAUTH_BLOCKED_GROUPS', + 'oauth.blocked_groups', + os.environ.get('OAUTH_BLOCKED_GROUPS', '[]'), +) + +OAUTH_GROUPS_SEPARATOR = os.environ.get('OAUTH_GROUPS_SEPARATOR', ';') + +OAUTH_ROLES_CLAIM = PersistentConfig( + 'OAUTH_ROLES_CLAIM', + 'oauth.roles_claim', + os.environ.get('OAUTH_ROLES_CLAIM', 'roles'), +) + +OAUTH_ROLES_SEPARATOR = os.environ.get('OAUTH_ROLES_SEPARATOR', ',') + +OAUTH_ALLOWED_ROLES = PersistentConfig( + 'OAUTH_ALLOWED_ROLES', + 'oauth.allowed_roles', + [ + role.strip() + for role in os.environ.get('OAUTH_ALLOWED_ROLES', f'user{OAUTH_ROLES_SEPARATOR}admin').split( + OAUTH_ROLES_SEPARATOR + ) + if role + ], +) + +OAUTH_ADMIN_ROLES = PersistentConfig( + 'OAUTH_ADMIN_ROLES', + 'oauth.admin_roles', + [role.strip() for role in os.environ.get('OAUTH_ADMIN_ROLES', 'admin').split(OAUTH_ROLES_SEPARATOR) if role], +) + +OAUTH_ALLOWED_DOMAINS = PersistentConfig( + 'OAUTH_ALLOWED_DOMAINS', + 'oauth.allowed_domains', + [domain.strip() for domain in os.environ.get('OAUTH_ALLOWED_DOMAINS', '*').split(',')], +) + +OAUTH_UPDATE_PICTURE_ON_LOGIN = PersistentConfig( + 'OAUTH_UPDATE_PICTURE_ON_LOGIN', + 'oauth.update_picture_on_login', + os.environ.get('OAUTH_UPDATE_PICTURE_ON_LOGIN', 'False').lower() == 'true', +) + +OAUTH_UPDATE_NAME_ON_LOGIN = PersistentConfig( + 'OAUTH_UPDATE_NAME_ON_LOGIN', + 'oauth.update_name_on_login', + os.environ.get('OAUTH_UPDATE_NAME_ON_LOGIN', 'False').lower() == 'true', +) + +OAUTH_UPDATE_EMAIL_ON_LOGIN = PersistentConfig( + 'OAUTH_UPDATE_EMAIL_ON_LOGIN', + 'oauth.update_email_on_login', + os.environ.get('OAUTH_UPDATE_EMAIL_ON_LOGIN', 'False').lower() == 'true', +) + +OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID = ( + os.environ.get('OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID', 'False').lower() == 'true' +) + +OAUTH_AUDIENCE = PersistentConfig( + 'OAUTH_AUDIENCE', + 'oauth.audience', + os.environ.get('OAUTH_AUDIENCE', ''), +) + +OAUTH_AUTHORIZE_PARAMS = {} +_oauth_authorize_params = os.environ.get('OAUTH_AUTHORIZE_PARAMS', '') +if _oauth_authorize_params: + try: + _parsed = json.loads(_oauth_authorize_params) + if isinstance(_parsed, dict): + OAUTH_AUTHORIZE_PARAMS = _parsed + else: + log.warning('OAUTH_AUTHORIZE_PARAMS must be a JSON object, ignoring') + except (json.JSONDecodeError, TypeError): + log.warning('OAUTH_AUTHORIZE_PARAMS is not valid JSON, ignoring') + + +def load_oauth_providers(): + OAUTH_PROVIDERS.clear() + if GOOGLE_CLIENT_ID.value and GOOGLE_CLIENT_SECRET.value: + + def google_oauth_register(oauth: OAuth): + client = oauth.register( + name='google', + client_id=GOOGLE_CLIENT_ID.value, + client_secret=GOOGLE_CLIENT_SECRET.value, + server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', + client_kwargs={ + 'scope': GOOGLE_OAUTH_SCOPE.value, + **({'timeout': int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}), + }, + redirect_uri=GOOGLE_REDIRECT_URI.value, + **({'authorize_params': GOOGLE_OAUTH_AUTHORIZE_PARAMS} if GOOGLE_OAUTH_AUTHORIZE_PARAMS else {}), + ) + return client + + OAUTH_PROVIDERS['google'] = { + 'register': google_oauth_register, + } + + if MICROSOFT_CLIENT_ID.value and MICROSOFT_CLIENT_SECRET.value and MICROSOFT_CLIENT_TENANT_ID.value: + + def microsoft_oauth_register(oauth: OAuth): + client = oauth.register( + name='microsoft', + client_id=MICROSOFT_CLIENT_ID.value, + client_secret=MICROSOFT_CLIENT_SECRET.value, + server_metadata_url=f'{MICROSOFT_CLIENT_LOGIN_BASE_URL.value}/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration?appid={MICROSOFT_CLIENT_ID.value}', + client_kwargs={ + 'scope': MICROSOFT_OAUTH_SCOPE.value, + **({'timeout': int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}), + }, + redirect_uri=MICROSOFT_REDIRECT_URI.value, + ) + return client + + OAUTH_PROVIDERS['microsoft'] = { + 'picture_url': MICROSOFT_CLIENT_PICTURE_URL.value, + 'register': microsoft_oauth_register, + } + + if GITHUB_CLIENT_ID.value and GITHUB_CLIENT_SECRET.value: + + def github_oauth_register(oauth: OAuth): + client = oauth.register( + name='github', + client_id=GITHUB_CLIENT_ID.value, + client_secret=GITHUB_CLIENT_SECRET.value, + access_token_url='https://github.com/login/oauth/access_token', + authorize_url='https://github.com/login/oauth/authorize', + api_base_url='https://api.github.com', + userinfo_endpoint='https://api.github.com/user', + client_kwargs={ + 'scope': GITHUB_CLIENT_SCOPE.value, + **({'timeout': int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}), + }, + redirect_uri=GITHUB_CLIENT_REDIRECT_URI.value, + ) + return client + + OAUTH_PROVIDERS['github'] = { + 'register': github_oauth_register, + 'sub_claim': 'id', + } + + if ( + OAUTH_CLIENT_ID.value + and (OAUTH_CLIENT_SECRET.value or OAUTH_CODE_CHALLENGE_METHOD.value) + and OPENID_PROVIDER_URL.value + ): + + def oidc_oauth_register(oauth: OAuth): + client_kwargs = { + 'scope': OAUTH_SCOPES.value, + **( + {'token_endpoint_auth_method': OAUTH_TOKEN_ENDPOINT_AUTH_METHOD.value} + if OAUTH_TOKEN_ENDPOINT_AUTH_METHOD.value + else {} + ), + **({'timeout': int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}), + } + + if OAUTH_CODE_CHALLENGE_METHOD.value and OAUTH_CODE_CHALLENGE_METHOD.value == 'S256': + client_kwargs['code_challenge_method'] = 'S256' + elif OAUTH_CODE_CHALLENGE_METHOD.value: + raise Exception( + 'Code challenge methods other than "%s" not supported. Given: "%s"' + % ('S256', OAUTH_CODE_CHALLENGE_METHOD.value) + ) + + client = oauth.register( + name='oidc', + client_id=OAUTH_CLIENT_ID.value, + client_secret=OAUTH_CLIENT_SECRET.value, + server_metadata_url=OPENID_PROVIDER_URL.value, + client_kwargs=client_kwargs, + redirect_uri=OPENID_REDIRECT_URI.value, + ) + return client + + OAUTH_PROVIDERS['oidc'] = { + 'name': OAUTH_PROVIDER_NAME.value, + 'register': oidc_oauth_register, + } + + if FEISHU_CLIENT_ID.value and FEISHU_CLIENT_SECRET.value: + + def feishu_oauth_register(oauth: OAuth): + client = oauth.register( + name='feishu', + client_id=FEISHU_CLIENT_ID.value, + client_secret=FEISHU_CLIENT_SECRET.value, + access_token_url='https://open.feishu.cn/open-apis/authen/v2/oauth/token', + authorize_url='https://accounts.feishu.cn/open-apis/authen/v1/authorize', + api_base_url='https://open.feishu.cn/open-apis', + userinfo_endpoint='https://open.feishu.cn/open-apis/authen/v1/user_info', + client_kwargs={ + 'scope': FEISHU_OAUTH_SCOPE.value, + **({'timeout': int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}), + }, + redirect_uri=FEISHU_REDIRECT_URI.value, + ) + return client + + OAUTH_PROVIDERS['feishu'] = { + 'register': feishu_oauth_register, + 'sub_claim': 'user_id', + } + + configured_providers = [] + if GOOGLE_CLIENT_ID.value: + configured_providers.append('Google') + if MICROSOFT_CLIENT_ID.value: + configured_providers.append('Microsoft') + if GITHUB_CLIENT_ID.value: + configured_providers.append('GitHub') + if FEISHU_CLIENT_ID.value: + configured_providers.append('Feishu') + + if configured_providers and not OPENID_PROVIDER_URL.value and not OPENID_END_SESSION_ENDPOINT.value: + provider_list = ', '.join(configured_providers) + log.warning( + f'⚠️ OAuth providers configured ({provider_list}) but OPENID_PROVIDER_URL not set - logout will not work!' + ) + log.warning( + f"Set OPENID_PROVIDER_URL to your OAuth provider's OpenID Connect discovery endpoint," + f' or set OPENID_END_SESSION_ENDPOINT to a custom logout URL to fix logout functionality.' + ) + + +load_oauth_providers() + +#################################### +# Static DIR +#################################### + +STATIC_DIR = Path(os.getenv('STATIC_DIR', OPEN_WEBUI_DIR / 'static')).resolve() + +try: + if STATIC_DIR.exists(): + for item in STATIC_DIR.iterdir(): + if item.is_file() or item.is_symlink(): + try: + item.unlink() + except Exception as e: + pass +except Exception as e: + pass + +for file_path in (FRONTEND_BUILD_DIR / 'static').glob('**/*'): + if file_path.is_file(): + target_path = STATIC_DIR / file_path.relative_to((FRONTEND_BUILD_DIR / 'static')) + target_path.parent.mkdir(parents=True, exist_ok=True) + try: + shutil.copyfile(file_path, target_path) + except Exception as e: + logging.error(f'An error occurred: {e}') + +frontend_favicon = FRONTEND_BUILD_DIR / 'static' / 'favicon.png' + +if frontend_favicon.exists(): + try: + shutil.copyfile(frontend_favicon, STATIC_DIR / 'favicon.png') + except Exception as e: + logging.error(f'An error occurred: {e}') + +frontend_splash = FRONTEND_BUILD_DIR / 'static' / 'splash.png' + +if frontend_splash.exists(): + try: + shutil.copyfile(frontend_splash, STATIC_DIR / 'splash.png') + except Exception as e: + logging.error(f'An error occurred: {e}') + +frontend_loader = FRONTEND_BUILD_DIR / 'static' / 'loader.js' + +if frontend_loader.exists(): + try: + shutil.copyfile(frontend_loader, STATIC_DIR / 'loader.js') + except Exception as e: + logging.error(f'An error occurred: {e}') + + +#################################### +# CUSTOM_NAME (Legacy) +#################################### + +CUSTOM_NAME = os.environ.get('CUSTOM_NAME', '') + +if CUSTOM_NAME: + try: + r = requests.get(f'https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}') + data = r.json() + if r.ok: + if 'logo' in data: + WEBUI_FAVICON_URL = url = ( + f'https://api.openwebui.com{data["logo"]}' if data['logo'][0] == '/' else data['logo'] + ) + + r = requests.get(url, stream=True) + if r.status_code == 200: + with open(f'{STATIC_DIR}/favicon.png', 'wb') as f: + r.raw.decode_content = True + shutil.copyfileobj(r.raw, f) + + if 'splash' in data: + url = f'https://api.openwebui.com{data["splash"]}' if data['splash'][0] == '/' else data['splash'] + + r = requests.get(url, stream=True) + if r.status_code == 200: + with open(f'{STATIC_DIR}/splash.png', 'wb') as f: + r.raw.decode_content = True + shutil.copyfileobj(r.raw, f) + + WEBUI_NAME = data['name'] + except Exception as e: + log.exception(e) + pass + + +#################################### +# STORAGE PROVIDER +#################################### + +STORAGE_PROVIDER = os.environ.get('STORAGE_PROVIDER', 'local') # defaults to local, s3 +STORAGE_LOCAL_CACHE = os.environ.get('STORAGE_LOCAL_CACHE', 'true').lower() == 'true' + +S3_ACCESS_KEY_ID = os.environ.get('S3_ACCESS_KEY_ID', None) +S3_SECRET_ACCESS_KEY = os.environ.get('S3_SECRET_ACCESS_KEY', None) +S3_REGION_NAME = os.environ.get('S3_REGION_NAME', None) +S3_BUCKET_NAME = os.environ.get('S3_BUCKET_NAME', None) +S3_KEY_PREFIX = os.environ.get('S3_KEY_PREFIX', None) +S3_ENDPOINT_URL = os.environ.get('S3_ENDPOINT_URL', None) +S3_USE_ACCELERATE_ENDPOINT = os.environ.get('S3_USE_ACCELERATE_ENDPOINT', 'false').lower() == 'true' +S3_ADDRESSING_STYLE = os.environ.get('S3_ADDRESSING_STYLE', None) +S3_ENABLE_TAGGING = os.getenv('S3_ENABLE_TAGGING', 'false').lower() == 'true' + +GCS_BUCKET_NAME = os.environ.get('GCS_BUCKET_NAME', None) +GOOGLE_APPLICATION_CREDENTIALS_JSON = os.environ.get('GOOGLE_APPLICATION_CREDENTIALS_JSON', None) + +AZURE_STORAGE_ENDPOINT = os.environ.get('AZURE_STORAGE_ENDPOINT', None) +AZURE_STORAGE_CONTAINER_NAME = os.environ.get('AZURE_STORAGE_CONTAINER_NAME', None) +AZURE_STORAGE_KEY = os.environ.get('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) + + +#################################### +# DIRECT CONNECTIONS +#################################### + +ENABLE_DIRECT_CONNECTIONS = PersistentConfig( + 'ENABLE_DIRECT_CONNECTIONS', + 'direct.enable', + os.environ.get('ENABLE_DIRECT_CONNECTIONS', 'False').lower() == 'true', +) + +#################################### +# OLLAMA_BASE_URL +#################################### + +ENABLE_OLLAMA_API = PersistentConfig( + 'ENABLE_OLLAMA_API', + 'ollama.enable', + os.environ.get('ENABLE_OLLAMA_API', 'True').lower() == 'true', +) + +OLLAMA_API_BASE_URL = os.environ.get('OLLAMA_API_BASE_URL', 'http://localhost:11434/api') + +OLLAMA_BASE_URL = os.environ.get('OLLAMA_BASE_URL', '') +if OLLAMA_BASE_URL: + # Remove trailing slash + OLLAMA_BASE_URL = OLLAMA_BASE_URL[:-1] if OLLAMA_BASE_URL.endswith('/') else OLLAMA_BASE_URL + + +K8S_FLAG = os.environ.get('K8S_FLAG', '') +USE_OLLAMA_DOCKER = os.environ.get('USE_OLLAMA_DOCKER', 'false') + +if OLLAMA_BASE_URL == '' and OLLAMA_API_BASE_URL != '': + OLLAMA_BASE_URL = OLLAMA_API_BASE_URL[:-4] if OLLAMA_API_BASE_URL.endswith('/api') else OLLAMA_API_BASE_URL + +if ENV == 'prod': + if OLLAMA_BASE_URL == '/ollama' and not K8S_FLAG: + if USE_OLLAMA_DOCKER.lower() == 'true': + # if you use all-in-one docker container (Open WebUI + Ollama) + # with the docker build arg USE_OLLAMA=true (--build-arg="USE_OLLAMA=true") this only works with http://localhost:11434 + OLLAMA_BASE_URL = 'http://localhost:11434' + else: + OLLAMA_BASE_URL = 'http://host.docker.internal:11434' + elif K8S_FLAG: + OLLAMA_BASE_URL = 'http://ollama-service.open-webui.svc.cluster.local:11434' + + +def _resolve_ollama_base_url(url: str) -> str: + """If the default Ollama port (11434) is unreachable, try the fallback port (12434).""" + + def reachable(host: str, port: int) -> bool: + try: + with socket.create_connection((host, port), timeout=1.0): + return True + except (OSError, TimeoutError): + return False + + host = urlparse(url).hostname or 'localhost' + + with ThreadPoolExecutor(max_workers=2) as pool: + default = pool.submit(reachable, host, 11434) + fallback = pool.submit(reachable, host, 12434) + + if not default.result() and fallback.result(): + url = url.replace(':11434', ':12434') + log.info(f'Ollama port 11434 unreachable on {host}, falling back to 12434') + elif not default.result(): + log.info(f'Ollama ports 11434 and 12434 both unreachable on {host}') + + return url + + +# Auto-resolve Ollama port when no explicit URL was provided by the user. +# The Dockerfile default is "/ollama" which the block above rewrites to :11434. +if os.environ.get('OLLAMA_BASE_URL', '') in ('', '/ollama') and not os.environ.get('OLLAMA_BASE_URLS', ''): + OLLAMA_BASE_URL = _resolve_ollama_base_url(OLLAMA_BASE_URL) + + +OLLAMA_BASE_URLS = os.environ.get('OLLAMA_BASE_URLS', '') +OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != '' else OLLAMA_BASE_URL + +OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(';')] +OLLAMA_BASE_URLS = PersistentConfig('OLLAMA_BASE_URLS', 'ollama.base_urls', OLLAMA_BASE_URLS) + +OLLAMA_API_CONFIGS = PersistentConfig( + 'OLLAMA_API_CONFIGS', + 'ollama.api_configs', + {}, +) + +#################################### +# OPENAI_API +#################################### + + +ENABLE_OPENAI_API = PersistentConfig( + 'ENABLE_OPENAI_API', + 'openai.enable', + os.environ.get('ENABLE_OPENAI_API', 'True').lower() == 'true', +) + + +OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY', '') +OPENAI_API_BASE_URL = os.environ.get('OPENAI_API_BASE_URL', '') + +GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY', '') +GEMINI_API_BASE_URL = os.environ.get('GEMINI_API_BASE_URL', '') + + +if OPENAI_API_BASE_URL == '': + OPENAI_API_BASE_URL = 'https://api.openai.com/v1' +else: + if OPENAI_API_BASE_URL.endswith('/'): + OPENAI_API_BASE_URL = OPENAI_API_BASE_URL[:-1] + +OPENAI_API_KEYS = os.environ.get('OPENAI_API_KEYS', '') +OPENAI_API_KEYS = OPENAI_API_KEYS if OPENAI_API_KEYS != '' else OPENAI_API_KEY + +OPENAI_API_KEYS = [url.strip() for url in OPENAI_API_KEYS.split(';')] +OPENAI_API_KEYS = PersistentConfig('OPENAI_API_KEYS', 'openai.api_keys', OPENAI_API_KEYS) + +OPENAI_API_BASE_URLS = os.environ.get('OPENAI_API_BASE_URLS', '') +OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS if OPENAI_API_BASE_URLS != '' else OPENAI_API_BASE_URL + +OPENAI_API_BASE_URLS = [ + url.strip() if url != '' else 'https://api.openai.com/v1' for url in OPENAI_API_BASE_URLS.split(';') +] +OPENAI_API_BASE_URLS = PersistentConfig('OPENAI_API_BASE_URLS', 'openai.api_base_urls', OPENAI_API_BASE_URLS) + +OPENAI_API_CONFIGS = PersistentConfig( + 'OPENAI_API_CONFIGS', + 'openai.api_configs', + {}, +) + +# Get the actual OpenAI API key based on the base URL +OPENAI_API_KEY = '' +try: + OPENAI_API_KEY = OPENAI_API_KEYS.value[OPENAI_API_BASE_URLS.value.index('https://api.openai.com/v1')] +except Exception: + pass +OPENAI_API_BASE_URL = 'https://api.openai.com/v1' + + +#################################### +# MODELS +#################################### + +ENABLE_BASE_MODELS_CACHE = PersistentConfig( + 'ENABLE_BASE_MODELS_CACHE', + 'models.base_models_cache', + os.environ.get('ENABLE_BASE_MODELS_CACHE', 'False').lower() == 'true', +) + + +#################################### +# TOOL_SERVERS +#################################### + +try: + tool_server_connections = json.loads(os.environ.get('TOOL_SERVER_CONNECTIONS', '[]')) +except Exception as e: + log.exception(f'Error loading TOOL_SERVER_CONNECTIONS: {e}') + tool_server_connections = [] + + +TOOL_SERVER_CONNECTIONS = PersistentConfig( + 'TOOL_SERVER_CONNECTIONS', + 'tool_server.connections', + tool_server_connections, +) + +#################################### +# TERMINAL_SERVER +#################################### + +terminal_server_connections = json.loads(os.environ.get('TERMINAL_SERVER_CONNECTIONS', '[]')) + +TERMINAL_SERVER_CONNECTIONS = PersistentConfig( + 'TERMINAL_SERVER_CONNECTIONS', + 'terminal_server.connections', + terminal_server_connections, +) + +#################################### +# WEBUI +#################################### + + +WEBUI_URL = PersistentConfig('WEBUI_URL', 'webui.url', os.environ.get('WEBUI_URL', '')) + + +ENABLE_SIGNUP = PersistentConfig( + 'ENABLE_SIGNUP', + 'ui.enable_signup', + (False if not WEBUI_AUTH else os.environ.get('ENABLE_SIGNUP', 'True').lower() == 'true'), +) + +ENABLE_LOGIN_FORM = PersistentConfig( + 'ENABLE_LOGIN_FORM', + 'ui.enable_login_form', + os.environ.get('ENABLE_LOGIN_FORM', 'True').lower() == 'true', +) + +ENABLE_PASSWORD_CHANGE_FORM = PersistentConfig( + 'ENABLE_PASSWORD_CHANGE_FORM', + 'ui.enable_password_change_form', + os.environ.get('ENABLE_PASSWORD_CHANGE_FORM', 'True').lower() == 'true', +) + +ENABLE_PASSWORD_AUTH = os.environ.get('ENABLE_PASSWORD_AUTH', 'True').lower() == 'true' + +DEFAULT_LOCALE = PersistentConfig( + 'DEFAULT_LOCALE', + 'ui.default_locale', + os.environ.get('DEFAULT_LOCALE', ''), +) + +DEFAULT_MODELS = PersistentConfig('DEFAULT_MODELS', 'ui.default_models', os.environ.get('DEFAULT_MODELS', None)) + +DEFAULT_PINNED_MODELS = PersistentConfig( + 'DEFAULT_PINNED_MODELS', + 'ui.default_pinned_models', + os.environ.get('DEFAULT_PINNED_MODELS', None), +) + +try: + default_prompt_suggestions = json.loads(os.environ.get('DEFAULT_PROMPT_SUGGESTIONS', '[]')) +except Exception as e: + log.exception(f'Error loading DEFAULT_PROMPT_SUGGESTIONS: {e}') + default_prompt_suggestions = [] +if default_prompt_suggestions == []: + default_prompt_suggestions = [ + { + 'title': ['Help me study', 'vocabulary for a college entrance exam'], + 'content': "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.", + }, + { + 'title': ['Give me ideas', "for what to do with my kids' art"], + 'content': "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.", + }, + { + 'title': ['Tell me a fun fact', 'about the Roman Empire'], + 'content': 'Tell me a random fun fact about the Roman Empire', + }, + { + 'title': ['Show me a code snippet', "of a website's sticky header"], + 'content': "Show me a code snippet of a website's sticky header in CSS and JavaScript.", + }, + { + 'title': [ + 'Explain options trading', + "if I'm familiar with buying and selling stocks", + ], + 'content': "Explain options trading in simple terms if I'm familiar with buying and selling stocks.", + }, + { + 'title': ['Overcome procrastination', 'give me tips'], + 'content': 'Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?', + }, + ] + +DEFAULT_PROMPT_SUGGESTIONS = PersistentConfig( + 'DEFAULT_PROMPT_SUGGESTIONS', + 'ui.prompt_suggestions', + default_prompt_suggestions, +) + +MODEL_ORDER_LIST = PersistentConfig( + 'MODEL_ORDER_LIST', + 'ui.model_order_list', + [], +) + +DEFAULT_MODEL_METADATA = PersistentConfig( + 'DEFAULT_MODEL_METADATA', + 'models.default_metadata', + {}, +) + +try: + default_model_params = json.loads(os.environ.get('DEFAULT_MODEL_PARAMS', '{}')) +except Exception as e: + log.exception(f'Error loading DEFAULT_MODEL_PARAMS: {e}') + default_model_params = {} + +DEFAULT_MODEL_PARAMS = PersistentConfig( + 'DEFAULT_MODEL_PARAMS', + 'models.default_params', + default_model_params, +) + +DEFAULT_USER_ROLE = PersistentConfig( + 'DEFAULT_USER_ROLE', + 'ui.default_user_role', + os.getenv('DEFAULT_USER_ROLE', 'pending'), +) + +DEFAULT_GROUP_ID = PersistentConfig( + 'DEFAULT_GROUP_ID', + 'ui.default_group_id', + os.environ.get('DEFAULT_GROUP_ID', ''), +) + +PENDING_USER_OVERLAY_TITLE = PersistentConfig( + 'PENDING_USER_OVERLAY_TITLE', + 'ui.pending_user_overlay_title', + os.environ.get('PENDING_USER_OVERLAY_TITLE', ''), +) + +PENDING_USER_OVERLAY_CONTENT = PersistentConfig( + 'PENDING_USER_OVERLAY_CONTENT', + 'ui.pending_user_overlay_content', + os.environ.get('PENDING_USER_OVERLAY_CONTENT', ''), +) + + +RESPONSE_WATERMARK = PersistentConfig( + 'RESPONSE_WATERMARK', + 'ui.watermark', + os.environ.get('RESPONSE_WATERMARK', ''), +) + + +USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_SKILLS_ACCESS = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_SKILLS_ACCESS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_MODELS_IMPORT = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_MODELS_IMPORT', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_MODELS_EXPORT = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_MODELS_EXPORT', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_PROMPTS_IMPORT = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_PROMPTS_IMPORT', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_PROMPTS_EXPORT = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_PROMPTS_EXPORT', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_TOOLS_IMPORT = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_TOOLS_IMPORT', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT', 'False').lower() == 'true' +) + + +USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + + +USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_SHARING = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING = ( + os.environ.get('USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + + +USER_PERMISSIONS_NOTES_ALLOW_SHARING = os.environ.get('USER_PERMISSIONS_NOTES_ALLOW_SHARING', 'False').lower() == 'true' + +USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING = ( + os.environ.get('USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS = ( + os.environ.get('USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS', 'True').lower() == 'true' +) + + +USER_PERMISSIONS_CHAT_CONTROLS = os.environ.get('USER_PERMISSIONS_CHAT_CONTROLS', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_VALVES = os.environ.get('USER_PERMISSIONS_CHAT_VALVES', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_SYSTEM_PROMPT = os.environ.get('USER_PERMISSIONS_CHAT_SYSTEM_PROMPT', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_PARAMS = os.environ.get('USER_PERMISSIONS_CHAT_PARAMS', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_FILE_UPLOAD = os.environ.get('USER_PERMISSIONS_CHAT_FILE_UPLOAD', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_WEB_UPLOAD = os.environ.get('USER_PERMISSIONS_CHAT_WEB_UPLOAD', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_DELETE = os.environ.get('USER_PERMISSIONS_CHAT_DELETE', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_DELETE_MESSAGE = os.environ.get('USER_PERMISSIONS_CHAT_DELETE_MESSAGE', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_CONTINUE_RESPONSE = ( + os.environ.get('USER_PERMISSIONS_CHAT_CONTINUE_RESPONSE', 'True').lower() == 'true' +) + +USER_PERMISSIONS_CHAT_REGENERATE_RESPONSE = ( + os.environ.get('USER_PERMISSIONS_CHAT_REGENERATE_RESPONSE', 'True').lower() == 'true' +) + +USER_PERMISSIONS_CHAT_RATE_RESPONSE = os.environ.get('USER_PERMISSIONS_CHAT_RATE_RESPONSE', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_EDIT = os.environ.get('USER_PERMISSIONS_CHAT_EDIT', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_SHARE = os.environ.get('USER_PERMISSIONS_CHAT_SHARE', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_EXPORT = os.environ.get('USER_PERMISSIONS_CHAT_EXPORT', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_STT = os.environ.get('USER_PERMISSIONS_CHAT_STT', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_TTS = os.environ.get('USER_PERMISSIONS_CHAT_TTS', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_CALL = os.environ.get('USER_PERMISSIONS_CHAT_CALL', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_MULTIPLE_MODELS = ( + os.environ.get('USER_PERMISSIONS_CHAT_MULTIPLE_MODELS', 'True').lower() == 'true' +) + +USER_PERMISSIONS_CHAT_TEMPORARY = os.environ.get('USER_PERMISSIONS_CHAT_TEMPORARY', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED = ( + os.environ.get('USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED', 'False').lower() == 'true' +) + + +USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS = ( + os.environ.get('USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_FEATURES_WEB_SEARCH = os.environ.get('USER_PERMISSIONS_FEATURES_WEB_SEARCH', 'True').lower() == 'true' + +USER_PERMISSIONS_FEATURES_IMAGE_GENERATION = ( + os.environ.get('USER_PERMISSIONS_FEATURES_IMAGE_GENERATION', 'True').lower() == 'true' +) + +USER_PERMISSIONS_FEATURES_CODE_INTERPRETER = ( + os.environ.get('USER_PERMISSIONS_FEATURES_CODE_INTERPRETER', 'True').lower() == 'true' +) + +USER_PERMISSIONS_FEATURES_FOLDERS = os.environ.get('USER_PERMISSIONS_FEATURES_FOLDERS', 'True').lower() == 'true' + +USER_PERMISSIONS_FEATURES_NOTES = os.environ.get('USER_PERMISSIONS_FEATURES_NOTES', 'True').lower() == 'true' + +USER_PERMISSIONS_FEATURES_CHANNELS = os.environ.get('USER_PERMISSIONS_FEATURES_CHANNELS', 'True').lower() == 'true' + +USER_PERMISSIONS_FEATURES_API_KEYS = os.environ.get('USER_PERMISSIONS_FEATURES_API_KEYS', 'False').lower() == 'true' + +USER_PERMISSIONS_FEATURES_MEMORIES = os.environ.get('USER_PERMISSIONS_FEATURES_MEMORIES', 'True').lower() == 'true' + +USER_PERMISSIONS_FEATURES_AUTOMATIONS = ( + os.environ.get('USER_PERMISSIONS_FEATURES_AUTOMATIONS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_FEATURES_CALENDAR = os.environ.get('USER_PERMISSIONS_FEATURES_CALENDAR', 'True').lower() == 'true' + + +USER_PERMISSIONS_SETTINGS_INTERFACE = os.environ.get('USER_PERMISSIONS_SETTINGS_INTERFACE', 'True').lower() == 'true' + + +DEFAULT_USER_PERMISSIONS = { + 'workspace': { + 'models': USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS, + 'knowledge': USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS, + 'prompts': USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS, + 'tools': USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS, + 'skills': USER_PERMISSIONS_WORKSPACE_SKILLS_ACCESS, + 'models_import': USER_PERMISSIONS_WORKSPACE_MODELS_IMPORT, + 'models_export': USER_PERMISSIONS_WORKSPACE_MODELS_EXPORT, + 'prompts_import': USER_PERMISSIONS_WORKSPACE_PROMPTS_IMPORT, + 'prompts_export': USER_PERMISSIONS_WORKSPACE_PROMPTS_EXPORT, + 'tools_import': USER_PERMISSIONS_WORKSPACE_TOOLS_IMPORT, + 'tools_export': USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT, + }, + 'sharing': { + 'models': USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING, + 'public_models': USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING, + 'knowledge': USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING, + 'public_knowledge': USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING, + 'prompts': USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING, + 'public_prompts': USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING, + 'tools': USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING, + 'public_tools': USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING, + 'skills': USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_SHARING, + 'public_skills': USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING, + 'notes': USER_PERMISSIONS_NOTES_ALLOW_SHARING, + 'public_notes': USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING, + }, + 'access_grants': { + 'allow_users': USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS, + }, + 'chat': { + 'controls': USER_PERMISSIONS_CHAT_CONTROLS, + 'valves': USER_PERMISSIONS_CHAT_VALVES, + 'system_prompt': USER_PERMISSIONS_CHAT_SYSTEM_PROMPT, + 'params': USER_PERMISSIONS_CHAT_PARAMS, + 'file_upload': USER_PERMISSIONS_CHAT_FILE_UPLOAD, + 'web_upload': USER_PERMISSIONS_CHAT_WEB_UPLOAD, + 'delete': USER_PERMISSIONS_CHAT_DELETE, + 'delete_message': USER_PERMISSIONS_CHAT_DELETE_MESSAGE, + 'continue_response': USER_PERMISSIONS_CHAT_CONTINUE_RESPONSE, + 'regenerate_response': USER_PERMISSIONS_CHAT_REGENERATE_RESPONSE, + 'rate_response': USER_PERMISSIONS_CHAT_RATE_RESPONSE, + 'edit': USER_PERMISSIONS_CHAT_EDIT, + 'share': USER_PERMISSIONS_CHAT_SHARE, + 'export': USER_PERMISSIONS_CHAT_EXPORT, + 'stt': USER_PERMISSIONS_CHAT_STT, + 'tts': USER_PERMISSIONS_CHAT_TTS, + 'call': USER_PERMISSIONS_CHAT_CALL, + 'multiple_models': USER_PERMISSIONS_CHAT_MULTIPLE_MODELS, + 'temporary': USER_PERMISSIONS_CHAT_TEMPORARY, + 'temporary_enforced': USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED, + }, + 'features': { + # General features + 'api_keys': USER_PERMISSIONS_FEATURES_API_KEYS, + 'notes': USER_PERMISSIONS_FEATURES_NOTES, + 'folders': USER_PERMISSIONS_FEATURES_FOLDERS, + 'channels': USER_PERMISSIONS_FEATURES_CHANNELS, + 'direct_tool_servers': USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS, + # Chat features + 'web_search': USER_PERMISSIONS_FEATURES_WEB_SEARCH, + 'image_generation': USER_PERMISSIONS_FEATURES_IMAGE_GENERATION, + 'code_interpreter': USER_PERMISSIONS_FEATURES_CODE_INTERPRETER, + 'memories': USER_PERMISSIONS_FEATURES_MEMORIES, + 'automations': USER_PERMISSIONS_FEATURES_AUTOMATIONS, + 'calendar': USER_PERMISSIONS_FEATURES_CALENDAR, + }, + 'settings': { + 'interface': USER_PERMISSIONS_SETTINGS_INTERFACE, + }, +} + +USER_PERMISSIONS = PersistentConfig( + 'USER_PERMISSIONS', + 'user.permissions', + DEFAULT_USER_PERMISSIONS, +) + +ENABLE_FOLDERS = PersistentConfig( + 'ENABLE_FOLDERS', + 'folders.enable', + os.environ.get('ENABLE_FOLDERS', 'True').lower() == 'true', +) + +FOLDER_MAX_FILE_COUNT = PersistentConfig( + 'FOLDER_MAX_FILE_COUNT', + 'folders.max_file_count', + os.environ.get('FOLDER_MAX_FILE_COUNT', ''), +) + +ENABLE_CHANNELS = PersistentConfig( + 'ENABLE_CHANNELS', + 'channels.enable', + os.environ.get('ENABLE_CHANNELS', 'False').lower() == 'true', +) + +ENABLE_CALENDAR = PersistentConfig( + 'ENABLE_CALENDAR', + 'calendar.enable', + os.environ.get('ENABLE_CALENDAR', 'True').lower() == 'true', +) + +ENABLE_AUTOMATIONS = PersistentConfig( + 'ENABLE_AUTOMATIONS', + 'automations.enable', + os.environ.get('ENABLE_AUTOMATIONS', 'True').lower() == 'true', +) + +AUTOMATION_MAX_COUNT = PersistentConfig( + 'AUTOMATION_MAX_COUNT', + 'automations.max_count', + os.environ.get('AUTOMATION_MAX_COUNT', ''), +) + +AUTOMATION_MIN_INTERVAL = PersistentConfig( + 'AUTOMATION_MIN_INTERVAL', + 'automations.min_interval', + os.environ.get('AUTOMATION_MIN_INTERVAL', ''), +) + +ENABLE_NOTES = PersistentConfig( + 'ENABLE_NOTES', + 'notes.enable', + os.environ.get('ENABLE_NOTES', 'True').lower() == 'true', +) + +ENABLE_USER_STATUS = PersistentConfig( + 'ENABLE_USER_STATUS', + 'users.enable_status', + os.environ.get('ENABLE_USER_STATUS', 'True').lower() == 'true', +) + +ENABLE_EVALUATION_ARENA_MODELS = PersistentConfig( + 'ENABLE_EVALUATION_ARENA_MODELS', + 'evaluation.arena.enable', + os.environ.get('ENABLE_EVALUATION_ARENA_MODELS', 'True').lower() == 'true', +) +EVALUATION_ARENA_MODELS = PersistentConfig( + 'EVALUATION_ARENA_MODELS', + 'evaluation.arena.models', + [], +) + +DEFAULT_ARENA_MODEL = { + 'id': 'arena-model', + 'name': 'Arena Model', + 'meta': { + 'profile_image_url': '/favicon.png', + 'description': 'Submit your questions to anonymous AI chatbots and vote on the best response.', + 'model_ids': None, + }, +} + +WEBHOOK_URL = PersistentConfig('WEBHOOK_URL', 'webhook_url', os.environ.get('WEBHOOK_URL', '')) + +ENABLE_ADMIN_EXPORT = os.environ.get('ENABLE_ADMIN_EXPORT', 'True').lower() == 'true' + +ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS = ( + os.environ.get('ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS', 'True').lower() == 'true' +) + +BYPASS_ADMIN_ACCESS_CONTROL = ( + os.environ.get( + 'BYPASS_ADMIN_ACCESS_CONTROL', + os.environ.get('ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS', 'True'), + ).lower() + == 'true' +) + +ENABLE_ADMIN_CHAT_ACCESS = os.environ.get('ENABLE_ADMIN_CHAT_ACCESS', 'True').lower() == 'true' + +ENABLE_ADMIN_ANALYTICS = os.environ.get('ENABLE_ADMIN_ANALYTICS', 'True').lower() == 'true' + +ENABLE_COMMUNITY_SHARING = PersistentConfig( + 'ENABLE_COMMUNITY_SHARING', + 'ui.enable_community_sharing', + os.environ.get('ENABLE_COMMUNITY_SHARING', 'True').lower() == 'true', +) + +ENABLE_MESSAGE_RATING = PersistentConfig( + 'ENABLE_MESSAGE_RATING', + 'ui.enable_message_rating', + os.environ.get('ENABLE_MESSAGE_RATING', 'True').lower() == 'true', +) + +ENABLE_USER_WEBHOOKS = PersistentConfig( + 'ENABLE_USER_WEBHOOKS', + 'ui.enable_user_webhooks', + os.environ.get('ENABLE_USER_WEBHOOKS', 'False').lower() == 'true', +) + +# FastAPI / AnyIO settings +THREAD_POOL_SIZE = os.getenv('THREAD_POOL_SIZE', None) + +if THREAD_POOL_SIZE is not None and isinstance(THREAD_POOL_SIZE, str): + try: + THREAD_POOL_SIZE = int(THREAD_POOL_SIZE) + except ValueError: + log.warning(f'THREAD_POOL_SIZE is not a valid integer: {THREAD_POOL_SIZE}. Defaulting to None.') + THREAD_POOL_SIZE = None + + +def validate_cors_origin(origin): + parsed_url = urlparse(origin) + + # Check if the scheme is either http or https, or a custom scheme + schemes = ['http', 'https'] + CORS_ALLOW_CUSTOM_SCHEME + if parsed_url.scheme not in schemes: + raise ValueError( + f"Invalid scheme in CORS_ALLOW_ORIGIN: '{origin}'. Only 'http' and 'https' and CORS_ALLOW_CUSTOM_SCHEME are allowed." + ) + + # Ensure that the netloc (domain + port) is present, indicating it's a valid URL + if not parsed_url.netloc: + raise ValueError(f"Invalid URL structure in CORS_ALLOW_ORIGIN: '{origin}'.") + + +# For production, you should only need one host as +# fastapi serves the svelte-kit built frontend and backend from the same host and port. +# To test CORS_ALLOW_ORIGIN locally, you can set something like +# CORS_ALLOW_ORIGIN=http://localhost:5173;http://localhost:8080 +# in your .env file depending on your frontend port, 5173 in this case. +CORS_ALLOW_ORIGIN = os.environ.get('CORS_ALLOW_ORIGIN', '*').split(';') + +# Allows custom URL schemes (e.g., app://) to be used as origins for CORS. +# Useful for local development or desktop clients with schemes like app:// or other custom protocols. +# Provide a semicolon-separated list of allowed schemes in the environment variable CORS_ALLOW_CUSTOM_SCHEMES. +CORS_ALLOW_CUSTOM_SCHEME = os.environ.get('CORS_ALLOW_CUSTOM_SCHEME', '').split(';') + +if CORS_ALLOW_ORIGIN == ['*']: + log.warning("\n\nWARNING: CORS_ALLOW_ORIGIN IS SET TO '*' - NOT RECOMMENDED FOR PRODUCTION DEPLOYMENTS.\n") +else: + # You have to pick between a single wildcard or a list of origins. + # Doing both will result in CORS errors in the browser. + for origin in CORS_ALLOW_ORIGIN: + validate_cors_origin(origin) + + +class BannerModel(BaseModel): + id: str + type: str + title: Optional[str] = None + content: str + dismissible: bool + timestamp: int + + +try: + banners = json.loads(os.environ.get('WEBUI_BANNERS', '[]')) + banners = [BannerModel(**banner) for banner in banners] +except Exception as e: + log.exception(f'Error loading WEBUI_BANNERS: {e}') + banners = [] + +WEBUI_BANNERS = PersistentConfig('WEBUI_BANNERS', 'ui.banners', banners) + + +SHOW_ADMIN_DETAILS = PersistentConfig( + 'SHOW_ADMIN_DETAILS', + 'auth.admin.show', + os.environ.get('SHOW_ADMIN_DETAILS', 'true').lower() == 'true', +) + +ADMIN_EMAIL = PersistentConfig( + 'ADMIN_EMAIL', + 'auth.admin.email', + os.environ.get('ADMIN_EMAIL', None), +) + + +#################################### +# TASKS +#################################### + + +TASK_MODEL = PersistentConfig( + 'TASK_MODEL', + 'task.model.default', + os.environ.get('TASK_MODEL', ''), +) + +TASK_MODEL_EXTERNAL = PersistentConfig( + 'TASK_MODEL_EXTERNAL', + 'task.model.external', + os.environ.get('TASK_MODEL_EXTERNAL', ''), +) + +TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig( + 'TITLE_GENERATION_PROMPT_TEMPLATE', + 'task.title.prompt_template', + os.environ.get('TITLE_GENERATION_PROMPT_TEMPLATE', ''), +) + +DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = """### Task: +Generate a concise, 3-5 word title with an emoji summarizing the chat history. +### Guidelines: +- The title should clearly represent the main theme or subject of the conversation. +- Use emojis that enhance understanding of the topic, but avoid quotation marks or special formatting. +- Write the title in the chat's primary language; default to English if multilingual. +- Prioritize accuracy over excessive creativity; keep it clear and simple. +- Your entire response must consist solely of the JSON object, without any introductory or concluding text. +- The output must be a single, raw JSON object, without any markdown code fences or other encapsulating text. +- Ensure no conversational text, affirmations, or explanations precede or follow the raw JSON output, as this will cause direct parsing failure. +### Output: +JSON format: { "title": "your concise title here" } +### Examples: +- { "title": "📉 Stock Market Trends" }, +- { "title": "🍪 Perfect Chocolate Chip Recipe" }, +- { "title": "Evolution of Music Streaming" }, +- { "title": "Remote Work Productivity Tips" }, +- { "title": "Artificial Intelligence in Healthcare" }, +- { "title": "🎮 Video Game Development Insights" } +### Chat History: + +{{MESSAGES:END:2}} +""" + +TAGS_GENERATION_PROMPT_TEMPLATE = PersistentConfig( + 'TAGS_GENERATION_PROMPT_TEMPLATE', + 'task.tags.prompt_template', + os.environ.get('TAGS_GENERATION_PROMPT_TEMPLATE', ''), +) + +DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE = """### Task: +Generate 1-3 broad tags categorizing the main themes of the chat history, along with 1-3 more specific subtopic tags. + +### Guidelines: +- Start with high-level domains (e.g. Science, Technology, Philosophy, Arts, Politics, Business, Health, Sports, Entertainment, Education) +- Consider including relevant subfields/subdomains if they are strongly represented throughout the conversation +- If content is too short (less than 3 messages) or too diverse, use only ["General"] +- Use the chat's primary language; default to English if multilingual +- Prioritize accuracy over specificity + +### Output: +JSON format: { "tags": ["tag1", "tag2", "tag3"] } + +### Chat History: + +{{MESSAGES:END:6}} +""" + +IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = PersistentConfig( + 'IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE', + 'task.image.prompt_template', + os.environ.get('IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE', ''), +) + +DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = """### Task: +Generate a detailed prompt for am image generation task based on the given language and context. Describe the image as if you were explaining it to someone who cannot see it. Include relevant details, colors, shapes, and any other important elements. + +### Guidelines: +- Be descriptive and detailed, focusing on the most important aspects of the image. +- Avoid making assumptions or adding information not present in the image. +- Use the chat's primary language; default to English if multilingual. +- If the image is too complex, focus on the most prominent elements. + +### Output: +Strictly return in JSON format: +{ + "prompt": "Your detailed description here." +} + +### Chat History: + +{{MESSAGES:END:6}} +""" + + +FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = PersistentConfig( + 'FOLLOW_UP_GENERATION_PROMPT_TEMPLATE', + 'task.follow_up.prompt_template', + os.environ.get('FOLLOW_UP_GENERATION_PROMPT_TEMPLATE', ''), +) + +DEFAULT_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = """### Task: +Suggest 3-5 relevant follow-up questions or prompts that the user might naturally ask next in this conversation as a **user**, based on the chat history, to help continue or deepen the discussion. +### Guidelines: +- Write all follow-up questions from the user’s point of view, directed to the assistant. +- Make questions concise, clear, and directly related to the discussed topic(s). +- Only suggest follow-ups that make sense given the chat content and do not repeat what was already covered. +- If the conversation is very short or not specific, suggest more general (but relevant) follow-ups the user might ask. +- Use the conversation's primary language; default to English if multilingual. +- Response must be a JSON object with a "follow_ups" key containing an array of strings, no extra text or formatting. +### Output: +JSON format: { "follow_ups": ["Question 1?", "Question 2?", "Question 3?"] } +### Chat History: + +{{MESSAGES:END:6}} +""" + +ENABLE_FOLLOW_UP_GENERATION = PersistentConfig( + 'ENABLE_FOLLOW_UP_GENERATION', + 'task.follow_up.enable', + os.environ.get('ENABLE_FOLLOW_UP_GENERATION', 'True').lower() == 'true', +) + +ENABLE_TAGS_GENERATION = PersistentConfig( + 'ENABLE_TAGS_GENERATION', + 'task.tags.enable', + os.environ.get('ENABLE_TAGS_GENERATION', 'True').lower() == 'true', +) + +ENABLE_TITLE_GENERATION = PersistentConfig( + 'ENABLE_TITLE_GENERATION', + 'task.title.enable', + os.environ.get('ENABLE_TITLE_GENERATION', 'True').lower() == 'true', +) + + +ENABLE_SEARCH_QUERY_GENERATION = PersistentConfig( + 'ENABLE_SEARCH_QUERY_GENERATION', + 'task.query.search.enable', + os.environ.get('ENABLE_SEARCH_QUERY_GENERATION', 'True').lower() == 'true', +) + +ENABLE_RETRIEVAL_QUERY_GENERATION = PersistentConfig( + 'ENABLE_RETRIEVAL_QUERY_GENERATION', + 'task.query.retrieval.enable', + os.environ.get('ENABLE_RETRIEVAL_QUERY_GENERATION', 'True').lower() == 'true', +) + + +QUERY_GENERATION_PROMPT_TEMPLATE = PersistentConfig( + 'QUERY_GENERATION_PROMPT_TEMPLATE', + 'task.query.prompt_template', + os.environ.get('QUERY_GENERATION_PROMPT_TEMPLATE', ''), +) + +DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE = """### Task: +Analyze the chat history to determine the necessity of generating search queries, in the given language. By default, **prioritize generating 1-3 broad and relevant search queries** unless it is absolutely certain that no additional information is required. The aim is to retrieve comprehensive, updated, and valuable information even with minimal uncertainty. If no search is unequivocally needed, return an empty list. + +### Guidelines: +- Respond **EXCLUSIVELY** with a JSON object. Any form of extra commentary, explanation, or additional text is strictly prohibited. +- When generating search queries, respond in the format: { "queries": ["query1", "query2"] }, ensuring each query is distinct, concise, and relevant to the topic. +- If and only if it is entirely certain that no useful results can be retrieved by a search, return: { "queries": [] }. +- Err on the side of suggesting search queries if there is **any chance** they might provide useful or updated information. +- Be concise and focused on composing high-quality search queries, avoiding unnecessary elaboration, commentary, or assumptions. +- Today's date is: {{CURRENT_DATE}}. +- Always prioritize providing actionable and broad queries that maximize informational coverage. + +### Output: +Strictly return in JSON format: +{ + "queries": ["query1", "query2"] +} + +### Chat History: + +{{MESSAGES:END:6}} + +""" + +ENABLE_AUTOCOMPLETE_GENERATION = PersistentConfig( + 'ENABLE_AUTOCOMPLETE_GENERATION', + 'task.autocomplete.enable', + os.environ.get('ENABLE_AUTOCOMPLETE_GENERATION', 'False').lower() == 'true', +) + +AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = PersistentConfig( + 'AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH', + 'task.autocomplete.input_max_length', + int(os.environ.get('AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH', '-1')), +) + +AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = PersistentConfig( + 'AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE', + 'task.autocomplete.prompt_template', + os.environ.get('AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE', ''), +) + + +DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = """### Task: +You are an autocompletion system. Continue the text in `` based on the **completion type** in `` and the given language. + +### **Instructions**: +1. Analyze `` for context and meaning. +2. Use `` to guide your output: + - **General**: Provide a natural, concise continuation. + - **Search Query**: Complete as if generating a realistic search query. +3. Start as if you are directly continuing ``. Do **not** repeat, paraphrase, or respond as a model. Simply complete the text. +4. Ensure the continuation: + - Flows naturally from ``. + - Avoids repetition, overexplaining, or unrelated ideas. +5. If unsure, return: `{ "text": "" }`. + +### **Output Rules**: +- Respond only in JSON format: `{ "text": "" }`. + +### **Examples**: +#### Example 1: +Input: +General +The sun was setting over the horizon, painting the sky +Output: +{ "text": "with vibrant shades of orange and pink." } + +#### Example 2: +Input: +Search Query +Top-rated restaurants in +Output: +{ "text": "New York City for Italian cuisine." } + +--- +### Context: + +{{MESSAGES:END:6}} + +{{TYPE}} +{{PROMPT}} +#### Output: +""" + + +VOICE_MODE_PROMPT_TEMPLATE = PersistentConfig( + 'VOICE_MODE_PROMPT_TEMPLATE', + 'task.voice.prompt_template', + os.environ.get('VOICE_MODE_PROMPT_TEMPLATE', ''), +) + +DEFAULT_VOICE_MODE_PROMPT_TEMPLATE = """You are a friendly, concise voice assistant. + +Everything you say will be spoken aloud. +Keep responses short, clear, and natural. + +STYLE: +- Use simple words and short sentences. +- Sound warm and conversational. +- Avoid long explanations, lists, or complex phrasing. + +BEHAVIOR: +- Give the quickest helpful answer first. +- Offer extra detail only if needed. +- Ask for clarification only when necessary. + +VOICE OPTIMIZATION: +- Break information into small, easy-to-hear chunks. +- Avoid dense wording or anything that sounds like reading text. + +ERROR HANDLING: +- If unsure, say so briefly and offer options. +- If something is unsafe or impossible, decline kindly and suggest a safe alternative. + +Stay consistent, helpful, and easy to listen to.""" + +TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig( + 'TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE', + 'task.tools.prompt_template', + os.environ.get('TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE', ''), +) + + +DEFAULT_TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = """Available Tools: {{TOOLS}} + +Your task is to choose and return the correct tool(s) from the list of available tools based on the query. Follow these guidelines: + +- Return only the JSON object, without any additional text or explanation. + +- If no tools match the query, return an empty array: + { + "tool_calls": [] + } + +- If one or more tools match the query, construct a JSON response containing a "tool_calls" array with objects that include: + - "name": The tool's name. + - "parameters": A dictionary of required parameters and their corresponding values. + +The format for the JSON response is strictly: +{ + "tool_calls": [ + {"name": "toolName1", "parameters": {"key1": "value1"}}, + {"name": "toolName2", "parameters": {"key2": "value2"}} + ] +}""" + + +DEFAULT_EMOJI_GENERATION_PROMPT_TEMPLATE = """Your task is to reflect the speaker's likely facial expression through a fitting emoji. Interpret emotions from the message and reflect their facial expression using fitting, diverse emojis (e.g., 😊, 😢, 😡, 😱). + +Message: ```{{prompt}}```""" + +DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE = """You have been provided with a set of responses from various models to the latest user query: "{{prompt}}" + +Your task is to synthesize these responses into a single, high-quality response. It is crucial to critically evaluate the information provided in these responses, recognizing that some of it may be biased or incorrect. Your response should not simply replicate the given answers but should offer a refined, accurate, and comprehensive reply to the instruction. Ensure your response is well-structured, coherent, and adheres to the highest standards of accuracy and reliability. + +Responses from models: {{responses}}""" + + +#################################### +# Code Interpreter +#################################### + +ENABLE_CODE_EXECUTION = PersistentConfig( + 'ENABLE_CODE_EXECUTION', + 'code_execution.enable', + os.environ.get('ENABLE_CODE_EXECUTION', 'True').lower() == 'true', +) + +CODE_EXECUTION_ENGINE = PersistentConfig( + 'CODE_EXECUTION_ENGINE', + 'code_execution.engine', + os.environ.get('CODE_EXECUTION_ENGINE', 'pyodide'), +) + +CODE_EXECUTION_JUPYTER_URL = PersistentConfig( + 'CODE_EXECUTION_JUPYTER_URL', + 'code_execution.jupyter.url', + os.environ.get('CODE_EXECUTION_JUPYTER_URL', ''), +) + +CODE_EXECUTION_JUPYTER_AUTH = PersistentConfig( + 'CODE_EXECUTION_JUPYTER_AUTH', + 'code_execution.jupyter.auth', + os.environ.get('CODE_EXECUTION_JUPYTER_AUTH', ''), +) + +CODE_EXECUTION_JUPYTER_AUTH_TOKEN = PersistentConfig( + 'CODE_EXECUTION_JUPYTER_AUTH_TOKEN', + 'code_execution.jupyter.auth_token', + os.environ.get('CODE_EXECUTION_JUPYTER_AUTH_TOKEN', ''), +) + + +CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = PersistentConfig( + 'CODE_EXECUTION_JUPYTER_AUTH_PASSWORD', + 'code_execution.jupyter.auth_password', + os.environ.get('CODE_EXECUTION_JUPYTER_AUTH_PASSWORD', ''), +) + +CODE_EXECUTION_JUPYTER_TIMEOUT = PersistentConfig( + 'CODE_EXECUTION_JUPYTER_TIMEOUT', + 'code_execution.jupyter.timeout', + int(os.environ.get('CODE_EXECUTION_JUPYTER_TIMEOUT', '60')), +) + +ENABLE_CODE_INTERPRETER = PersistentConfig( + 'ENABLE_CODE_INTERPRETER', + 'code_interpreter.enable', + os.environ.get('ENABLE_CODE_INTERPRETER', 'True').lower() == 'true', +) + +ENABLE_MEMORIES = PersistentConfig( + 'ENABLE_MEMORIES', + 'memories.enable', + os.environ.get('ENABLE_MEMORIES', 'True').lower() == 'true', +) + +CODE_INTERPRETER_ENGINE = PersistentConfig( + 'CODE_INTERPRETER_ENGINE', + 'code_interpreter.engine', + os.environ.get('CODE_INTERPRETER_ENGINE', 'pyodide'), +) + +CODE_INTERPRETER_PROMPT_TEMPLATE = PersistentConfig( + 'CODE_INTERPRETER_PROMPT_TEMPLATE', + 'code_interpreter.prompt_template', + os.environ.get('CODE_INTERPRETER_PROMPT_TEMPLATE', ''), +) + +CODE_INTERPRETER_JUPYTER_URL = PersistentConfig( + 'CODE_INTERPRETER_JUPYTER_URL', + 'code_interpreter.jupyter.url', + os.environ.get('CODE_INTERPRETER_JUPYTER_URL', os.environ.get('CODE_EXECUTION_JUPYTER_URL', '')), +) + +CODE_INTERPRETER_JUPYTER_AUTH = PersistentConfig( + 'CODE_INTERPRETER_JUPYTER_AUTH', + 'code_interpreter.jupyter.auth', + os.environ.get( + 'CODE_INTERPRETER_JUPYTER_AUTH', + os.environ.get('CODE_EXECUTION_JUPYTER_AUTH', ''), + ), +) + +CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = PersistentConfig( + 'CODE_INTERPRETER_JUPYTER_AUTH_TOKEN', + 'code_interpreter.jupyter.auth_token', + os.environ.get( + 'CODE_INTERPRETER_JUPYTER_AUTH_TOKEN', + os.environ.get('CODE_EXECUTION_JUPYTER_AUTH_TOKEN', ''), + ), +) + + +CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = PersistentConfig( + 'CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD', + 'code_interpreter.jupyter.auth_password', + os.environ.get( + 'CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD', + os.environ.get('CODE_EXECUTION_JUPYTER_AUTH_PASSWORD', ''), + ), +) + +CODE_INTERPRETER_JUPYTER_TIMEOUT = PersistentConfig( + 'CODE_INTERPRETER_JUPYTER_TIMEOUT', + 'code_interpreter.jupyter.timeout', + int( + os.environ.get( + 'CODE_INTERPRETER_JUPYTER_TIMEOUT', + os.environ.get('CODE_EXECUTION_JUPYTER_TIMEOUT', '60'), + ) + ), +) + +CODE_INTERPRETER_BLOCKED_MODULES = [ + library.strip() for library in os.environ.get('CODE_INTERPRETER_BLOCKED_MODULES', '').split(',') if library.strip() +] + +DEFAULT_CODE_INTERPRETER_PROMPT = """ +#### Code Interpreter + +You have access to a Python code interpreter via: `` + +- The Python shell runs directly in the user's browser for fast execution of analysis, calculations, or problem-solving. Use it in this response. +- You can use a wide array of libraries for data manipulation, visualization, API calls, or any computational task. Think outside the box and harness Python's full potential. +- **You must enclose your code within `` XML tags** and stop right away. If you don't, the code won't execute. +- Do NOT use triple backticks (```py ... ```) inside the XML tags — that is markdown formatting, not executable Python code. +- **Always print meaningful outputs** (results, tables, summaries, visuals). Avoid implicit outputs; use explicit print statements. +- After obtaining output, **provide a concise analysis, interpretation, or next steps** to help the user understand the findings. +- If results are unclear or unexpected, refine the code and re-execute. Iterate until you deliver meaningful insights. +- **If a link to an image, audio, or any file appears in the output, display it exactly as-is** in your response so the user can access it. Do not modify the link. +- Respond in the chat's primary language. Default to English if multilingual. + +Ensure the code interpreter is effectively utilized to achieve the highest-quality analysis for the user.""" + +# Appended to the code interpreter prompt only when engine is pyodide (not jupyter) +CODE_INTERPRETER_PYODIDE_PROMPT = """ + +##### Pyodide Environment + +- This Python environment runs via Pyodide in the browser. **Do not install packages** — `pip install`, `subprocess`, and `micropip.install()` are not available. +- If a required library is unavailable, use an alternative approach with available modules. Do not attempt to install anything. + +##### Persistent File System + +- User-uploaded files are available at `/mnt/uploads/`. When the user asks you to work with their files, read from this directory. +- You can also write output files to `/mnt/uploads/` so the user can access and download them from the file browser. +- The file system persists across code executions within the same session. +- Use `import os; os.listdir('/mnt/uploads')` to discover available files.""" + + +#################################### +# Vector Database +#################################### + +VECTOR_DB = os.environ.get('VECTOR_DB', 'chroma') + +# Chroma +CHROMA_DATA_PATH = f'{DATA_DIR}/vector_db' + +if VECTOR_DB == 'chroma': + import chromadb + + CHROMA_TENANT = os.environ.get('CHROMA_TENANT', chromadb.DEFAULT_TENANT) + CHROMA_DATABASE = os.environ.get('CHROMA_DATABASE', chromadb.DEFAULT_DATABASE) + CHROMA_HTTP_HOST = os.environ.get('CHROMA_HTTP_HOST', '') + CHROMA_HTTP_PORT = int(os.environ.get('CHROMA_HTTP_PORT', '8000')) + CHROMA_CLIENT_AUTH_PROVIDER = os.environ.get('CHROMA_CLIENT_AUTH_PROVIDER', '') + CHROMA_CLIENT_AUTH_CREDENTIALS = os.environ.get('CHROMA_CLIENT_AUTH_CREDENTIALS', '') + # Comma-separated list of header=value pairs + CHROMA_HTTP_HEADERS = os.environ.get('CHROMA_HTTP_HEADERS', '') + if CHROMA_HTTP_HEADERS: + CHROMA_HTTP_HEADERS = dict([pair.split('=') for pair in CHROMA_HTTP_HEADERS.split(',')]) + else: + CHROMA_HTTP_HEADERS = None + CHROMA_HTTP_SSL = os.environ.get('CHROMA_HTTP_SSL', 'false').lower() == 'true' +# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2) + + +# MariaDB Vector (mariadb-vector) +MARIADB_VECTOR_DB_URL = os.environ.get('MARIADB_VECTOR_DB_URL', '').strip() + +MARIADB_VECTOR_INITIALIZE_MAX_VECTOR_LENGTH = int( + os.environ.get('MARIADB_VECTOR_INITIALIZE_MAX_VECTOR_LENGTH', '1536').strip() or '1536' +) + +# Distance strategy: +# - cosine => vec_distance_cosine(...) +# - euclidean => vec_distance_euclidean(...) +MARIADB_VECTOR_DISTANCE_STRATEGY = os.environ.get('MARIADB_VECTOR_DISTANCE_STRATEGY', 'cosine').strip().lower() + +# HNSW M parameter (MariaDB VECTOR INDEX ... M=) +MARIADB_VECTOR_INDEX_M = int(os.environ.get('MARIADB_VECTOR_INDEX_M', '8').strip() or '8') + +# Pooling (MariaDB-Vector) +MARIADB_VECTOR_POOL_SIZE = os.environ.get('MARIADB_VECTOR_POOL_SIZE', None) + +if MARIADB_VECTOR_POOL_SIZE != None: + try: + MARIADB_VECTOR_POOL_SIZE = int(MARIADB_VECTOR_POOL_SIZE) + except Exception: + MARIADB_VECTOR_POOL_SIZE = None + +MARIADB_VECTOR_POOL_MAX_OVERFLOW = os.environ.get('MARIADB_VECTOR_POOL_MAX_OVERFLOW', 0) + +if MARIADB_VECTOR_POOL_MAX_OVERFLOW == '': + MARIADB_VECTOR_POOL_MAX_OVERFLOW = 0 +else: + try: + MARIADB_VECTOR_POOL_MAX_OVERFLOW = int(MARIADB_VECTOR_POOL_MAX_OVERFLOW) + except Exception: + MARIADB_VECTOR_POOL_MAX_OVERFLOW = 0 + +MARIADB_VECTOR_POOL_TIMEOUT = os.environ.get('MARIADB_VECTOR_POOL_TIMEOUT', 30) + +if MARIADB_VECTOR_POOL_TIMEOUT == '': + MARIADB_VECTOR_POOL_TIMEOUT = 30 +else: + try: + MARIADB_VECTOR_POOL_TIMEOUT = int(MARIADB_VECTOR_POOL_TIMEOUT) + except Exception: + MARIADB_VECTOR_POOL_TIMEOUT = 30 + +MARIADB_VECTOR_POOL_RECYCLE = os.environ.get('MARIADB_VECTOR_POOL_RECYCLE', 3600) + +if MARIADB_VECTOR_POOL_RECYCLE == '': + MARIADB_VECTOR_POOL_RECYCLE = 3600 +else: + try: + MARIADB_VECTOR_POOL_RECYCLE = int(MARIADB_VECTOR_POOL_RECYCLE) + except Exception: + MARIADB_VECTOR_POOL_RECYCLE = 3600 + +ENABLE_MARIADB_VECTOR = True +if VECTOR_DB == 'mariadb-vector': + if not MARIADB_VECTOR_DB_URL: + ENABLE_MARIADB_VECTOR = False + else: + try: + parsed = urlparse(MARIADB_VECTOR_DB_URL) + scheme = (parsed.scheme or '').lower() + # Require official driver so VECTOR binds as float32 bytes correctly + if scheme != 'mariadb+mariadbconnector': + ENABLE_MARIADB_VECTOR = False + except Exception: + ENABLE_MARIADB_VECTOR = False + + +# Milvus +MILVUS_URI = os.environ.get('MILVUS_URI', f'{DATA_DIR}/vector_db/milvus.db') +MILVUS_DB = os.environ.get('MILVUS_DB', 'default') +MILVUS_TOKEN = os.environ.get('MILVUS_TOKEN', None) +MILVUS_INDEX_TYPE = os.environ.get('MILVUS_INDEX_TYPE', 'HNSW') +MILVUS_METRIC_TYPE = os.environ.get('MILVUS_METRIC_TYPE', 'COSINE') +MILVUS_HNSW_M = int(os.environ.get('MILVUS_HNSW_M', '16')) +MILVUS_HNSW_EFCONSTRUCTION = int(os.environ.get('MILVUS_HNSW_EFCONSTRUCTION', '100')) +MILVUS_IVF_FLAT_NLIST = int(os.environ.get('MILVUS_IVF_FLAT_NLIST', '128')) +MILVUS_DISKANN_MAX_DEGREE = int(os.environ.get('MILVUS_DISKANN_MAX_DEGREE', '56')) +MILVUS_DISKANN_SEARCH_LIST_SIZE = int(os.environ.get('MILVUS_DISKANN_SEARCH_LIST_SIZE', '100')) +ENABLE_MILVUS_MULTITENANCY_MODE = os.environ.get('ENABLE_MILVUS_MULTITENANCY_MODE', 'false').lower() == 'true' +# Hyphens not allowed, need to use underscores in collection names +MILVUS_COLLECTION_PREFIX = os.environ.get('MILVUS_COLLECTION_PREFIX', 'open_webui') + +# Qdrant +QDRANT_URI = os.environ.get('QDRANT_URI', None) +QDRANT_API_KEY = os.environ.get('QDRANT_API_KEY', None) +QDRANT_ON_DISK = os.environ.get('QDRANT_ON_DISK', 'false').lower() == 'true' +QDRANT_PREFER_GRPC = os.environ.get('QDRANT_PREFER_GRPC', 'false').lower() == 'true' +QDRANT_GRPC_PORT = int(os.environ.get('QDRANT_GRPC_PORT', '6334')) +QDRANT_TIMEOUT = int(os.environ.get('QDRANT_TIMEOUT', '5')) +QDRANT_HNSW_M = int(os.environ.get('QDRANT_HNSW_M', '16')) +ENABLE_QDRANT_MULTITENANCY_MODE = os.environ.get('ENABLE_QDRANT_MULTITENANCY_MODE', 'true').lower() == 'true' +QDRANT_COLLECTION_PREFIX = os.environ.get('QDRANT_COLLECTION_PREFIX', 'open-webui') + +WEAVIATE_HTTP_HOST = os.environ.get('WEAVIATE_HTTP_HOST', '') +WEAVIATE_GRPC_HOST = os.environ.get('WEAVIATE_GRPC_HOST', '') +WEAVIATE_HTTP_PORT = int(os.environ.get('WEAVIATE_HTTP_PORT', '8080')) +WEAVIATE_GRPC_PORT = int(os.environ.get('WEAVIATE_GRPC_PORT', '50051')) +WEAVIATE_API_KEY = os.environ.get('WEAVIATE_API_KEY') +WEAVIATE_HTTP_SECURE = os.environ.get('WEAVIATE_HTTP_SECURE', 'false').lower() == 'true' +WEAVIATE_GRPC_SECURE = os.environ.get('WEAVIATE_GRPC_SECURE', 'false').lower() == 'true' +WEAVIATE_SKIP_INIT_CHECKS = os.environ.get('WEAVIATE_SKIP_INIT_CHECKS', 'false').lower() == 'true' + +# OpenSearch +OPENSEARCH_URI = os.environ.get('OPENSEARCH_URI', 'https://localhost:9200') +OPENSEARCH_SSL = os.environ.get('OPENSEARCH_SSL', 'true').lower() == 'true' +OPENSEARCH_CERT_VERIFY = os.environ.get('OPENSEARCH_CERT_VERIFY', 'false').lower() == 'true' +OPENSEARCH_USERNAME = os.environ.get('OPENSEARCH_USERNAME', None) +OPENSEARCH_PASSWORD = os.environ.get('OPENSEARCH_PASSWORD', None) + +# ElasticSearch +ELASTICSEARCH_URL = os.environ.get('ELASTICSEARCH_URL', 'https://localhost:9200') +ELASTICSEARCH_CA_CERTS = os.environ.get('ELASTICSEARCH_CA_CERTS', None) +ELASTICSEARCH_API_KEY = os.environ.get('ELASTICSEARCH_API_KEY', None) +ELASTICSEARCH_USERNAME = os.environ.get('ELASTICSEARCH_USERNAME', None) +ELASTICSEARCH_PASSWORD = os.environ.get('ELASTICSEARCH_PASSWORD', None) +ELASTICSEARCH_CLOUD_ID = os.environ.get('ELASTICSEARCH_CLOUD_ID', None) +SSL_ASSERT_FINGERPRINT = os.environ.get('SSL_ASSERT_FINGERPRINT', None) +ELASTICSEARCH_INDEX_PREFIX = os.environ.get('ELASTICSEARCH_INDEX_PREFIX', 'open_webui_collections') +# Pgvector +PGVECTOR_DB_URL = os.environ.get('PGVECTOR_DB_URL', DATABASE_URL) +if VECTOR_DB == 'pgvector' and not PGVECTOR_DB_URL.startswith('postgres'): + raise ValueError( + 'Pgvector requires setting PGVECTOR_DB_URL or using Postgres with vector extension as the primary database.' + ) +PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH = int(os.environ.get('PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH', '1536')) + +PGVECTOR_USE_HALFVEC = os.getenv('PGVECTOR_USE_HALFVEC', 'false').lower() == 'true' + +if PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH > 2000 and not PGVECTOR_USE_HALFVEC: + raise ValueError( + 'PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH is set to ' + f'{PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH}, which exceeds the 2000 dimension limit of the ' + "'vector' type. Set PGVECTOR_USE_HALFVEC=true to enable the 'halfvec' " + 'type required for high-dimensional embeddings.' + ) + +PGVECTOR_CREATE_EXTENSION = os.getenv('PGVECTOR_CREATE_EXTENSION', 'true').lower() == 'true' +PGVECTOR_PGCRYPTO = os.getenv('PGVECTOR_PGCRYPTO', 'false').lower() == 'true' +PGVECTOR_PGCRYPTO_KEY = os.getenv('PGVECTOR_PGCRYPTO_KEY', None) +if PGVECTOR_PGCRYPTO and not PGVECTOR_PGCRYPTO_KEY: + raise ValueError('PGVECTOR_PGCRYPTO is enabled but PGVECTOR_PGCRYPTO_KEY is not set. Please provide a valid key.') + + +PGVECTOR_POOL_SIZE = os.environ.get('PGVECTOR_POOL_SIZE', None) + +if PGVECTOR_POOL_SIZE != None: + try: + PGVECTOR_POOL_SIZE = int(PGVECTOR_POOL_SIZE) + except Exception: + PGVECTOR_POOL_SIZE = None + +PGVECTOR_POOL_MAX_OVERFLOW = os.environ.get('PGVECTOR_POOL_MAX_OVERFLOW', 0) + +if PGVECTOR_POOL_MAX_OVERFLOW == '': + PGVECTOR_POOL_MAX_OVERFLOW = 0 +else: + try: + PGVECTOR_POOL_MAX_OVERFLOW = int(PGVECTOR_POOL_MAX_OVERFLOW) + except Exception: + PGVECTOR_POOL_MAX_OVERFLOW = 0 + +PGVECTOR_POOL_TIMEOUT = os.environ.get('PGVECTOR_POOL_TIMEOUT', 30) + +if PGVECTOR_POOL_TIMEOUT == '': + PGVECTOR_POOL_TIMEOUT = 30 +else: + try: + PGVECTOR_POOL_TIMEOUT = int(PGVECTOR_POOL_TIMEOUT) + except Exception: + PGVECTOR_POOL_TIMEOUT = 30 + +PGVECTOR_POOL_RECYCLE = os.environ.get('PGVECTOR_POOL_RECYCLE', 3600) + +if PGVECTOR_POOL_RECYCLE == '': + PGVECTOR_POOL_RECYCLE = 3600 +else: + try: + PGVECTOR_POOL_RECYCLE = int(PGVECTOR_POOL_RECYCLE) + except Exception: + PGVECTOR_POOL_RECYCLE = 3600 + +PGVECTOR_INDEX_METHOD = os.getenv('PGVECTOR_INDEX_METHOD', '').strip().lower() +if PGVECTOR_INDEX_METHOD not in ('ivfflat', 'hnsw', ''): + PGVECTOR_INDEX_METHOD = '' + +PGVECTOR_HNSW_M = os.environ.get('PGVECTOR_HNSW_M', 16) + +if PGVECTOR_HNSW_M == '': + PGVECTOR_HNSW_M = 16 +else: + try: + PGVECTOR_HNSW_M = int(PGVECTOR_HNSW_M) + except Exception: + PGVECTOR_HNSW_M = 16 + +PGVECTOR_HNSW_EF_CONSTRUCTION = os.environ.get('PGVECTOR_HNSW_EF_CONSTRUCTION', 64) + +if PGVECTOR_HNSW_EF_CONSTRUCTION == '': + PGVECTOR_HNSW_EF_CONSTRUCTION = 64 +else: + try: + PGVECTOR_HNSW_EF_CONSTRUCTION = int(PGVECTOR_HNSW_EF_CONSTRUCTION) + except Exception: + PGVECTOR_HNSW_EF_CONSTRUCTION = 64 + +PGVECTOR_IVFFLAT_LISTS = os.environ.get('PGVECTOR_IVFFLAT_LISTS', 100) + +if PGVECTOR_IVFFLAT_LISTS == '': + PGVECTOR_IVFFLAT_LISTS = 100 +else: + try: + PGVECTOR_IVFFLAT_LISTS = int(PGVECTOR_IVFFLAT_LISTS) + except Exception: + PGVECTOR_IVFFLAT_LISTS = 100 + +# openGauss +OPENGAUSS_DB_URL = os.environ.get('OPENGAUSS_DB_URL', DATABASE_URL) + +OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH = int(os.environ.get('OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH', '1536')) + +OPENGAUSS_POOL_SIZE = os.environ.get('OPENGAUSS_POOL_SIZE', None) + +if OPENGAUSS_POOL_SIZE != None: + try: + OPENGAUSS_POOL_SIZE = int(OPENGAUSS_POOL_SIZE) + except Exception: + OPENGAUSS_POOL_SIZE = None + +OPENGAUSS_POOL_MAX_OVERFLOW = os.environ.get('OPENGAUSS_POOL_MAX_OVERFLOW', 0) + +if OPENGAUSS_POOL_MAX_OVERFLOW == '': + OPENGAUSS_POOL_MAX_OVERFLOW = 0 +else: + try: + OPENGAUSS_POOL_MAX_OVERFLOW = int(OPENGAUSS_POOL_MAX_OVERFLOW) + except Exception: + OPENGAUSS_POOL_MAX_OVERFLOW = 0 + +OPENGAUSS_POOL_TIMEOUT = os.environ.get('OPENGAUSS_POOL_TIMEOUT', 30) + +if OPENGAUSS_POOL_TIMEOUT == '': + OPENGAUSS_POOL_TIMEOUT = 30 +else: + try: + OPENGAUSS_POOL_TIMEOUT = int(OPENGAUSS_POOL_TIMEOUT) + except Exception: + OPENGAUSS_POOL_TIMEOUT = 30 + +OPENGAUSS_POOL_RECYCLE = os.environ.get('OPENGAUSS_POOL_RECYCLE', 3600) + +if OPENGAUSS_POOL_RECYCLE == '': + OPENGAUSS_POOL_RECYCLE = 3600 +else: + try: + OPENGAUSS_POOL_RECYCLE = int(OPENGAUSS_POOL_RECYCLE) + except Exception: + OPENGAUSS_POOL_RECYCLE = 3600 + +# Pinecone +PINECONE_API_KEY = os.environ.get('PINECONE_API_KEY', None) +PINECONE_ENVIRONMENT = os.environ.get('PINECONE_ENVIRONMENT', None) +PINECONE_INDEX_NAME = os.getenv('PINECONE_INDEX_NAME', 'open-webui-index') +PINECONE_DIMENSION = int(os.getenv('PINECONE_DIMENSION', 1536)) # or 3072, 1024, 768 +PINECONE_METRIC = os.getenv('PINECONE_METRIC', 'cosine') +PINECONE_CLOUD = os.getenv('PINECONE_CLOUD', 'aws') # or "gcp" or "azure" + +# ORACLE23AI (Oracle23ai Vector Search) + +ORACLE_DB_USE_WALLET = os.environ.get('ORACLE_DB_USE_WALLET', 'false').lower() == 'true' +ORACLE_DB_USER = os.environ.get('ORACLE_DB_USER', None) # +ORACLE_DB_PASSWORD = os.environ.get('ORACLE_DB_PASSWORD', None) # +ORACLE_DB_DSN = os.environ.get('ORACLE_DB_DSN', None) # +ORACLE_WALLET_DIR = os.environ.get('ORACLE_WALLET_DIR', None) +ORACLE_WALLET_PASSWORD = os.environ.get('ORACLE_WALLET_PASSWORD', None) +ORACLE_VECTOR_LENGTH = os.environ.get('ORACLE_VECTOR_LENGTH', 768) + +ORACLE_DB_POOL_MIN = int(os.environ.get('ORACLE_DB_POOL_MIN', 2)) +ORACLE_DB_POOL_MAX = int(os.environ.get('ORACLE_DB_POOL_MAX', 10)) +ORACLE_DB_POOL_INCREMENT = int(os.environ.get('ORACLE_DB_POOL_INCREMENT', 1)) + + +if VECTOR_DB == 'oracle23ai': + if not ORACLE_DB_USER or not ORACLE_DB_PASSWORD or not ORACLE_DB_DSN: + raise ValueError('Oracle23ai requires setting ORACLE_DB_USER, ORACLE_DB_PASSWORD, and ORACLE_DB_DSN.') + if ORACLE_DB_USE_WALLET and (not ORACLE_WALLET_DIR or not ORACLE_WALLET_PASSWORD): + raise ValueError( + 'Oracle23ai requires setting ORACLE_WALLET_DIR and ORACLE_WALLET_PASSWORD when using wallet authentication.' + ) + +log.info(f'VECTOR_DB: {VECTOR_DB}') + +# S3 Vector +S3_VECTOR_BUCKET_NAME = os.environ.get('S3_VECTOR_BUCKET_NAME', None) +S3_VECTOR_REGION = os.environ.get('S3_VECTOR_REGION', None) + +#################################### +# Information Retrieval (RAG) +#################################### + + +# If configured, Google Drive will be available as an upload option. +ENABLE_GOOGLE_DRIVE_INTEGRATION = PersistentConfig( + 'ENABLE_GOOGLE_DRIVE_INTEGRATION', + 'google_drive.enable', + os.getenv('ENABLE_GOOGLE_DRIVE_INTEGRATION', 'False').lower() == 'true', +) + +GOOGLE_DRIVE_CLIENT_ID = PersistentConfig( + 'GOOGLE_DRIVE_CLIENT_ID', + 'google_drive.client_id', + os.environ.get('GOOGLE_DRIVE_CLIENT_ID', ''), +) + +GOOGLE_DRIVE_API_KEY = PersistentConfig( + 'GOOGLE_DRIVE_API_KEY', + 'google_drive.api_key', + os.environ.get('GOOGLE_DRIVE_API_KEY', ''), +) + +ENABLE_ONEDRIVE_INTEGRATION = PersistentConfig( + 'ENABLE_ONEDRIVE_INTEGRATION', + 'onedrive.enable', + os.getenv('ENABLE_ONEDRIVE_INTEGRATION', 'False').lower() == 'true', +) + + +ENABLE_ONEDRIVE_PERSONAL = os.environ.get('ENABLE_ONEDRIVE_PERSONAL', 'True').lower() == 'true' +ENABLE_ONEDRIVE_BUSINESS = os.environ.get('ENABLE_ONEDRIVE_BUSINESS', 'True').lower() == 'true' + +ONEDRIVE_CLIENT_ID = os.environ.get('ONEDRIVE_CLIENT_ID', '') +ONEDRIVE_CLIENT_ID_PERSONAL = os.environ.get('ONEDRIVE_CLIENT_ID_PERSONAL', ONEDRIVE_CLIENT_ID) +ONEDRIVE_CLIENT_ID_BUSINESS = os.environ.get('ONEDRIVE_CLIENT_ID_BUSINESS', ONEDRIVE_CLIENT_ID) + +ONEDRIVE_SHAREPOINT_URL = PersistentConfig( + 'ONEDRIVE_SHAREPOINT_URL', + 'onedrive.sharepoint_url', + os.environ.get('ONEDRIVE_SHAREPOINT_URL', ''), +) + +ONEDRIVE_SHAREPOINT_TENANT_ID = PersistentConfig( + 'ONEDRIVE_SHAREPOINT_TENANT_ID', + 'onedrive.sharepoint_tenant_id', + os.environ.get('ONEDRIVE_SHAREPOINT_TENANT_ID', ''), +) + +# RAG Content Extraction +CONTENT_EXTRACTION_ENGINE = PersistentConfig( + 'CONTENT_EXTRACTION_ENGINE', + 'rag.CONTENT_EXTRACTION_ENGINE', + os.environ.get('CONTENT_EXTRACTION_ENGINE', '').lower(), +) + +DATALAB_MARKER_API_KEY = PersistentConfig( + 'DATALAB_MARKER_API_KEY', + 'rag.datalab_marker_api_key', + os.environ.get('DATALAB_MARKER_API_KEY', ''), +) + +DATALAB_MARKER_API_BASE_URL = PersistentConfig( + 'DATALAB_MARKER_API_BASE_URL', + 'rag.datalab_marker_api_base_url', + os.environ.get('DATALAB_MARKER_API_BASE_URL', ''), +) + +DATALAB_MARKER_ADDITIONAL_CONFIG = PersistentConfig( + 'DATALAB_MARKER_ADDITIONAL_CONFIG', + 'rag.datalab_marker_additional_config', + os.environ.get('DATALAB_MARKER_ADDITIONAL_CONFIG', ''), +) + +DATALAB_MARKER_USE_LLM = PersistentConfig( + 'DATALAB_MARKER_USE_LLM', + 'rag.DATALAB_MARKER_USE_LLM', + os.environ.get('DATALAB_MARKER_USE_LLM', 'false').lower() == 'true', +) + +DATALAB_MARKER_SKIP_CACHE = PersistentConfig( + 'DATALAB_MARKER_SKIP_CACHE', + 'rag.datalab_marker_skip_cache', + os.environ.get('DATALAB_MARKER_SKIP_CACHE', 'false').lower() == 'true', +) + +DATALAB_MARKER_FORCE_OCR = PersistentConfig( + 'DATALAB_MARKER_FORCE_OCR', + 'rag.datalab_marker_force_ocr', + os.environ.get('DATALAB_MARKER_FORCE_OCR', 'false').lower() == 'true', +) + +DATALAB_MARKER_PAGINATE = PersistentConfig( + 'DATALAB_MARKER_PAGINATE', + 'rag.datalab_marker_paginate', + os.environ.get('DATALAB_MARKER_PAGINATE', 'false').lower() == 'true', +) + +DATALAB_MARKER_STRIP_EXISTING_OCR = PersistentConfig( + 'DATALAB_MARKER_STRIP_EXISTING_OCR', + 'rag.datalab_marker_strip_existing_ocr', + os.environ.get('DATALAB_MARKER_STRIP_EXISTING_OCR', 'false').lower() == 'true', +) + +DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION = PersistentConfig( + 'DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION', + 'rag.datalab_marker_disable_image_extraction', + os.environ.get('DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION', 'false').lower() == 'true', +) + +DATALAB_MARKER_FORMAT_LINES = PersistentConfig( + 'DATALAB_MARKER_FORMAT_LINES', + 'rag.datalab_marker_format_lines', + os.environ.get('DATALAB_MARKER_FORMAT_LINES', 'false').lower() == 'true', +) + +DATALAB_MARKER_OUTPUT_FORMAT = PersistentConfig( + 'DATALAB_MARKER_OUTPUT_FORMAT', + 'rag.datalab_marker_output_format', + os.environ.get('DATALAB_MARKER_OUTPUT_FORMAT', 'markdown'), +) + +MINERU_API_MODE = PersistentConfig( + 'MINERU_API_MODE', + 'rag.mineru_api_mode', + os.environ.get('MINERU_API_MODE', 'local'), # "local" or "cloud" +) + +MINERU_API_URL = PersistentConfig( + 'MINERU_API_URL', + 'rag.mineru_api_url', + os.environ.get('MINERU_API_URL', 'http://localhost:8000'), +) + +MINERU_API_TIMEOUT = PersistentConfig( + 'MINERU_API_TIMEOUT', + 'rag.mineru_api_timeout', + os.environ.get('MINERU_API_TIMEOUT', '300'), +) + +MINERU_API_KEY = PersistentConfig( + 'MINERU_API_KEY', + 'rag.mineru_api_key', + os.environ.get('MINERU_API_KEY', ''), +) + +mineru_params = os.getenv('MINERU_PARAMS', '') +try: + mineru_params = json.loads(mineru_params) +except json.JSONDecodeError: + mineru_params = {} + +MINERU_PARAMS = PersistentConfig( + 'MINERU_PARAMS', + 'rag.mineru_params', + mineru_params, +) + +EXTERNAL_DOCUMENT_LOADER_URL = PersistentConfig( + 'EXTERNAL_DOCUMENT_LOADER_URL', + 'rag.external_document_loader_url', + os.environ.get('EXTERNAL_DOCUMENT_LOADER_URL', ''), +) + +EXTERNAL_DOCUMENT_LOADER_API_KEY = PersistentConfig( + 'EXTERNAL_DOCUMENT_LOADER_API_KEY', + 'rag.external_document_loader_api_key', + os.environ.get('EXTERNAL_DOCUMENT_LOADER_API_KEY', ''), +) + +TIKA_SERVER_URL = PersistentConfig( + 'TIKA_SERVER_URL', + 'rag.tika_server_url', + os.getenv('TIKA_SERVER_URL', 'http://tika:9998'), # Default for sidecar deployment +) + +DOCLING_SERVER_URL = PersistentConfig( + 'DOCLING_SERVER_URL', + 'rag.docling_server_url', + os.getenv('DOCLING_SERVER_URL', 'http://docling:5001'), +) + +DOCLING_API_KEY = PersistentConfig( + 'DOCLING_API_KEY', + 'rag.docling_api_key', + os.getenv('DOCLING_API_KEY', ''), +) + +docling_params = os.getenv('DOCLING_PARAMS', '') +try: + docling_params = json.loads(docling_params) +except json.JSONDecodeError: + docling_params = {} + +DOCLING_PARAMS = PersistentConfig( + 'DOCLING_PARAMS', + 'rag.docling_params', + docling_params, +) + +DOCUMENT_INTELLIGENCE_ENDPOINT = PersistentConfig( + 'DOCUMENT_INTELLIGENCE_ENDPOINT', + 'rag.document_intelligence_endpoint', + os.getenv('DOCUMENT_INTELLIGENCE_ENDPOINT', ''), +) + +DOCUMENT_INTELLIGENCE_KEY = PersistentConfig( + 'DOCUMENT_INTELLIGENCE_KEY', + 'rag.document_intelligence_key', + os.getenv('DOCUMENT_INTELLIGENCE_KEY', ''), +) + +DOCUMENT_INTELLIGENCE_MODEL = PersistentConfig( + 'DOCUMENT_INTELLIGENCE_MODEL', + 'rag.document_intelligence_model', + os.getenv('DOCUMENT_INTELLIGENCE_MODEL', 'prebuilt-layout'), +) + +MISTRAL_OCR_API_BASE_URL = PersistentConfig( + 'MISTRAL_OCR_API_BASE_URL', + 'rag.MISTRAL_OCR_API_BASE_URL', + os.getenv('MISTRAL_OCR_API_BASE_URL', 'https://api.mistral.ai/v1'), +) + +MISTRAL_OCR_API_KEY = PersistentConfig( + 'MISTRAL_OCR_API_KEY', + 'rag.mistral_ocr_api_key', + os.getenv('MISTRAL_OCR_API_KEY', ''), +) + +PADDLEOCR_VL_BASE_URL = PersistentConfig( + 'PADDLEOCR_VL_BASE_URL', + 'rag.paddleocr_vl_base_url', + os.getenv('PADDLEOCR_VL_BASE_URL', 'http://localhost:8080'), +) + +PADDLEOCR_VL_TOKEN = PersistentConfig( + 'PADDLEOCR_VL_TOKEN', + 'rag.paddleocr_vl_token', + os.getenv('PADDLEOCR_VL_TOKEN', ''), +) + +BYPASS_EMBEDDING_AND_RETRIEVAL = PersistentConfig( + 'BYPASS_EMBEDDING_AND_RETRIEVAL', + 'rag.bypass_embedding_and_retrieval', + os.environ.get('BYPASS_EMBEDDING_AND_RETRIEVAL', 'False').lower() == 'true', +) + + +RAG_TOP_K = PersistentConfig('RAG_TOP_K', 'rag.top_k', int(os.environ.get('RAG_TOP_K', '3'))) +RAG_TOP_K_RERANKER = PersistentConfig( + 'RAG_TOP_K_RERANKER', + 'rag.top_k_reranker', + int(os.environ.get('RAG_TOP_K_RERANKER', '3')), +) +RAG_RELEVANCE_THRESHOLD = PersistentConfig( + 'RAG_RELEVANCE_THRESHOLD', + 'rag.relevance_threshold', + float(os.environ.get('RAG_RELEVANCE_THRESHOLD', '0.0')), +) +RAG_HYBRID_BM25_WEIGHT = PersistentConfig( + 'RAG_HYBRID_BM25_WEIGHT', + 'rag.hybrid_bm25_weight', + float(os.environ.get('RAG_HYBRID_BM25_WEIGHT', '0.5')), +) + +ENABLE_RAG_HYBRID_SEARCH = PersistentConfig( + 'ENABLE_RAG_HYBRID_SEARCH', + 'rag.enable_hybrid_search', + os.environ.get('ENABLE_RAG_HYBRID_SEARCH', '').lower() == 'true', +) + +ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS = PersistentConfig( + 'ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS', + 'rag.enable_hybrid_search_enriched_texts', + os.environ.get('ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS', 'False').lower() == 'true', +) + +RAG_FULL_CONTEXT = PersistentConfig( + 'RAG_FULL_CONTEXT', + 'rag.full_context', + os.getenv('RAG_FULL_CONTEXT', 'False').lower() == 'true', +) + +RAG_FILE_MAX_COUNT = PersistentConfig( + 'RAG_FILE_MAX_COUNT', + 'rag.file.max_count', + (int(os.environ.get('RAG_FILE_MAX_COUNT')) if os.environ.get('RAG_FILE_MAX_COUNT') else None), +) + +RAG_FILE_MAX_SIZE = PersistentConfig( + 'RAG_FILE_MAX_SIZE', + 'rag.file.max_size', + (int(os.environ.get('RAG_FILE_MAX_SIZE')) if os.environ.get('RAG_FILE_MAX_SIZE') else None), +) + +FILE_IMAGE_COMPRESSION_WIDTH = PersistentConfig( + 'FILE_IMAGE_COMPRESSION_WIDTH', + 'file.image_compression_width', + (int(os.environ.get('FILE_IMAGE_COMPRESSION_WIDTH')) if os.environ.get('FILE_IMAGE_COMPRESSION_WIDTH') else None), +) + +FILE_IMAGE_COMPRESSION_HEIGHT = PersistentConfig( + 'FILE_IMAGE_COMPRESSION_HEIGHT', + 'file.image_compression_height', + (int(os.environ.get('FILE_IMAGE_COMPRESSION_HEIGHT')) if os.environ.get('FILE_IMAGE_COMPRESSION_HEIGHT') else None), +) + + +RAG_ALLOWED_FILE_EXTENSIONS = PersistentConfig( + 'RAG_ALLOWED_FILE_EXTENSIONS', + 'rag.file.allowed_extensions', + [ext.strip() for ext in os.environ.get('RAG_ALLOWED_FILE_EXTENSIONS', '').split(',') if ext.strip()], +) + +RAG_EMBEDDING_ENGINE = PersistentConfig( + 'RAG_EMBEDDING_ENGINE', + 'rag.embedding_engine', + os.environ.get('RAG_EMBEDDING_ENGINE', ''), +) + +PDF_EXTRACT_IMAGES = PersistentConfig( + 'PDF_EXTRACT_IMAGES', + 'rag.pdf_extract_images', + os.environ.get('PDF_EXTRACT_IMAGES', 'False').lower() == 'true', +) + +PDF_LOADER_MODE = PersistentConfig( + 'PDF_LOADER_MODE', + 'rag.pdf_loader_mode', + os.environ.get('PDF_LOADER_MODE', 'page'), +) + +RAG_EMBEDDING_MODEL = PersistentConfig( + 'RAG_EMBEDDING_MODEL', + 'rag.embedding_model', + os.environ.get('RAG_EMBEDDING_MODEL', 'sentence-transformers/all-MiniLM-L6-v2'), +) +log.info(f'Embedding model set: {RAG_EMBEDDING_MODEL.value}') + +RAG_EMBEDDING_MODEL_AUTO_UPDATE = ( + not OFFLINE_MODE and os.environ.get('RAG_EMBEDDING_MODEL_AUTO_UPDATE', 'True').lower() == 'true' +) + +RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = ( + os.environ.get('RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE', 'True').lower() == 'true' +) + +RAG_EMBEDDING_BATCH_SIZE = PersistentConfig( + 'RAG_EMBEDDING_BATCH_SIZE', + 'rag.embedding_batch_size', + int(os.environ.get('RAG_EMBEDDING_BATCH_SIZE') or os.environ.get('RAG_EMBEDDING_OPENAI_BATCH_SIZE', '1')), +) + +ENABLE_ASYNC_EMBEDDING = PersistentConfig( + 'ENABLE_ASYNC_EMBEDDING', + 'rag.enable_async_embedding', + os.environ.get('ENABLE_ASYNC_EMBEDDING', 'True').lower() == 'true', +) + +RAG_EMBEDDING_CONCURRENT_REQUESTS = PersistentConfig( + 'RAG_EMBEDDING_CONCURRENT_REQUESTS', + 'rag.embedding_concurrent_requests', + int(os.getenv('RAG_EMBEDDING_CONCURRENT_REQUESTS', '0')), +) + +RAG_EMBEDDING_QUERY_PREFIX = os.environ.get('RAG_EMBEDDING_QUERY_PREFIX', None) + +RAG_EMBEDDING_CONTENT_PREFIX = os.environ.get('RAG_EMBEDDING_CONTENT_PREFIX', None) + +RAG_EMBEDDING_PREFIX_FIELD_NAME = os.environ.get('RAG_EMBEDDING_PREFIX_FIELD_NAME', None) + +RAG_RERANKING_ENGINE = PersistentConfig( + 'RAG_RERANKING_ENGINE', + 'rag.reranking_engine', + os.environ.get('RAG_RERANKING_ENGINE', ''), +) + +RAG_RERANKING_MODEL = PersistentConfig( + 'RAG_RERANKING_MODEL', + 'rag.reranking_model', + os.environ.get('RAG_RERANKING_MODEL', ''), +) +if RAG_RERANKING_MODEL.value != '': + log.info(f'Reranking model set: {RAG_RERANKING_MODEL.value}') + + +RAG_RERANKING_MODEL_AUTO_UPDATE = ( + not OFFLINE_MODE and os.environ.get('RAG_RERANKING_MODEL_AUTO_UPDATE', 'True').lower() == 'true' +) + +RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = ( + os.environ.get('RAG_RERANKING_MODEL_TRUST_REMOTE_CODE', 'True').lower() == 'true' +) + +RAG_RERANKING_BATCH_SIZE = PersistentConfig( + 'RAG_RERANKING_BATCH_SIZE', + 'rag.reranking_batch_size', + int(os.environ.get('RAG_RERANKING_BATCH_SIZE', '32')), +) + +RAG_EXTERNAL_RERANKER_URL = PersistentConfig( + 'RAG_EXTERNAL_RERANKER_URL', + 'rag.external_reranker_url', + os.environ.get('RAG_EXTERNAL_RERANKER_URL', ''), +) + +RAG_EXTERNAL_RERANKER_API_KEY = PersistentConfig( + 'RAG_EXTERNAL_RERANKER_API_KEY', + 'rag.external_reranker_api_key', + os.environ.get('RAG_EXTERNAL_RERANKER_API_KEY', ''), +) + +RAG_EXTERNAL_RERANKER_TIMEOUT = PersistentConfig( + 'RAG_EXTERNAL_RERANKER_TIMEOUT', + 'rag.external_reranker_timeout', + os.environ.get('RAG_EXTERNAL_RERANKER_TIMEOUT', ''), +) + + +RAG_TEXT_SPLITTER = PersistentConfig( + 'RAG_TEXT_SPLITTER', + 'rag.text_splitter', + os.environ.get('RAG_TEXT_SPLITTER', ''), +) + +ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER = PersistentConfig( + 'ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER', + 'rag.enable_markdown_header_text_splitter', + os.environ.get('ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER', 'True').lower() == 'true', +) + + +TIKTOKEN_CACHE_DIR = os.environ.get('TIKTOKEN_CACHE_DIR', f'{CACHE_DIR}/tiktoken') +TIKTOKEN_ENCODING_NAME = PersistentConfig( + 'TIKTOKEN_ENCODING_NAME', + 'rag.tiktoken_encoding_name', + os.environ.get('TIKTOKEN_ENCODING_NAME', 'cl100k_base'), +) + + +CHUNK_SIZE = PersistentConfig('CHUNK_SIZE', 'rag.chunk_size', int(os.environ.get('CHUNK_SIZE', '1000'))) + +CHUNK_MIN_SIZE_TARGET = PersistentConfig( + 'CHUNK_MIN_SIZE_TARGET', + 'rag.chunk_min_size_target', + int(os.environ.get('CHUNK_MIN_SIZE_TARGET', '0')), +) + +CHUNK_OVERLAP = PersistentConfig( + 'CHUNK_OVERLAP', + 'rag.chunk_overlap', + int(os.environ.get('CHUNK_OVERLAP', '100')), +) + +DEFAULT_RAG_TEMPLATE = """### Task: +Respond to the user query using the provided context, incorporating inline citations in the format [id] **only when the tag includes an explicit id attribute** (e.g., ). + +### Guidelines: +- If you don't know the answer, clearly state that. +- If uncertain, ask the user for clarification. +- Respond in the same language as the user's query. +- If the context is unreadable or of poor quality, inform the user and provide the best possible answer. +- If the answer isn't present in the context but you possess the knowledge, explain this to the user and provide the answer using your own understanding. +- **Only include inline citations using [id] (e.g., [1], [2]) when the tag includes an id attribute.** +- Do not cite if the tag does not contain an id attribute. +- Do not use XML tags in your response. +- Ensure citations are concise and directly related to the information provided. + +### Example of Citation: +If the user asks about a specific topic and the information is found in a source with a provided id attribute, the response should include the citation like in the following example: +* "According to the study, the proposed method increases efficiency by 20% [1]." + +### Output: +Provide a clear and direct response to the user's query, including inline citations in the format [id] only when the tag with id attribute is present in the context. + + +{{CONTEXT}} + +""" + +RAG_TEMPLATE = PersistentConfig( + 'RAG_TEMPLATE', + 'rag.template', + os.environ.get('RAG_TEMPLATE', DEFAULT_RAG_TEMPLATE), +) + +RAG_OPENAI_API_BASE_URL = PersistentConfig( + 'RAG_OPENAI_API_BASE_URL', + 'rag.openai_api_base_url', + os.getenv('RAG_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL), +) +RAG_OPENAI_API_KEY = PersistentConfig( + 'RAG_OPENAI_API_KEY', + 'rag.openai_api_key', + os.getenv('RAG_OPENAI_API_KEY', OPENAI_API_KEY), +) + +RAG_AZURE_OPENAI_BASE_URL = PersistentConfig( + 'RAG_AZURE_OPENAI_BASE_URL', + 'rag.azure_openai.base_url', + os.getenv('RAG_AZURE_OPENAI_BASE_URL', ''), +) +RAG_AZURE_OPENAI_API_KEY = PersistentConfig( + 'RAG_AZURE_OPENAI_API_KEY', + 'rag.azure_openai.api_key', + os.getenv('RAG_AZURE_OPENAI_API_KEY', ''), +) +RAG_AZURE_OPENAI_API_VERSION = PersistentConfig( + 'RAG_AZURE_OPENAI_API_VERSION', + 'rag.azure_openai.api_version', + os.getenv('RAG_AZURE_OPENAI_API_VERSION', ''), +) + +RAG_OLLAMA_BASE_URL = PersistentConfig( + 'RAG_OLLAMA_BASE_URL', + 'rag.ollama.url', + os.getenv('RAG_OLLAMA_BASE_URL', OLLAMA_BASE_URL), +) + +RAG_OLLAMA_API_KEY = PersistentConfig( + 'RAG_OLLAMA_API_KEY', + 'rag.ollama.key', + os.getenv('RAG_OLLAMA_API_KEY', ''), +) + + +ENABLE_RAG_LOCAL_WEB_FETCH = os.getenv('ENABLE_RAG_LOCAL_WEB_FETCH', 'False').lower() == 'true' + + +DEFAULT_WEB_FETCH_FILTER_LIST = [ + '!169.254.169.254', + '!fd00:ec2::254', + '!metadata.google.internal', + '!metadata.azure.com', + '!100.100.100.200', +] + +web_fetch_filter_list = os.getenv('WEB_FETCH_FILTER_LIST', '') +if web_fetch_filter_list == '': + web_fetch_filter_list = [] +else: + web_fetch_filter_list = [item.strip() for item in web_fetch_filter_list.split(',') if item.strip()] + +WEB_FETCH_FILTER_LIST = list(set(DEFAULT_WEB_FETCH_FILTER_LIST + web_fetch_filter_list)) + + +YOUTUBE_LOADER_LANGUAGE = PersistentConfig( + 'YOUTUBE_LOADER_LANGUAGE', + 'rag.youtube_loader_language', + os.getenv('YOUTUBE_LOADER_LANGUAGE', 'en').split(','), +) + +YOUTUBE_LOADER_PROXY_URL = PersistentConfig( + 'YOUTUBE_LOADER_PROXY_URL', + 'rag.youtube_loader_proxy_url', + os.getenv('YOUTUBE_LOADER_PROXY_URL', ''), +) + + +#################################### +# Web Search (RAG) +#################################### + +ENABLE_WEB_SEARCH = PersistentConfig( + 'ENABLE_WEB_SEARCH', + 'rag.web.search.enable', + os.getenv('ENABLE_WEB_SEARCH', 'False').lower() == 'true', +) + +WEB_SEARCH_ENGINE = PersistentConfig( + 'WEB_SEARCH_ENGINE', + 'rag.web.search.engine', + os.getenv('WEB_SEARCH_ENGINE', ''), +) + +BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = PersistentConfig( + 'BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL', + 'rag.web.search.bypass_embedding_and_retrieval', + os.getenv('BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL', 'False').lower() == 'true', +) + + +BYPASS_WEB_SEARCH_WEB_LOADER = PersistentConfig( + 'BYPASS_WEB_SEARCH_WEB_LOADER', + 'rag.web.search.bypass_web_loader', + os.getenv('BYPASS_WEB_SEARCH_WEB_LOADER', 'False').lower() == 'true', +) + +WEB_SEARCH_RESULT_COUNT = PersistentConfig( + 'WEB_SEARCH_RESULT_COUNT', + 'rag.web.search.result_count', + int(os.getenv('WEB_SEARCH_RESULT_COUNT', '3')), +) + + +try: + web_search_domain_filter_list = json.loads(os.getenv('WEB_SEARCH_DOMAIN_FILTER_LIST', '[]')) +except Exception as e: + web_search_domain_filter_list = [ + # "wikipedia.com", + # "wikimedia.org", + # "wikidata.org", + # "!stackoverflow.com", + ] + +# You can provide a list of your own websites to filter after performing a web search. +# This ensures the highest level of safety and reliability of the information sources. +WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig( + 'WEB_SEARCH_DOMAIN_FILTER_LIST', + 'rag.web.search.domain.filter_list', + web_search_domain_filter_list, +) + +WEB_SEARCH_CONCURRENT_REQUESTS = PersistentConfig( + 'WEB_SEARCH_CONCURRENT_REQUESTS', + 'rag.web.search.concurrent_requests', + int(os.getenv('WEB_SEARCH_CONCURRENT_REQUESTS', '0')), +) + +WEB_FETCH_MAX_CONTENT_LENGTH = PersistentConfig( + 'WEB_FETCH_MAX_CONTENT_LENGTH', + 'rag.web.fetch.max_content_length', + (int(os.environ.get('WEB_FETCH_MAX_CONTENT_LENGTH')) if os.environ.get('WEB_FETCH_MAX_CONTENT_LENGTH') else None), +) + +WEB_LOADER_ENGINE = PersistentConfig( + 'WEB_LOADER_ENGINE', + 'rag.web.loader.engine', + os.environ.get('WEB_LOADER_ENGINE', ''), +) + + +WEB_LOADER_CONCURRENT_REQUESTS = PersistentConfig( + 'WEB_LOADER_CONCURRENT_REQUESTS', + 'rag.web.loader.concurrent_requests', + int(os.getenv('WEB_LOADER_CONCURRENT_REQUESTS', '10')), +) + +WEB_LOADER_TIMEOUT = PersistentConfig( + 'WEB_LOADER_TIMEOUT', + 'rag.web.loader.timeout', + os.getenv('WEB_LOADER_TIMEOUT', ''), +) + + +ENABLE_WEB_LOADER_SSL_VERIFICATION = PersistentConfig( + 'ENABLE_WEB_LOADER_SSL_VERIFICATION', + 'rag.web.loader.ssl_verification', + os.environ.get('ENABLE_WEB_LOADER_SSL_VERIFICATION', 'True').lower() == 'true', +) + +WEB_SEARCH_TRUST_ENV = PersistentConfig( + 'WEB_SEARCH_TRUST_ENV', + 'rag.web.search.trust_env', + os.getenv('WEB_SEARCH_TRUST_ENV', 'False').lower() == 'true', +) + + +OLLAMA_CLOUD_WEB_SEARCH_API_KEY = PersistentConfig( + 'OLLAMA_CLOUD_WEB_SEARCH_API_KEY', + 'rag.web.search.ollama_cloud_api_key', + os.getenv('OLLAMA_CLOUD_API_KEY', ''), +) + +SEARXNG_QUERY_URL = PersistentConfig( + 'SEARXNG_QUERY_URL', + 'rag.web.search.searxng_query_url', + os.getenv('SEARXNG_QUERY_URL', ''), +) + +SEARXNG_LANGUAGE = PersistentConfig( + 'SEARXNG_LANGUAGE', + 'rag.web.search.searxng_language', + os.getenv('SEARXNG_LANGUAGE', 'all'), +) + +YACY_QUERY_URL = PersistentConfig( + 'YACY_QUERY_URL', + 'rag.web.search.yacy_query_url', + os.getenv('YACY_QUERY_URL', ''), +) + +YACY_USERNAME = PersistentConfig( + 'YACY_USERNAME', + 'rag.web.search.yacy_username', + os.getenv('YACY_USERNAME', ''), +) + +YACY_PASSWORD = PersistentConfig( + 'YACY_PASSWORD', + 'rag.web.search.yacy_password', + os.getenv('YACY_PASSWORD', ''), +) + +GOOGLE_PSE_API_KEY = PersistentConfig( + 'GOOGLE_PSE_API_KEY', + 'rag.web.search.google_pse_api_key', + os.getenv('GOOGLE_PSE_API_KEY', ''), +) + +GOOGLE_PSE_ENGINE_ID = PersistentConfig( + 'GOOGLE_PSE_ENGINE_ID', + 'rag.web.search.google_pse_engine_id', + os.getenv('GOOGLE_PSE_ENGINE_ID', ''), +) + +BRAVE_SEARCH_API_KEY = PersistentConfig( + 'BRAVE_SEARCH_API_KEY', + 'rag.web.search.brave_search_api_key', + os.getenv('BRAVE_SEARCH_API_KEY', ''), +) + +KAGI_SEARCH_API_KEY = PersistentConfig( + 'KAGI_SEARCH_API_KEY', + 'rag.web.search.kagi_search_api_key', + os.getenv('KAGI_SEARCH_API_KEY', ''), +) + +MOJEEK_SEARCH_API_KEY = PersistentConfig( + 'MOJEEK_SEARCH_API_KEY', + 'rag.web.search.mojeek_search_api_key', + os.getenv('MOJEEK_SEARCH_API_KEY', ''), +) + +BOCHA_SEARCH_API_KEY = PersistentConfig( + 'BOCHA_SEARCH_API_KEY', + 'rag.web.search.bocha_search_api_key', + os.getenv('BOCHA_SEARCH_API_KEY', ''), +) + +SERPSTACK_API_KEY = PersistentConfig( + 'SERPSTACK_API_KEY', + 'rag.web.search.serpstack_api_key', + os.getenv('SERPSTACK_API_KEY', ''), +) + +SERPSTACK_HTTPS = PersistentConfig( + 'SERPSTACK_HTTPS', + 'rag.web.search.serpstack_https', + os.getenv('SERPSTACK_HTTPS', 'True').lower() == 'true', +) + +SERPER_API_KEY = PersistentConfig( + 'SERPER_API_KEY', + 'rag.web.search.serper_api_key', + os.getenv('SERPER_API_KEY', ''), +) + +SERPLY_API_KEY = PersistentConfig( + 'SERPLY_API_KEY', + 'rag.web.search.serply_api_key', + os.getenv('SERPLY_API_KEY', ''), +) + +DDGS_BACKEND = PersistentConfig( + 'DDGS_BACKEND', + 'rag.web.search.ddgs_backend', + os.getenv('DDGS_BACKEND', 'auto'), +) + +JINA_API_KEY = PersistentConfig( + 'JINA_API_KEY', + 'rag.web.search.jina_api_key', + os.getenv('JINA_API_KEY', ''), +) + +JINA_API_BASE_URL = PersistentConfig( + 'JINA_API_BASE_URL', + 'rag.web.search.jina_api_base_url', + os.getenv('JINA_API_BASE_URL', ''), +) + +SEARCHAPI_API_KEY = PersistentConfig( + 'SEARCHAPI_API_KEY', + 'rag.web.search.searchapi_api_key', + os.getenv('SEARCHAPI_API_KEY', ''), +) + +SEARCHAPI_ENGINE = PersistentConfig( + 'SEARCHAPI_ENGINE', + 'rag.web.search.searchapi_engine', + os.getenv('SEARCHAPI_ENGINE', ''), +) + +SERPAPI_API_KEY = PersistentConfig( + 'SERPAPI_API_KEY', + 'rag.web.search.serpapi_api_key', + os.getenv('SERPAPI_API_KEY', ''), +) + +SERPAPI_ENGINE = PersistentConfig( + 'SERPAPI_ENGINE', + 'rag.web.search.serpapi_engine', + os.getenv('SERPAPI_ENGINE', ''), +) + +BING_SEARCH_V7_ENDPOINT = PersistentConfig( + 'BING_SEARCH_V7_ENDPOINT', + 'rag.web.search.bing_search_v7_endpoint', + os.environ.get('BING_SEARCH_V7_ENDPOINT', 'https://api.bing.microsoft.com/v7.0/search'), +) + +BING_SEARCH_V7_SUBSCRIPTION_KEY = PersistentConfig( + 'BING_SEARCH_V7_SUBSCRIPTION_KEY', + 'rag.web.search.bing_search_v7_subscription_key', + os.environ.get('BING_SEARCH_V7_SUBSCRIPTION_KEY', ''), +) + +AZURE_AI_SEARCH_API_KEY = PersistentConfig( + 'AZURE_AI_SEARCH_API_KEY', + 'rag.web.search.azure_ai_search_api_key', + os.environ.get('AZURE_AI_SEARCH_API_KEY', ''), +) + +AZURE_AI_SEARCH_ENDPOINT = PersistentConfig( + 'AZURE_AI_SEARCH_ENDPOINT', + 'rag.web.search.azure_ai_search_endpoint', + os.environ.get('AZURE_AI_SEARCH_ENDPOINT', ''), +) + +AZURE_AI_SEARCH_INDEX_NAME = PersistentConfig( + 'AZURE_AI_SEARCH_INDEX_NAME', + 'rag.web.search.azure_ai_search_index_name', + os.environ.get('AZURE_AI_SEARCH_INDEX_NAME', ''), +) + +EXA_API_KEY = PersistentConfig( + 'EXA_API_KEY', + 'rag.web.search.exa_api_key', + os.getenv('EXA_API_KEY', ''), +) + +PERPLEXITY_API_KEY = PersistentConfig( + 'PERPLEXITY_API_KEY', + 'rag.web.search.perplexity_api_key', + os.getenv('PERPLEXITY_API_KEY', ''), +) + +PERPLEXITY_MODEL = PersistentConfig( + 'PERPLEXITY_MODEL', + 'rag.web.search.perplexity_model', + os.getenv('PERPLEXITY_MODEL', 'sonar'), +) + +PERPLEXITY_SEARCH_CONTEXT_USAGE = PersistentConfig( + 'PERPLEXITY_SEARCH_CONTEXT_USAGE', + 'rag.web.search.perplexity_search_context_usage', + os.getenv('PERPLEXITY_SEARCH_CONTEXT_USAGE', 'medium'), +) + +PERPLEXITY_SEARCH_API_URL = PersistentConfig( + 'PERPLEXITY_SEARCH_API_URL', + 'rag.web.search.perplexity_search_api_url', + os.getenv('PERPLEXITY_SEARCH_API_URL', 'https://api.perplexity.ai/search'), +) + +SOUGOU_API_SID = PersistentConfig( + 'SOUGOU_API_SID', + 'rag.web.search.sougou_api_sid', + os.getenv('SOUGOU_API_SID', ''), +) + +SOUGOU_API_SK = PersistentConfig( + 'SOUGOU_API_SK', + 'rag.web.search.sougou_api_sk', + os.getenv('SOUGOU_API_SK', ''), +) + +TAVILY_API_KEY = PersistentConfig( + 'TAVILY_API_KEY', + 'rag.web.search.tavily_api_key', + os.getenv('TAVILY_API_KEY', ''), +) + +TAVILY_EXTRACT_DEPTH = PersistentConfig( + 'TAVILY_EXTRACT_DEPTH', + 'rag.web.search.tavily_extract_depth', + os.getenv('TAVILY_EXTRACT_DEPTH', 'basic'), +) + +PLAYWRIGHT_WS_URL = PersistentConfig( + 'PLAYWRIGHT_WS_URL', + 'rag.web.loader.playwright_ws_url', + os.environ.get('PLAYWRIGHT_WS_URL', ''), +) + +PLAYWRIGHT_TIMEOUT = PersistentConfig( + 'PLAYWRIGHT_TIMEOUT', + 'rag.web.loader.playwright_timeout', + int(os.environ.get('PLAYWRIGHT_TIMEOUT', '10000')), +) + +FIRECRAWL_API_KEY = PersistentConfig( + 'FIRECRAWL_API_KEY', + 'rag.web.loader.firecrawl_api_key', + os.environ.get('FIRECRAWL_API_KEY', ''), +) + +FIRECRAWL_API_BASE_URL = PersistentConfig( + 'FIRECRAWL_API_BASE_URL', + 'rag.web.loader.firecrawl_api_url', + os.environ.get('FIRECRAWL_API_BASE_URL', 'https://api.firecrawl.dev'), +) + +FIRECRAWL_TIMEOUT = PersistentConfig( + 'FIRECRAWL_TIMEOUT', + 'rag.web.loader.firecrawl_timeout', + os.environ.get('FIRECRAWL_TIMEOUT', ''), +) + +EXTERNAL_WEB_SEARCH_URL = PersistentConfig( + 'EXTERNAL_WEB_SEARCH_URL', + 'rag.web.search.external_web_search_url', + os.environ.get('EXTERNAL_WEB_SEARCH_URL', ''), +) + +EXTERNAL_WEB_SEARCH_API_KEY = PersistentConfig( + 'EXTERNAL_WEB_SEARCH_API_KEY', + 'rag.web.search.external_web_search_api_key', + os.environ.get('EXTERNAL_WEB_SEARCH_API_KEY', ''), +) + +EXTERNAL_WEB_LOADER_URL = PersistentConfig( + 'EXTERNAL_WEB_LOADER_URL', + 'rag.web.loader.external_web_loader_url', + os.environ.get('EXTERNAL_WEB_LOADER_URL', ''), +) + +EXTERNAL_WEB_LOADER_API_KEY = PersistentConfig( + 'EXTERNAL_WEB_LOADER_API_KEY', + 'rag.web.loader.external_web_loader_api_key', + os.environ.get('EXTERNAL_WEB_LOADER_API_KEY', ''), +) + +YANDEX_WEB_SEARCH_URL = PersistentConfig( + 'YANDEX_WEB_SEARCH_URL', + 'rag.web.search.yandex_web_search_url', + os.environ.get('YANDEX_WEB_SEARCH_URL', ''), +) + +YANDEX_WEB_SEARCH_API_KEY = PersistentConfig( + 'YANDEX_WEB_SEARCH_API_KEY', + 'rag.web.search.yandex_web_search_api_key', + os.environ.get('YANDEX_WEB_SEARCH_API_KEY', ''), +) + +YANDEX_WEB_SEARCH_CONFIG = PersistentConfig( + 'YANDEX_WEB_SEARCH_CONFIG', + 'rag.web.search.yandex_web_search_config', + os.environ.get('YANDEX_WEB_SEARCH_CONFIG', ''), +) + +YOUCOM_API_KEY = PersistentConfig( + 'YOUCOM_API_KEY', + 'rag.web.search.youcom_api_key', + os.environ.get('YOUCOM_API_KEY', ''), +) + +#################################### +# Images +#################################### + +ENABLE_IMAGE_GENERATION = PersistentConfig( + 'ENABLE_IMAGE_GENERATION', + 'image_generation.enable', + os.environ.get('ENABLE_IMAGE_GENERATION', '').lower() == 'true', +) + +IMAGE_GENERATION_ENGINE = PersistentConfig( + 'IMAGE_GENERATION_ENGINE', + 'image_generation.engine', + os.getenv('IMAGE_GENERATION_ENGINE', 'openai'), +) + +IMAGE_GENERATION_MODEL = PersistentConfig( + 'IMAGE_GENERATION_MODEL', + 'image_generation.model', + os.getenv('IMAGE_GENERATION_MODEL', ''), +) + +# Regex pattern for models that support IMAGE_SIZE = "auto". +IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN = os.getenv('IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN', '^gpt-image') + +# Regex pattern for models that return URLs instead of base64 data. +IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN = os.getenv('IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN', '^gpt-image') + +IMAGE_SIZE = PersistentConfig('IMAGE_SIZE', 'image_generation.size', os.getenv('IMAGE_SIZE', '512x512')) + +IMAGE_STEPS = PersistentConfig('IMAGE_STEPS', 'image_generation.steps', int(os.getenv('IMAGE_STEPS', 50))) + +ENABLE_IMAGE_PROMPT_GENERATION = PersistentConfig( + 'ENABLE_IMAGE_PROMPT_GENERATION', + 'image_generation.prompt.enable', + os.environ.get('ENABLE_IMAGE_PROMPT_GENERATION', 'true').lower() == 'true', +) + +AUTOMATIC1111_BASE_URL = PersistentConfig( + 'AUTOMATIC1111_BASE_URL', + 'image_generation.automatic1111.base_url', + os.getenv('AUTOMATIC1111_BASE_URL', ''), +) +AUTOMATIC1111_API_AUTH = PersistentConfig( + 'AUTOMATIC1111_API_AUTH', + 'image_generation.automatic1111.api_auth', + os.getenv('AUTOMATIC1111_API_AUTH', ''), +) + +automatic1111_params = os.getenv('AUTOMATIC1111_PARAMS', '') +try: + automatic1111_params = json.loads(automatic1111_params) +except json.JSONDecodeError: + automatic1111_params = {} + +AUTOMATIC1111_PARAMS = PersistentConfig( + 'AUTOMATIC1111_PARAMS', + 'image_generation.automatic1111.api_params', + automatic1111_params, +) + +COMFYUI_BASE_URL = PersistentConfig( + 'COMFYUI_BASE_URL', + 'image_generation.comfyui.base_url', + os.getenv('COMFYUI_BASE_URL', ''), +) + +COMFYUI_API_KEY = PersistentConfig( + 'COMFYUI_API_KEY', + 'image_generation.comfyui.api_key', + os.getenv('COMFYUI_API_KEY', ''), +) + +COMFYUI_DEFAULT_WORKFLOW = """ +{ + "3": { + "inputs": { + "seed": 0, + "steps": 20, + "cfg": 8, + "sampler_name": "euler", + "scheduler": "normal", + "denoise": 1, + "model": [ + "4", + 0 + ], + "positive": [ + "6", + 0 + ], + "negative": [ + "7", + 0 + ], + "latent_image": [ + "5", + 0 + ] + }, + "class_type": "KSampler", + "_meta": { + "title": "KSampler" + } + }, + "4": { + "inputs": { + "ckpt_name": "model.safetensors" + }, + "class_type": "CheckpointLoaderSimple", + "_meta": { + "title": "Load Checkpoint" + } + }, + "5": { + "inputs": { + "width": 512, + "height": 512, + "batch_size": 1 + }, + "class_type": "EmptyLatentImage", + "_meta": { + "title": "Empty Latent Image" + } + }, + "6": { + "inputs": { + "text": "Prompt", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "7": { + "inputs": { + "text": "", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "8": { + "inputs": { + "samples": [ + "3", + 0 + ], + "vae": [ + "4", + 2 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE Decode" + } + }, + "9": { + "inputs": { + "filename_prefix": "ComfyUI", + "images": [ + "8", + 0 + ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "Save Image" + } + } +} +""" + + +COMFYUI_WORKFLOW = PersistentConfig( + 'COMFYUI_WORKFLOW', + 'image_generation.comfyui.workflow', + os.getenv('COMFYUI_WORKFLOW', COMFYUI_DEFAULT_WORKFLOW), +) + +comfyui_workflow_nodes = os.getenv('COMFYUI_WORKFLOW_NODES', '') +try: + comfyui_workflow_nodes = json.loads(comfyui_workflow_nodes) +except json.JSONDecodeError: + comfyui_workflow_nodes = [] + +COMFYUI_WORKFLOW_NODES = PersistentConfig( + 'COMFYUI_WORKFLOW_NODES', + 'image_generation.comfyui.nodes', + comfyui_workflow_nodes, +) + +IMAGES_OPENAI_API_BASE_URL = PersistentConfig( + 'IMAGES_OPENAI_API_BASE_URL', + 'image_generation.openai.api_base_url', + os.getenv('IMAGES_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL), +) +IMAGES_OPENAI_API_VERSION = PersistentConfig( + 'IMAGES_OPENAI_API_VERSION', + 'image_generation.openai.api_version', + os.getenv('IMAGES_OPENAI_API_VERSION', ''), +) + +IMAGES_OPENAI_API_KEY = PersistentConfig( + 'IMAGES_OPENAI_API_KEY', + 'image_generation.openai.api_key', + os.getenv('IMAGES_OPENAI_API_KEY', OPENAI_API_KEY), +) + +images_openai_params = os.getenv('IMAGES_OPENAI_PARAMS', '') +try: + images_openai_params = json.loads(images_openai_params) +except json.JSONDecodeError: + images_openai_params = {} + + +IMAGES_OPENAI_API_PARAMS = PersistentConfig( + 'IMAGES_OPENAI_API_PARAMS', 'image_generation.openai.params', images_openai_params +) + + +IMAGES_GEMINI_API_BASE_URL = PersistentConfig( + 'IMAGES_GEMINI_API_BASE_URL', + 'image_generation.gemini.api_base_url', + os.getenv('IMAGES_GEMINI_API_BASE_URL', GEMINI_API_BASE_URL), +) +IMAGES_GEMINI_API_KEY = PersistentConfig( + 'IMAGES_GEMINI_API_KEY', + 'image_generation.gemini.api_key', + os.getenv('IMAGES_GEMINI_API_KEY', GEMINI_API_KEY), +) + +IMAGES_GEMINI_ENDPOINT_METHOD = PersistentConfig( + 'IMAGES_GEMINI_ENDPOINT_METHOD', + 'image_generation.gemini.endpoint_method', + os.getenv('IMAGES_GEMINI_ENDPOINT_METHOD', ''), +) + +ENABLE_IMAGE_EDIT = PersistentConfig( + 'ENABLE_IMAGE_EDIT', + 'images.edit.enable', + os.environ.get('ENABLE_IMAGE_EDIT', '').lower() == 'true', +) + +IMAGE_EDIT_ENGINE = PersistentConfig( + 'IMAGE_EDIT_ENGINE', + 'images.edit.engine', + os.getenv('IMAGE_EDIT_ENGINE', 'openai'), +) + +IMAGE_EDIT_MODEL = PersistentConfig( + 'IMAGE_EDIT_MODEL', + 'images.edit.model', + os.getenv('IMAGE_EDIT_MODEL', ''), +) + +IMAGE_EDIT_SIZE = PersistentConfig('IMAGE_EDIT_SIZE', 'images.edit.size', os.getenv('IMAGE_EDIT_SIZE', '')) + +IMAGES_EDIT_OPENAI_API_BASE_URL = PersistentConfig( + 'IMAGES_EDIT_OPENAI_API_BASE_URL', + 'images.edit.openai.api_base_url', + os.getenv('IMAGES_EDIT_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL), +) +IMAGES_EDIT_OPENAI_API_VERSION = PersistentConfig( + 'IMAGES_EDIT_OPENAI_API_VERSION', + 'images.edit.openai.api_version', + os.getenv('IMAGES_EDIT_OPENAI_API_VERSION', ''), +) + +IMAGES_EDIT_OPENAI_API_KEY = PersistentConfig( + 'IMAGES_EDIT_OPENAI_API_KEY', + 'images.edit.openai.api_key', + os.getenv('IMAGES_EDIT_OPENAI_API_KEY', OPENAI_API_KEY), +) + +IMAGES_EDIT_GEMINI_API_BASE_URL = PersistentConfig( + 'IMAGES_EDIT_GEMINI_API_BASE_URL', + 'images.edit.gemini.api_base_url', + os.getenv('IMAGES_EDIT_GEMINI_API_BASE_URL', GEMINI_API_BASE_URL), +) +IMAGES_EDIT_GEMINI_API_KEY = PersistentConfig( + 'IMAGES_EDIT_GEMINI_API_KEY', + 'images.edit.gemini.api_key', + os.getenv('IMAGES_EDIT_GEMINI_API_KEY', GEMINI_API_KEY), +) + + +IMAGES_EDIT_COMFYUI_BASE_URL = PersistentConfig( + 'IMAGES_EDIT_COMFYUI_BASE_URL', + 'images.edit.comfyui.base_url', + os.getenv('IMAGES_EDIT_COMFYUI_BASE_URL', ''), +) +IMAGES_EDIT_COMFYUI_API_KEY = PersistentConfig( + 'IMAGES_EDIT_COMFYUI_API_KEY', + 'images.edit.comfyui.api_key', + os.getenv('IMAGES_EDIT_COMFYUI_API_KEY', ''), +) + +IMAGES_EDIT_COMFYUI_WORKFLOW = PersistentConfig( + 'IMAGES_EDIT_COMFYUI_WORKFLOW', + 'images.edit.comfyui.workflow', + os.getenv('IMAGES_EDIT_COMFYUI_WORKFLOW', ''), +) + +images_edit_comfyui_workflow_nodes = os.getenv('IMAGES_EDIT_COMFYUI_WORKFLOW_NODES', '') +try: + images_edit_comfyui_workflow_nodes = json.loads(images_edit_comfyui_workflow_nodes) +except json.JSONDecodeError: + images_edit_comfyui_workflow_nodes = [] + +IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = PersistentConfig( + 'IMAGES_EDIT_COMFYUI_WORKFLOW_NODES', + 'images.edit.comfyui.nodes', + images_edit_comfyui_workflow_nodes, +) + +#################################### +# Audio +#################################### + +# Transcription +WHISPER_MODEL = PersistentConfig( + 'WHISPER_MODEL', + 'audio.stt.whisper_model', + os.getenv('WHISPER_MODEL', 'base'), +) + +WHISPER_COMPUTE_TYPE = os.getenv('WHISPER_COMPUTE_TYPE', 'int8') +WHISPER_MODEL_DIR = os.getenv('WHISPER_MODEL_DIR', f'{CACHE_DIR}/whisper/models') +WHISPER_MODEL_AUTO_UPDATE = not OFFLINE_MODE and os.environ.get('WHISPER_MODEL_AUTO_UPDATE', '').lower() == 'true' + +WHISPER_VAD_FILTER = os.getenv('WHISPER_VAD_FILTER', 'False').lower() == 'true' + +WHISPER_MULTILINGUAL = os.getenv('WHISPER_MULTILINGUAL', 'False').lower() == 'true' + +WHISPER_LANGUAGE = os.getenv('WHISPER_LANGUAGE', '').lower() or None + +# Add Deepgram configuration +DEEPGRAM_API_KEY = PersistentConfig( + 'DEEPGRAM_API_KEY', + 'audio.stt.deepgram.api_key', + os.getenv('DEEPGRAM_API_KEY', ''), +) + +# ElevenLabs configuration +ELEVENLABS_API_BASE_URL = os.getenv('ELEVENLABS_API_BASE_URL', 'https://api.elevenlabs.io') + +AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig( + 'AUDIO_STT_OPENAI_API_BASE_URL', + 'audio.stt.openai.api_base_url', + os.getenv('AUDIO_STT_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL), +) + +AUDIO_STT_OPENAI_API_KEY = PersistentConfig( + 'AUDIO_STT_OPENAI_API_KEY', + 'audio.stt.openai.api_key', + os.getenv('AUDIO_STT_OPENAI_API_KEY', OPENAI_API_KEY), +) + +AUDIO_STT_ENGINE = PersistentConfig( + 'AUDIO_STT_ENGINE', + 'audio.stt.engine', + os.getenv('AUDIO_STT_ENGINE', ''), +) + +AUDIO_STT_MODEL = PersistentConfig( + 'AUDIO_STT_MODEL', + 'audio.stt.model', + os.getenv('AUDIO_STT_MODEL', ''), +) + +AUDIO_STT_SUPPORTED_CONTENT_TYPES = PersistentConfig( + 'AUDIO_STT_SUPPORTED_CONTENT_TYPES', + 'audio.stt.supported_content_types', + [ + content_type.strip() + for content_type in os.environ.get('AUDIO_STT_SUPPORTED_CONTENT_TYPES', '').split(',') + if content_type.strip() + ], +) + +AUDIO_STT_AZURE_API_KEY = PersistentConfig( + 'AUDIO_STT_AZURE_API_KEY', + 'audio.stt.azure.api_key', + os.getenv('AUDIO_STT_AZURE_API_KEY', ''), +) + +AUDIO_STT_AZURE_REGION = PersistentConfig( + 'AUDIO_STT_AZURE_REGION', + 'audio.stt.azure.region', + os.getenv('AUDIO_STT_AZURE_REGION', ''), +) + +AUDIO_STT_AZURE_LOCALES = PersistentConfig( + 'AUDIO_STT_AZURE_LOCALES', + 'audio.stt.azure.locales', + os.getenv('AUDIO_STT_AZURE_LOCALES', ''), +) + +AUDIO_STT_AZURE_BASE_URL = PersistentConfig( + 'AUDIO_STT_AZURE_BASE_URL', + 'audio.stt.azure.base_url', + os.getenv('AUDIO_STT_AZURE_BASE_URL', ''), +) + +AUDIO_STT_AZURE_MAX_SPEAKERS = PersistentConfig( + 'AUDIO_STT_AZURE_MAX_SPEAKERS', + 'audio.stt.azure.max_speakers', + os.getenv('AUDIO_STT_AZURE_MAX_SPEAKERS', ''), +) + +AUDIO_STT_MISTRAL_API_KEY = PersistentConfig( + 'AUDIO_STT_MISTRAL_API_KEY', + 'audio.stt.mistral.api_key', + os.getenv('AUDIO_STT_MISTRAL_API_KEY', ''), +) + +AUDIO_STT_MISTRAL_API_BASE_URL = PersistentConfig( + 'AUDIO_STT_MISTRAL_API_BASE_URL', + 'audio.stt.mistral.api_base_url', + os.getenv('AUDIO_STT_MISTRAL_API_BASE_URL', 'https://api.mistral.ai/v1'), +) + +AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = PersistentConfig( + 'AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS', + 'audio.stt.mistral.use_chat_completions', + os.getenv('AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS', 'false').lower() == 'true', +) + +AUDIO_TTS_OPENAI_API_BASE_URL = PersistentConfig( + 'AUDIO_TTS_OPENAI_API_BASE_URL', + 'audio.tts.openai.api_base_url', + os.getenv('AUDIO_TTS_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL), +) +AUDIO_TTS_OPENAI_API_KEY = PersistentConfig( + 'AUDIO_TTS_OPENAI_API_KEY', + 'audio.tts.openai.api_key', + os.getenv('AUDIO_TTS_OPENAI_API_KEY', OPENAI_API_KEY), +) + +audio_tts_openai_params = os.getenv('AUDIO_TTS_OPENAI_PARAMS', '') +try: + audio_tts_openai_params = json.loads(audio_tts_openai_params) +except json.JSONDecodeError: + audio_tts_openai_params = {} + +AUDIO_TTS_OPENAI_PARAMS = PersistentConfig( + 'AUDIO_TTS_OPENAI_PARAMS', + 'audio.tts.openai.params', + audio_tts_openai_params, +) + + +AUDIO_TTS_API_KEY = PersistentConfig( + 'AUDIO_TTS_API_KEY', + 'audio.tts.api_key', + os.getenv('AUDIO_TTS_API_KEY', ''), +) + +AUDIO_TTS_ENGINE = PersistentConfig( + 'AUDIO_TTS_ENGINE', + 'audio.tts.engine', + os.getenv('AUDIO_TTS_ENGINE', ''), +) + + +AUDIO_TTS_MODEL = PersistentConfig( + 'AUDIO_TTS_MODEL', + 'audio.tts.model', + os.getenv('AUDIO_TTS_MODEL', 'tts-1'), # OpenAI default model +) + +AUDIO_TTS_VOICE = PersistentConfig( + 'AUDIO_TTS_VOICE', + 'audio.tts.voice', + os.getenv('AUDIO_TTS_VOICE', 'alloy'), # OpenAI default voice +) + +AUDIO_TTS_SPLIT_ON = PersistentConfig( + 'AUDIO_TTS_SPLIT_ON', + 'audio.tts.split_on', + os.getenv('AUDIO_TTS_SPLIT_ON', 'punctuation'), +) + +AUDIO_TTS_AZURE_SPEECH_REGION = PersistentConfig( + 'AUDIO_TTS_AZURE_SPEECH_REGION', + 'audio.tts.azure.speech_region', + os.getenv('AUDIO_TTS_AZURE_SPEECH_REGION', ''), +) + +AUDIO_TTS_AZURE_SPEECH_BASE_URL = PersistentConfig( + 'AUDIO_TTS_AZURE_SPEECH_BASE_URL', + 'audio.tts.azure.speech_base_url', + os.getenv('AUDIO_TTS_AZURE_SPEECH_BASE_URL', ''), +) + +AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT = PersistentConfig( + 'AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT', + 'audio.tts.azure.speech_output_format', + os.getenv('AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT', 'audio-24khz-160kbitrate-mono-mp3'), +) + +AUDIO_TTS_MISTRAL_API_KEY = PersistentConfig( + 'AUDIO_TTS_MISTRAL_API_KEY', + 'audio.tts.mistral.api_key', + os.getenv('AUDIO_TTS_MISTRAL_API_KEY', ''), +) + +AUDIO_TTS_MISTRAL_API_BASE_URL = PersistentConfig( + 'AUDIO_TTS_MISTRAL_API_BASE_URL', + 'audio.tts.mistral.api_base_url', + os.getenv('AUDIO_TTS_MISTRAL_API_BASE_URL', 'https://api.mistral.ai/v1'), +) + + +#################################### +# LDAP +#################################### + +ENABLE_LDAP = PersistentConfig( + 'ENABLE_LDAP', + 'ldap.enable', + os.environ.get('ENABLE_LDAP', 'false').lower() == 'true', +) + +LDAP_SERVER_LABEL = PersistentConfig( + 'LDAP_SERVER_LABEL', + 'ldap.server.label', + os.environ.get('LDAP_SERVER_LABEL', 'LDAP Server'), +) + +LDAP_SERVER_HOST = PersistentConfig( + 'LDAP_SERVER_HOST', + 'ldap.server.host', + os.environ.get('LDAP_SERVER_HOST', 'localhost'), +) + +LDAP_SERVER_PORT = PersistentConfig( + 'LDAP_SERVER_PORT', + 'ldap.server.port', + int(os.environ.get('LDAP_SERVER_PORT', '389')), +) + +LDAP_ATTRIBUTE_FOR_MAIL = PersistentConfig( + 'LDAP_ATTRIBUTE_FOR_MAIL', + 'ldap.server.attribute_for_mail', + os.environ.get('LDAP_ATTRIBUTE_FOR_MAIL', 'mail'), +) + +LDAP_ATTRIBUTE_FOR_USERNAME = PersistentConfig( + 'LDAP_ATTRIBUTE_FOR_USERNAME', + 'ldap.server.attribute_for_username', + os.environ.get('LDAP_ATTRIBUTE_FOR_USERNAME', 'uid'), +) + +LDAP_APP_DN = PersistentConfig('LDAP_APP_DN', 'ldap.server.app_dn', os.environ.get('LDAP_APP_DN', '')) + +LDAP_APP_PASSWORD = PersistentConfig( + 'LDAP_APP_PASSWORD', + 'ldap.server.app_password', + os.environ.get('LDAP_APP_PASSWORD', ''), +) + +LDAP_SEARCH_BASE = PersistentConfig('LDAP_SEARCH_BASE', 'ldap.server.users_dn', os.environ.get('LDAP_SEARCH_BASE', '')) + +LDAP_SEARCH_FILTERS = PersistentConfig( + 'LDAP_SEARCH_FILTER', + 'ldap.server.search_filter', + os.environ.get('LDAP_SEARCH_FILTER', os.environ.get('LDAP_SEARCH_FILTERS', '')), +) + +LDAP_USE_TLS = PersistentConfig( + 'LDAP_USE_TLS', + 'ldap.server.use_tls', + os.environ.get('LDAP_USE_TLS', 'True').lower() == 'true', +) + +LDAP_CA_CERT_FILE = PersistentConfig( + 'LDAP_CA_CERT_FILE', + 'ldap.server.ca_cert_file', + os.environ.get('LDAP_CA_CERT_FILE', ''), +) + +LDAP_VALIDATE_CERT = PersistentConfig( + 'LDAP_VALIDATE_CERT', + 'ldap.server.validate_cert', + os.environ.get('LDAP_VALIDATE_CERT', 'True').lower() == 'true', +) + +LDAP_CIPHERS = PersistentConfig('LDAP_CIPHERS', 'ldap.server.ciphers', os.environ.get('LDAP_CIPHERS', 'ALL')) + +# For LDAP Group Management +ENABLE_LDAP_GROUP_MANAGEMENT = PersistentConfig( + 'ENABLE_LDAP_GROUP_MANAGEMENT', + 'ldap.group.enable_management', + os.environ.get('ENABLE_LDAP_GROUP_MANAGEMENT', 'False').lower() == 'true', +) + +ENABLE_LDAP_GROUP_CREATION = PersistentConfig( + 'ENABLE_LDAP_GROUP_CREATION', + 'ldap.group.enable_creation', + os.environ.get('ENABLE_LDAP_GROUP_CREATION', 'False').lower() == 'true', +) + +LDAP_ATTRIBUTE_FOR_GROUPS = PersistentConfig( + 'LDAP_ATTRIBUTE_FOR_GROUPS', + 'ldap.server.attribute_for_groups', + os.environ.get('LDAP_ATTRIBUTE_FOR_GROUPS', 'memberOf'), +) diff --git a/backend/open_webui/constants.py b/backend/open_webui/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..ad1bdf4a207bacea56681a90a44262621d377ea8 --- /dev/null +++ b/backend/open_webui/constants.py @@ -0,0 +1,119 @@ +from enum import Enum + + +class MESSAGES(str, Enum): + DEFAULT = lambda msg='': f'{msg if msg else ""}' + MODEL_ADDED = lambda model='': f"The model '{model}' has been added successfully." + MODEL_DELETED = lambda model='': f"The model '{model}' has been deleted successfully." + + +class WEBHOOK_MESSAGES(str, Enum): + DEFAULT = lambda msg='': f'{msg if msg else ""}' + USER_SIGNUP = lambda username='': f'New user signed up: {username}' if username else 'New user signed up' + + +class ERROR_MESSAGES(str, Enum): + def __str__(self) -> str: + return super().__str__() + + DEFAULT = lambda err='': f'{"Something went wrong :/" if err == "" else "[ERROR: " + str(err) + "]"}' + ENV_VAR_NOT_FOUND = 'Required environment variable not found. Terminating now.' + CREATE_USER_ERROR = 'Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance.' + DELETE_USER_ERROR = 'Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot.' + EMAIL_MISMATCH = 'Uh-oh! This email does not match the email your provider is registered with. Please check your email and try again.' + EMAIL_TAKEN = 'Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew.' + USERNAME_TAKEN = 'Uh-oh! This username is already registered. Please choose another username.' + PASSWORD_TOO_LONG = ( + 'Uh-oh! The password you entered is too long. Please make sure your password is less than 72 bytes long.' + ) + COMMAND_TAKEN = 'Uh-oh! This command is already registered. Please choose another command string.' + FILE_EXISTS = 'Uh-oh! This file is already registered. Please choose another file.' + + ID_TAKEN = 'Uh-oh! This id is already registered. Please choose another id string.' + MODEL_ID_TAKEN = 'Uh-oh! This model id is already registered. Please choose another model id string.' + NAME_TAG_TAKEN = 'Uh-oh! This name tag is already registered. Please choose another name tag string.' + MODEL_ID_TOO_LONG = 'The model id is too long. Please make sure your model id is less than 256 characters long.' + + INVALID_TOKEN = 'Your session has expired or the token is invalid. Please sign in again.' + INVALID_CRED = 'The email or password provided is incorrect. Please check for typos and try logging in again.' + INVALID_EMAIL_FORMAT = "The email format you entered is invalid. Please double-check and make sure you're using a valid email address (e.g., yourname@example.com)." + INCORRECT_PASSWORD = 'The password provided is incorrect. Please check for typos and try again.' + INVALID_TRUSTED_HEADER = ( + 'Your provider has not provided a trusted header. Please contact your administrator for assistance.' + ) + + EXISTING_USERS = "You can't turn off authentication because there are existing users. If you want to disable WEBUI_AUTH, make sure your web interface doesn't have any existing users and is a fresh installation." + + UNAUTHORIZED = '401 Unauthorized' + ACCESS_PROHIBITED = ( + 'You do not have permission to access this resource. Please contact your administrator for assistance.' + ) + ACTION_PROHIBITED = 'The requested action has been restricted as a security measure.' + + FILE_NOT_SENT = 'FILE_NOT_SENT' + FILE_NOT_SUPPORTED = "Oops! It seems like the file format you're trying to upload is not supported. Please upload a file with a supported format and try again." + + NOT_FOUND = "We could not find what you're looking for :/" + USER_NOT_FOUND = "We could not find what you're looking for :/" + API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature." + API_KEY_NOT_ALLOWED = 'Use of API key is not enabled in the environment.' + + MALICIOUS = 'Unusual activities detected, please try again in a few minutes.' + + PANDOC_NOT_INSTALLED = 'Pandoc is not installed on the server. Please contact your administrator for assistance.' + INCORRECT_FORMAT = lambda err='': f'Invalid format. Please use the correct format{err}' + RATE_LIMIT_EXCEEDED = 'API rate limit exceeded' + + MODEL_NOT_FOUND = lambda name='': f"Model '{name}' was not found" + OPENAI_NOT_FOUND = lambda name='': 'OpenAI API was not found' + OLLAMA_NOT_FOUND = 'WebUI could not connect to Ollama' + CREATE_API_KEY_ERROR = 'Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance.' + API_KEY_CREATION_NOT_ALLOWED = 'API key creation is not allowed in the environment.' + + EMPTY_CONTENT = 'The content provided is empty. Please ensure that there is text or data present before proceeding.' + + DB_NOT_SQLITE = 'This feature is only available when running with SQLite databases.' + + INVALID_URL = 'Oops! The URL you provided is invalid. Please double-check and try again.' + + WEB_SEARCH_ERROR = lambda err='': f'{err if err else "Oops! Something went wrong while searching the web."}' + + OLLAMA_API_DISABLED = 'The Ollama API is disabled. Please enable it to use this feature.' + + FILE_TOO_LARGE = lambda size='': ( + f"Oops! The file you're trying to upload is too large. Please upload a file that is less than {size}." + ) + + DUPLICATE_CONTENT = 'Duplicate content detected. Please provide unique content to proceed.' + FILE_NOT_PROCESSED = ( + 'Extracted content is not available for this file. Please ensure that the file is processed before proceeding.' + ) + + INVALID_PASSWORD = lambda err='': err if err else 'The password does not meet the required validation criteria.' + + AUTOMATION_LIMIT_EXCEEDED = lambda size='': f'Automation limit reached ({size})' + AUTOMATION_TOO_FREQUENT = lambda interval='': f'Schedule too frequent. Minimum interval is {interval} seconds.' + AUTOMATION_INVALID_RRULE = lambda err='': f'Invalid RRULE: {err}' + AUTOMATION_NO_FUTURE_RUNS = 'RRULE has no future occurrences' + + FEATURE_DISABLED = lambda name='': f'{name} is disabled' + INPUT_TOO_LONG = lambda size='': f'Input prompt exceeds maximum length of {size}' + SERVER_CONNECTION_ERROR = 'Open WebUI: Server Connection Error' + REQUIRED_FIELD_EMPTY = lambda name='': f'Required field {name} is empty' + OAUTH_NOT_CONFIGURED = lambda name='': f"Provider '{name}' is not configured" + + +class TASKS(str, Enum): + def __str__(self) -> str: + return super().__str__() + + DEFAULT = lambda task='': f'{task if task else "generation"}' + TITLE_GENERATION = 'title_generation' + FOLLOW_UP_GENERATION = 'follow_up_generation' + TAGS_GENERATION = 'tags_generation' + EMOJI_GENERATION = 'emoji_generation' + QUERY_GENERATION = 'query_generation' + IMAGE_PROMPT_GENERATION = 'image_prompt_generation' + AUTOCOMPLETE_GENERATION = 'autocomplete_generation' + FUNCTION_CALLING = 'function_calling' + MOA_RESPONSE_GENERATION = 'moa_response_generation' diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py new file mode 100644 index 0000000000000000000000000000000000000000..e734a2f865f55fbd6ac4490bc1343c68ff77f7a4 --- /dev/null +++ b/backend/open_webui/env.py @@ -0,0 +1,1058 @@ +import importlib.metadata +import json +import logging +import os +import pkgutil +import sys +import shutil +import traceback +from datetime import datetime, timezone +from typing import Any +from uuid import uuid4 +from pathlib import Path +from cryptography.hazmat.primitives import serialization +import re + + +import markdown +from bs4 import BeautifulSoup +from open_webui.constants import ERROR_MESSAGES + +#################################### +# Load .env file +#################################### + +# Use .resolve() to get the canonical path, removing any '..' or '.' components +ENV_FILE_PATH = Path(__file__).resolve() + +# OPEN_WEBUI_DIR should be the directory where env.py resides (open_webui/) +OPEN_WEBUI_DIR = ENV_FILE_PATH.parent + +# BACKEND_DIR is the parent of OPEN_WEBUI_DIR (backend/) +BACKEND_DIR = OPEN_WEBUI_DIR.parent + +# BASE_DIR is the parent of BACKEND_DIR (open-webui-dev/) +BASE_DIR = BACKEND_DIR.parent + +try: + from dotenv import find_dotenv, load_dotenv + + load_dotenv(find_dotenv(str(BASE_DIR / '.env'))) +except ImportError: + print('dotenv not installed, skipping...') + +DOCKER = os.environ.get('DOCKER', 'False').lower() == 'true' + +# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance +USE_CUDA = os.environ.get('USE_CUDA_DOCKER', 'false') + +if USE_CUDA.lower() == 'true': + try: + import torch + + assert torch.cuda.is_available(), 'CUDA not available' + DEVICE_TYPE = 'cuda' + except Exception as e: + cuda_error = f'Error when testing CUDA but USE_CUDA_DOCKER is true. Resetting USE_CUDA_DOCKER to false: {e}' + os.environ['USE_CUDA_DOCKER'] = 'false' + USE_CUDA = 'false' + DEVICE_TYPE = 'cpu' +else: + DEVICE_TYPE = 'cpu' + +if sys.platform == 'darwin': + try: + import torch + + if torch.backends.mps.is_available() and torch.backends.mps.is_built(): + DEVICE_TYPE = 'mps' + except Exception: + pass + +#################################### +# LOGGING +#################################### + +_LEVEL_MAP = { + 'DEBUG': 'debug', + 'INFO': 'info', + 'WARNING': 'warn', + 'ERROR': 'error', + 'CRITICAL': 'fatal', +} + + +class JSONFormatter(logging.Formatter): + """Format log records as single-line JSON objects for structured logging.""" + + def format(self, record: logging.LogRecord) -> str: + log_entry: dict[str, Any] = { + 'ts': datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(timespec='milliseconds'), + 'level': _LEVEL_MAP.get(record.levelname, record.levelname.lower()), + 'msg': record.getMessage(), + 'caller': record.name, + } + + if record.exc_info and record.exc_info[0] is not None: + log_entry['error'] = ''.join(traceback.format_exception(*record.exc_info)).rstrip() + elif record.exc_text: + log_entry['error'] = record.exc_text + + if record.stack_info: + log_entry['stacktrace'] = record.stack_info + + return json.dumps(log_entry, ensure_ascii=False, default=str) + + +LOG_FORMAT = os.environ.get('LOG_FORMAT', '').lower() + +GLOBAL_LOG_LEVEL = os.environ.get('GLOBAL_LOG_LEVEL', '').upper() +if GLOBAL_LOG_LEVEL in logging.getLevelNamesMapping(): + if LOG_FORMAT == 'json': + _handler = logging.StreamHandler(sys.stdout) + _handler.setFormatter(JSONFormatter()) + logging.basicConfig(handlers=[_handler], level=GLOBAL_LOG_LEVEL, force=True) + else: + logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True) +else: + GLOBAL_LOG_LEVEL = 'INFO' + +log = logging.getLogger(__name__) +log.info(f'GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}') + +if 'cuda_error' in locals(): + log.exception(cuda_error) + del cuda_error + +SRC_LOG_LEVELS = {} # Legacy variable, do not remove + +WEBUI_NAME = os.environ.get('WEBUI_NAME', 'Open WebUI') +if WEBUI_NAME != 'Open WebUI': + WEBUI_NAME += ' (Open WebUI)' + +WEBUI_FAVICON_URL = 'https://openwebui.com/favicon.png' + +TRUSTED_SIGNATURE_KEY = os.environ.get('TRUSTED_SIGNATURE_KEY', '') + +#################################### +# ENV (dev,test,prod) +#################################### + +ENV = os.environ.get('ENV', 'dev') + +FROM_INIT_PY = os.environ.get('FROM_INIT_PY', 'False').lower() == 'true' + +if FROM_INIT_PY: + PACKAGE_DATA = {'version': importlib.metadata.version('open-webui')} +else: + try: + PACKAGE_DATA = json.loads((BASE_DIR / 'package.json').read_text()) + except Exception: + PACKAGE_DATA = {'version': '0.0.0'} + +VERSION = PACKAGE_DATA['version'] + + +DEPLOYMENT_ID = os.environ.get('DEPLOYMENT_ID', '') +INSTANCE_ID = os.environ.get('INSTANCE_ID', str(uuid4())) + +ENABLE_DB_MIGRATIONS = os.environ.get('ENABLE_DB_MIGRATIONS', 'True').lower() == 'true' + + +# Function to parse each section +def parse_section(section): + items = [] + for li in section.find_all('li'): + # Extract raw HTML string + raw_html = str(li) + + # Extract text without HTML tags + text = li.get_text(separator=' ', strip=True) + + # Split into title and content + parts = text.split(': ', 1) + title = parts[0].strip() if len(parts) > 1 else '' + content = parts[1].strip() if len(parts) > 1 else text + + items.append({'title': title, 'content': content, 'raw': raw_html}) + return items + + +try: + changelog_path = BASE_DIR / 'CHANGELOG.md' + with open(str(changelog_path.absolute()), 'r', encoding='utf8') as file: + changelog_content = file.read() + +except Exception: + changelog_content = (pkgutil.get_data('open_webui', 'CHANGELOG.md') or b'').decode() + +# Convert markdown content to HTML +html_content = markdown.markdown(changelog_content) + +# Parse the HTML content +soup = BeautifulSoup(html_content, 'html.parser') + +# Initialize JSON structure +changelog_json = {} + +# Iterate over each version +for version in soup.find_all('h2'): + version_number = version.get_text().strip().split(' - ')[0][1:-1] # Remove brackets + date = version.get_text().strip().split(' - ')[1] + + version_data = {'date': date} + + # Find the next sibling that is a h3 tag (section title) + current = version.find_next_sibling() + + while current and current.name != 'h2': + if current.name == 'h3': + section_title = current.get_text().lower() # e.g., "added", "fixed" + section_items = parse_section(current.find_next_sibling('ul')) + version_data[section_title] = section_items + + # Move to the next element + current = current.find_next_sibling() + + changelog_json[version_number] = version_data + +CHANGELOG = changelog_json + +#################################### +# SAFE_MODE +#################################### + +SAFE_MODE = os.environ.get('SAFE_MODE', 'false').lower() == 'true' + + +#################################### +# ENABLE_FORWARD_USER_INFO_HEADERS +#################################### + +ENABLE_FORWARD_USER_INFO_HEADERS = os.environ.get('ENABLE_FORWARD_USER_INFO_HEADERS', 'False').lower() == 'true' + +# Header names for user info forwarding (customizable via environment variables) +FORWARD_USER_INFO_HEADER_USER_NAME = os.environ.get('FORWARD_USER_INFO_HEADER_USER_NAME', 'X-OpenWebUI-User-Name') +FORWARD_USER_INFO_HEADER_USER_ID = os.environ.get('FORWARD_USER_INFO_HEADER_USER_ID', 'X-OpenWebUI-User-Id') +FORWARD_USER_INFO_HEADER_USER_EMAIL = os.environ.get('FORWARD_USER_INFO_HEADER_USER_EMAIL', 'X-OpenWebUI-User-Email') +FORWARD_USER_INFO_HEADER_USER_ROLE = os.environ.get('FORWARD_USER_INFO_HEADER_USER_ROLE', 'X-OpenWebUI-User-Role') + +# Header name for chat ID forwarding (customizable via environment variable) +FORWARD_SESSION_INFO_HEADER_MESSAGE_ID = os.environ.get( + 'FORWARD_SESSION_INFO_HEADER_MESSAGE_ID', 'X-OpenWebUI-Message-Id' +) +FORWARD_SESSION_INFO_HEADER_CHAT_ID = os.environ.get('FORWARD_SESSION_INFO_HEADER_CHAT_ID', 'X-OpenWebUI-Chat-Id') + +# Experimental feature, may be removed in future +ENABLE_STAR_SESSIONS_MIDDLEWARE = os.environ.get('ENABLE_STAR_SESSIONS_MIDDLEWARE', 'False').lower() == 'true' + +ENABLE_EASTER_EGGS = os.environ.get('ENABLE_EASTER_EGGS', 'True').lower() == 'true' + +#################################### +# WEBUI_BUILD_HASH +#################################### + +WEBUI_BUILD_HASH = os.environ.get('WEBUI_BUILD_HASH', 'dev-build') + +#################################### +# DATA/FRONTEND BUILD DIR +#################################### + +DATA_DIR = Path(os.getenv('DATA_DIR', BACKEND_DIR / 'data')).resolve() + +if FROM_INIT_PY: + NEW_DATA_DIR = Path(os.getenv('DATA_DIR', OPEN_WEBUI_DIR / 'data')).resolve() + NEW_DATA_DIR.mkdir(parents=True, exist_ok=True) + + # Check if the data directory exists in the package directory + if DATA_DIR.exists() and DATA_DIR != NEW_DATA_DIR: + log.info(f'Moving {DATA_DIR} to {NEW_DATA_DIR}') + for item in DATA_DIR.iterdir(): + dest = NEW_DATA_DIR / item.name + if item.is_dir(): + shutil.copytree(item, dest, dirs_exist_ok=True) + else: + shutil.copy2(item, dest) + + # Zip the data directory + shutil.make_archive(DATA_DIR.parent / 'open_webui_data', 'zip', DATA_DIR) + + # Remove the old data directory + shutil.rmtree(DATA_DIR) + + DATA_DIR = Path(os.getenv('DATA_DIR', OPEN_WEBUI_DIR / 'data')) + +STATIC_DIR = Path(os.getenv('STATIC_DIR', OPEN_WEBUI_DIR / 'static')) + +FONTS_DIR = Path(os.getenv('FONTS_DIR', OPEN_WEBUI_DIR / 'static' / 'fonts')) + +FRONTEND_BUILD_DIR = Path(os.getenv('FRONTEND_BUILD_DIR', BASE_DIR / 'build')).resolve() + +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'): + # Rename the file + os.rename(f'{DATA_DIR}/ollama.db', f'{DATA_DIR}/webui.db') + log.info('Database migrated from Ollama-WebUI successfully.') +else: + pass + +DATABASE_URL = os.environ.get('DATABASE_URL', f'sqlite:///{DATA_DIR}/webui.db') + +DATABASE_TYPE = os.environ.get('DATABASE_TYPE') +DATABASE_USER = os.environ.get('DATABASE_USER') +DATABASE_PASSWORD = os.environ.get('DATABASE_PASSWORD') + +DATABASE_CRED = '' +if DATABASE_USER: + DATABASE_CRED += f'{DATABASE_USER}' +if DATABASE_PASSWORD: + DATABASE_CRED += f':{DATABASE_PASSWORD}' + +DB_VARS = { + 'db_type': DATABASE_TYPE, + 'db_cred': DATABASE_CRED, + 'db_host': os.environ.get('DATABASE_HOST'), + 'db_port': os.environ.get('DATABASE_PORT'), + 'db_name': os.environ.get('DATABASE_NAME'), +} + +if all(DB_VARS.values()): + DATABASE_URL = ( + f'{DB_VARS["db_type"]}://{DB_VARS["db_cred"]}@{DB_VARS["db_host"]}:{DB_VARS["db_port"]}/{DB_VARS["db_name"]}' + ) +elif DATABASE_TYPE == 'sqlite+sqlcipher' and not os.environ.get('DATABASE_URL'): + # Handle SQLCipher with local file when DATABASE_URL wasn't explicitly set + DATABASE_URL = f'sqlite+sqlcipher:///{DATA_DIR}/webui.db' + +# Replace the postgres:// with postgresql:// +if 'postgres://' in DATABASE_URL: + DATABASE_URL = DATABASE_URL.replace('postgres://', 'postgresql://') + +DATABASE_SCHEMA = os.environ.get('DATABASE_SCHEMA', None) + +DATABASE_POOL_SIZE = os.environ.get('DATABASE_POOL_SIZE', None) + +if DATABASE_POOL_SIZE != None: + try: + DATABASE_POOL_SIZE = int(DATABASE_POOL_SIZE) + except Exception: + DATABASE_POOL_SIZE = None + +DATABASE_POOL_MAX_OVERFLOW = os.environ.get('DATABASE_POOL_MAX_OVERFLOW', 0) + +if DATABASE_POOL_MAX_OVERFLOW == '': + DATABASE_POOL_MAX_OVERFLOW = 0 +else: + try: + DATABASE_POOL_MAX_OVERFLOW = int(DATABASE_POOL_MAX_OVERFLOW) + except Exception: + DATABASE_POOL_MAX_OVERFLOW = 0 + +DATABASE_POOL_TIMEOUT = os.environ.get('DATABASE_POOL_TIMEOUT', 30) + +if DATABASE_POOL_TIMEOUT == '': + DATABASE_POOL_TIMEOUT = 30 +else: + try: + DATABASE_POOL_TIMEOUT = int(DATABASE_POOL_TIMEOUT) + except Exception: + DATABASE_POOL_TIMEOUT = 30 + +DATABASE_POOL_RECYCLE = os.environ.get('DATABASE_POOL_RECYCLE', 3600) + +if DATABASE_POOL_RECYCLE == '': + DATABASE_POOL_RECYCLE = 3600 +else: + try: + DATABASE_POOL_RECYCLE = int(DATABASE_POOL_RECYCLE) + except Exception: + DATABASE_POOL_RECYCLE = 3600 + +DATABASE_ENABLE_SQLITE_WAL = os.environ.get('DATABASE_ENABLE_SQLITE_WAL', 'True').lower() == 'true' + +# SQLite PRAGMA tuning — these defaults are optimised for WAL-mode web-server +# workloads. Each can be overridden via its environment variable. +# Set any value to an empty string to skip that PRAGMA entirely. + +# PRAGMA synchronous: NORMAL (1) is safe with WAL and avoids an fsync per +# transaction. Valid values: OFF (0), NORMAL (1), FULL (2), EXTRA (3). +DATABASE_SQLITE_PRAGMA_SYNCHRONOUS = os.environ.get('DATABASE_SQLITE_PRAGMA_SYNCHRONOUS', 'NORMAL') + +# PRAGMA busy_timeout (ms): how long a connection waits for a write lock +# before raising SQLITE_BUSY. +DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT = os.environ.get('DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT', '5000') + +# PRAGMA cache_size: negative value = KiB. -65536 ≈ 64 MB page cache. +DATABASE_SQLITE_PRAGMA_CACHE_SIZE = os.environ.get('DATABASE_SQLITE_PRAGMA_CACHE_SIZE', '-65536') + +# PRAGMA temp_store: MEMORY (2) keeps temp tables and indices in RAM. +# Valid values: DEFAULT (0), FILE (1), MEMORY (2). +DATABASE_SQLITE_PRAGMA_TEMP_STORE = os.environ.get('DATABASE_SQLITE_PRAGMA_TEMP_STORE', 'MEMORY') + +# PRAGMA mmap_size (bytes): memory-mapped I/O size. 268435456 ≈ 256 MB. +# Set to 0 to disable mmap. +DATABASE_SQLITE_PRAGMA_MMAP_SIZE = os.environ.get('DATABASE_SQLITE_PRAGMA_MMAP_SIZE', '268435456') + +# PRAGMA journal_size_limit (bytes): caps the WAL file size after checkpoint. +# Without this the WAL grows unbounded during write bursts and is never +# truncated. 67108864 ≈ 64 MB. Set to -1 for no limit (SQLite default). +DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT = os.environ.get('DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT', '67108864') + +DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = os.environ.get('DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL', None) +if DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL is not None: + try: + DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = float(DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL) + except Exception: + DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = 0.0 + +# When enabled, get_db_context reuses existing sessions; set to False to always create new sessions +DATABASE_ENABLE_SESSION_SHARING = os.environ.get('DATABASE_ENABLE_SESSION_SHARING', 'False').lower() == 'true' + +# Enable public visibility of active user count (when disabled, only admins can see it) +ENABLE_PUBLIC_ACTIVE_USERS_COUNT = os.environ.get('ENABLE_PUBLIC_ACTIVE_USERS_COUNT', 'True').lower() == 'true' + +RESET_CONFIG_ON_START = os.environ.get('RESET_CONFIG_ON_START', 'False').lower() == 'true' + +ENABLE_REALTIME_CHAT_SAVE = os.environ.get('ENABLE_REALTIME_CHAT_SAVE', 'False').lower() == 'true' + +ENABLE_QUERIES_CACHE = os.environ.get('ENABLE_QUERIES_CACHE', 'False').lower() == 'true' + +RAG_SYSTEM_CONTEXT = os.environ.get('RAG_SYSTEM_CONTEXT', 'False').lower() == 'true' + +#################################### +# REDIS +#################################### + +REDIS_URL = os.environ.get('REDIS_URL', '') +REDIS_CLUSTER = os.environ.get('REDIS_CLUSTER', 'False').lower() == 'true' + +REDIS_KEY_PREFIX = os.environ.get('REDIS_KEY_PREFIX', 'open-webui') + +REDIS_SENTINEL_HOSTS = os.environ.get('REDIS_SENTINEL_HOSTS', '') +REDIS_SENTINEL_PORT = os.environ.get('REDIS_SENTINEL_PORT', '26379') + +# Maximum number of retries for Redis operations when using Sentinel fail-over +REDIS_SENTINEL_MAX_RETRY_COUNT = os.environ.get('REDIS_SENTINEL_MAX_RETRY_COUNT', '2') +try: + REDIS_SENTINEL_MAX_RETRY_COUNT = int(REDIS_SENTINEL_MAX_RETRY_COUNT) + if REDIS_SENTINEL_MAX_RETRY_COUNT < 1: + REDIS_SENTINEL_MAX_RETRY_COUNT = 2 +except ValueError: + REDIS_SENTINEL_MAX_RETRY_COUNT = 2 + + +REDIS_SOCKET_CONNECT_TIMEOUT = os.environ.get('REDIS_SOCKET_CONNECT_TIMEOUT', '') +try: + REDIS_SOCKET_CONNECT_TIMEOUT = float(REDIS_SOCKET_CONNECT_TIMEOUT) +except ValueError: + REDIS_SOCKET_CONNECT_TIMEOUT = None + +# Whether to enable TCP SO_KEEPALIVE on Redis client sockets. Opt-in: +# defaults to off so behavior is unchanged for existing deployments. When +# enabled, the kernel sends TCP keepalive probes on idle connections so +# half-closed sockets (e.g. after a silent firewall/LB reset or a NIC +# flap) are detected before the next command lands on them. +REDIS_SOCKET_KEEPALIVE = os.environ.get('REDIS_SOCKET_KEEPALIVE', 'False').lower() == 'true' + +# How often (in seconds) redis-py should PING an idle pooled connection +# before reusing it. Opt-in: defaults to unset (empty string) so behavior +# is unchanged for existing deployments. When set, should be shorter than +# the Redis server `timeout` setting and any firewall/LB idle timeout on +# the path to Redis, so stale connections are detected before a real +# command lands on them. Set to 0 or empty to disable. +REDIS_HEALTH_CHECK_INTERVAL = os.environ.get('REDIS_HEALTH_CHECK_INTERVAL', '') +try: + REDIS_HEALTH_CHECK_INTERVAL = int(REDIS_HEALTH_CHECK_INTERVAL) + if REDIS_HEALTH_CHECK_INTERVAL <= 0: + REDIS_HEALTH_CHECK_INTERVAL = None +except ValueError: + REDIS_HEALTH_CHECK_INTERVAL = None + +REDIS_RECONNECT_DELAY = os.environ.get('REDIS_RECONNECT_DELAY', '') + +if REDIS_RECONNECT_DELAY == '': + REDIS_RECONNECT_DELAY = None +else: + try: + REDIS_RECONNECT_DELAY = float(REDIS_RECONNECT_DELAY) + if REDIS_RECONNECT_DELAY < 0: + REDIS_RECONNECT_DELAY = None + except Exception: + REDIS_RECONNECT_DELAY = None + +#################################### +# UVICORN WORKERS +#################################### + +# Number of uvicorn worker processes for handling requests +UVICORN_WORKERS = os.environ.get('UVICORN_WORKERS', '1') +try: + UVICORN_WORKERS = int(UVICORN_WORKERS) + if UVICORN_WORKERS < 1: + UVICORN_WORKERS = 1 +except ValueError: + UVICORN_WORKERS = 1 + log.info(f'Invalid UVICORN_WORKERS value, defaulting to {UVICORN_WORKERS}') + +#################################### +# WEBUI_AUTH (Required for security) +#################################### + +WEBUI_AUTH = os.environ.get('WEBUI_AUTH', 'True').lower() == 'true' + +ENABLE_INITIAL_ADMIN_SIGNUP = os.environ.get('ENABLE_INITIAL_ADMIN_SIGNUP', 'False').lower() == 'true' +ENABLE_SIGNUP_PASSWORD_CONFIRMATION = os.environ.get('ENABLE_SIGNUP_PASSWORD_CONFIRMATION', 'False').lower() == 'true' + +#################################### +# Admin Account Runtime Creation +#################################### + +# Optional env vars for creating an admin account on startup +# Useful for headless/automated deployments +WEBUI_ADMIN_EMAIL = os.environ.get('WEBUI_ADMIN_EMAIL', '') +WEBUI_ADMIN_PASSWORD = os.environ.get('WEBUI_ADMIN_PASSWORD', '') +WEBUI_ADMIN_NAME = os.environ.get('WEBUI_ADMIN_NAME', 'Admin') + +WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get('WEBUI_AUTH_TRUSTED_EMAIL_HEADER', None) +WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get('WEBUI_AUTH_TRUSTED_NAME_HEADER', None) +WEBUI_AUTH_TRUSTED_GROUPS_HEADER = os.environ.get('WEBUI_AUTH_TRUSTED_GROUPS_HEADER', None) +WEBUI_AUTH_TRUSTED_ROLE_HEADER = os.environ.get('WEBUI_AUTH_TRUSTED_ROLE_HEADER', None) + +# Custom header name for API key authentication. Defaults to 'x-api-key'. +# Useful when Open WebUI sits behind a reverse proxy / API gateway that +# already uses the Authorization header for its own authentication — set +# this to a unique header (e.g. 'X-OpenWebUI-Key') so the middleware +# checks the custom header instead and avoids the 401 short-circuit. +CUSTOM_API_KEY_HEADER = os.environ.get('CUSTOM_API_KEY_HEADER', 'x-api-key') + +ENABLE_PASSWORD_VALIDATION = os.environ.get('ENABLE_PASSWORD_VALIDATION', 'False').lower() == 'true' +PASSWORD_VALIDATION_REGEX_PATTERN = os.environ.get( + 'PASSWORD_VALIDATION_REGEX_PATTERN', + r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$', +) + + +try: + PASSWORD_VALIDATION_REGEX_PATTERN = rf'{PASSWORD_VALIDATION_REGEX_PATTERN}' + PASSWORD_VALIDATION_REGEX_PATTERN = re.compile(PASSWORD_VALIDATION_REGEX_PATTERN) +except Exception as e: + log.error(f'Invalid PASSWORD_VALIDATION_REGEX_PATTERN: {e}') + PASSWORD_VALIDATION_REGEX_PATTERN = re.compile(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$') + +PASSWORD_VALIDATION_HINT = os.environ.get('PASSWORD_VALIDATION_HINT', '') + + +BYPASS_MODEL_ACCESS_CONTROL = os.environ.get('BYPASS_MODEL_ACCESS_CONTROL', 'False').lower() == 'true' + +# When enabled, skips pydub-based preprocessing (format conversion, compression, +# and chunked splitting) before sending files to processing engines. Useful when +# the upstream provider handles these steps or when ffmpeg is unavailable. +BYPASS_PYDUB_PREPROCESSING = os.environ.get('BYPASS_PYDUB_PREPROCESSING', 'False').lower() == 'true' + +# When disabled (default), the OpenAI catch-all proxy endpoint (/{path:path}) +# is blocked. Enable only if you need direct passthrough to upstream OpenAI- +# compatible APIs for endpoints not natively handled by Open WebUI. +ENABLE_OPENAI_API_PASSTHROUGH = os.environ.get('ENABLE_OPENAI_API_PASSTHROUGH', 'False').lower() == 'true' + +WEBUI_AUTH_SIGNOUT_REDIRECT_URL = os.environ.get('WEBUI_AUTH_SIGNOUT_REDIRECT_URL', None) + +#################################### +# WEBUI_SECRET_KEY +#################################### + +WEBUI_SECRET_KEY = os.environ.get( + 'WEBUI_SECRET_KEY', + os.environ.get('WEBUI_JWT_SECRET_KEY', 't0p-s3cr3t'), # DEPRECATED: remove at next major version +) + +WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get('WEBUI_SESSION_COOKIE_SAME_SITE', 'lax') + +WEBUI_SESSION_COOKIE_SECURE = os.environ.get('WEBUI_SESSION_COOKIE_SECURE', 'false').lower() == 'true' + +WEBUI_AUTH_COOKIE_SAME_SITE = os.environ.get('WEBUI_AUTH_COOKIE_SAME_SITE', WEBUI_SESSION_COOKIE_SAME_SITE) + +WEBUI_AUTH_COOKIE_SECURE = ( + os.environ.get( + 'WEBUI_AUTH_COOKIE_SECURE', + os.environ.get('WEBUI_SESSION_COOKIE_SECURE', 'false'), + ).lower() + == 'true' +) + +if WEBUI_AUTH and WEBUI_SECRET_KEY == '': + raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND) + +ENABLE_COMPRESSION_MIDDLEWARE = os.environ.get('ENABLE_COMPRESSION_MIDDLEWARE', 'True').lower() == 'true' + +#################################### +# OAUTH Configuration +#################################### +ENABLE_OAUTH_EMAIL_FALLBACK = os.environ.get('ENABLE_OAUTH_EMAIL_FALLBACK', 'False').lower() == 'true' + +ENABLE_OAUTH_ID_TOKEN_COOKIE = os.environ.get('ENABLE_OAUTH_ID_TOKEN_COOKIE', 'True').lower() == 'true' + +OAUTH_CLIENT_INFO_ENCRYPTION_KEY = os.environ.get('OAUTH_CLIENT_INFO_ENCRYPTION_KEY', WEBUI_SECRET_KEY) + +OAUTH_SESSION_TOKEN_ENCRYPTION_KEY = os.environ.get('OAUTH_SESSION_TOKEN_ENCRYPTION_KEY', WEBUI_SECRET_KEY) + +# Maximum number of concurrent OAuth sessions per user per provider +# This prevents unbounded session growth while allowing multi-device usage +OAUTH_MAX_SESSIONS_PER_USER = int(os.environ.get('OAUTH_MAX_SESSIONS_PER_USER', '10')) + +# Token Exchange Configuration +# Allows external apps to exchange OAuth tokens for OpenWebUI tokens +ENABLE_OAUTH_TOKEN_EXCHANGE = os.environ.get('ENABLE_OAUTH_TOKEN_EXCHANGE', 'False').lower() == 'true' + +# Back-Channel Logout Configuration +# When enabled, exposes POST /oauth/backchannel-logout for IdP-initiated logout +# per OpenID Connect Back-Channel Logout 1.0 spec. +# Requires Redis for JWT revocation. +ENABLE_OAUTH_BACKCHANNEL_LOGOUT = os.environ.get('ENABLE_OAUTH_BACKCHANNEL_LOGOUT', 'False').lower() == 'true' + +#################################### +# SCIM Configuration +#################################### + +ENABLE_SCIM = os.environ.get('ENABLE_SCIM', os.environ.get('SCIM_ENABLED', 'False')).lower() == 'true' +SCIM_TOKEN = os.environ.get('SCIM_TOKEN', '') +SCIM_AUTH_PROVIDER = os.environ.get('SCIM_AUTH_PROVIDER', '') + +if ENABLE_SCIM and not SCIM_AUTH_PROVIDER: + log.warning( + 'SCIM is enabled but SCIM_AUTH_PROVIDER is not set. ' + "Set SCIM_AUTH_PROVIDER to the OAuth provider name (e.g. 'microsoft', 'oidc') " + 'to enable externalId storage.' + ) + +#################################### +# LICENSE_KEY +#################################### + +LICENSE_KEY = os.environ.get('LICENSE_KEY', '') + +LICENSE_BLOB = None +LICENSE_BLOB_PATH = os.environ.get('LICENSE_BLOB_PATH', DATA_DIR / 'l.data') +if LICENSE_BLOB_PATH and os.path.exists(LICENSE_BLOB_PATH): + with open(LICENSE_BLOB_PATH, 'rb') as f: + LICENSE_BLOB = f.read() + +LICENSE_PUBLIC_KEY = os.environ.get('LICENSE_PUBLIC_KEY', '') + +pk = None +if LICENSE_PUBLIC_KEY: + pk = serialization.load_pem_public_key( + f""" +-----BEGIN PUBLIC KEY----- +{LICENSE_PUBLIC_KEY} +-----END PUBLIC KEY----- +""".encode('utf-8') + ) + + +#################################### +# MODELS +#################################### + +ENABLE_CUSTOM_MODEL_FALLBACK = os.environ.get('ENABLE_CUSTOM_MODEL_FALLBACK', 'False').lower() == 'true' + +MODELS_CACHE_TTL = os.environ.get('MODELS_CACHE_TTL', '1') +if MODELS_CACHE_TTL == '': + MODELS_CACHE_TTL = None +else: + try: + MODELS_CACHE_TTL = int(MODELS_CACHE_TTL) + except Exception: + MODELS_CACHE_TTL = 1 + + +#################################### +# CHAT +#################################### + +ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION = ( + os.environ.get('ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION', 'False').lower() == 'true' +) + +# When enabled, uses a hardcoded extension-to-MIME dictionary as a last-resort +# fallback when both mimetypes.guess_type() and file.meta.content_type fail to +# determine the content type. This can help on minimal container images (e.g. +# wolfi-base) that lack /etc/mime.types AND have legacy files without stored +# content_type metadata. +ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK = ( + os.environ.get('ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK', 'False').lower() == 'true' +) + +CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = os.environ.get('CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE', '1') + +if CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE == '': + CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = 1 +else: + try: + CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = int(CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE) + except Exception: + CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = 1 + + +CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = os.environ.get('CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES', '30') + +if CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES == '': + CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = 30 +else: + try: + CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = int(CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES) + except Exception: + CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = 30 + + +# WARNING: Experimental. Only enable if your upstream Responses API endpoint +# supports stateful sessions (i.e. server-side response storage with +# previous_response_id anchoring). Most proxies and third-party endpoints +# are stateless and will break if this is enabled. +ENABLE_RESPONSES_API_STATEFUL = os.environ.get('ENABLE_RESPONSES_API_STATEFUL', 'False').lower() == 'true' + + +CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = os.environ.get('CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE', '') + +if CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE == '': + CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = None +else: + try: + CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = int(CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE) + except Exception: + CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = None + + +#################################### +# WEBSOCKET SUPPORT +#################################### + +ENABLE_WEBSOCKET_SUPPORT = os.environ.get('ENABLE_WEBSOCKET_SUPPORT', 'True').lower() == 'true' + + +WEBSOCKET_MANAGER = os.environ.get('WEBSOCKET_MANAGER', '') + +WEBSOCKET_REDIS_OPTIONS = os.environ.get('WEBSOCKET_REDIS_OPTIONS', '') + + +if WEBSOCKET_REDIS_OPTIONS == '': + if REDIS_SOCKET_CONNECT_TIMEOUT: + WEBSOCKET_REDIS_OPTIONS = {'socket_connect_timeout': REDIS_SOCKET_CONNECT_TIMEOUT} + else: + log.debug('No WEBSOCKET_REDIS_OPTIONS provided, defaulting to None') + WEBSOCKET_REDIS_OPTIONS = None +else: + try: + WEBSOCKET_REDIS_OPTIONS = json.loads(WEBSOCKET_REDIS_OPTIONS) + except Exception: + log.warning('Invalid WEBSOCKET_REDIS_OPTIONS, defaulting to None') + WEBSOCKET_REDIS_OPTIONS = None + +WEBSOCKET_REDIS_URL = os.environ.get('WEBSOCKET_REDIS_URL', REDIS_URL) +WEBSOCKET_REDIS_CLUSTER = os.environ.get('WEBSOCKET_REDIS_CLUSTER', str(REDIS_CLUSTER)).lower() == 'true' + +websocket_redis_lock_timeout = os.environ.get('WEBSOCKET_REDIS_LOCK_TIMEOUT', '60') + +try: + WEBSOCKET_REDIS_LOCK_TIMEOUT = int(websocket_redis_lock_timeout) +except ValueError: + WEBSOCKET_REDIS_LOCK_TIMEOUT = 60 + +WEBSOCKET_SENTINEL_HOSTS = os.environ.get('WEBSOCKET_SENTINEL_HOSTS', '') +WEBSOCKET_SENTINEL_PORT = os.environ.get('WEBSOCKET_SENTINEL_PORT', '26379') +WEBSOCKET_SERVER_LOGGING = os.environ.get('WEBSOCKET_SERVER_LOGGING', 'False').lower() == 'true' +WEBSOCKET_SERVER_ENGINEIO_LOGGING = ( + os.environ.get( + 'WEBSOCKET_SERVER_ENGINEIO_LOGGING', + os.environ.get('WEBSOCKET_SERVER_LOGGING', 'False'), + ).lower() + == 'true' +) +WEBSOCKET_SERVER_PING_TIMEOUT = os.environ.get('WEBSOCKET_SERVER_PING_TIMEOUT', '20') +try: + WEBSOCKET_SERVER_PING_TIMEOUT = int(WEBSOCKET_SERVER_PING_TIMEOUT) +except ValueError: + WEBSOCKET_SERVER_PING_TIMEOUT = 20 + +WEBSOCKET_SERVER_PING_INTERVAL = os.environ.get('WEBSOCKET_SERVER_PING_INTERVAL', '25') +try: + WEBSOCKET_SERVER_PING_INTERVAL = int(WEBSOCKET_SERVER_PING_INTERVAL) +except ValueError: + WEBSOCKET_SERVER_PING_INTERVAL = 25 + +WEBSOCKET_EVENT_CALLER_TIMEOUT = os.environ.get('WEBSOCKET_EVENT_CALLER_TIMEOUT', '') + +if WEBSOCKET_EVENT_CALLER_TIMEOUT == '': + WEBSOCKET_EVENT_CALLER_TIMEOUT = None +else: + try: + WEBSOCKET_EVENT_CALLER_TIMEOUT = int(WEBSOCKET_EVENT_CALLER_TIMEOUT) + except ValueError: + WEBSOCKET_EVENT_CALLER_TIMEOUT = 300 + + +REQUESTS_VERIFY = os.environ.get('REQUESTS_VERIFY', 'True').lower() == 'true' + +AIOHTTP_CLIENT_TIMEOUT = os.environ.get('AIOHTTP_CLIENT_TIMEOUT', '') + +if AIOHTTP_CLIENT_TIMEOUT == '': + AIOHTTP_CLIENT_TIMEOUT = None +else: + try: + AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT) + except Exception: + AIOHTTP_CLIENT_TIMEOUT = 300 + + +AIOHTTP_CLIENT_SESSION_SSL = os.environ.get('AIOHTTP_CLIENT_SESSION_SSL', 'True').lower() == 'true' + +AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = os.environ.get( + 'AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST', + os.environ.get('AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST', '10'), +) + +if AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST == '': + AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = None +else: + try: + AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = int(AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) + except Exception: + AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = 10 + + +AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = os.environ.get('AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA', '10') + +if AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA == '': + AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = None +else: + try: + AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = int(AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA) + except Exception: + AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = 10 + + +AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL = ( + os.environ.get('AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL', 'True').lower() == 'true' +) + +AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = os.environ.get('AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER', '') + +if AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER == '': + AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = AIOHTTP_CLIENT_TIMEOUT +else: + try: + AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = int(AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER) + except Exception: + AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = AIOHTTP_CLIENT_TIMEOUT + + +#################################### +# AIOHTTP Connection Pool +#################################### + +AIOHTTP_POOL_CONNECTIONS = os.environ.get('AIOHTTP_POOL_CONNECTIONS', '') +if AIOHTTP_POOL_CONNECTIONS == '': + AIOHTTP_POOL_CONNECTIONS = None +else: + try: + AIOHTTP_POOL_CONNECTIONS = int(AIOHTTP_POOL_CONNECTIONS) + except ValueError: + AIOHTTP_POOL_CONNECTIONS = None + +AIOHTTP_POOL_CONNECTIONS_PER_HOST = os.environ.get('AIOHTTP_POOL_CONNECTIONS_PER_HOST', '') +if AIOHTTP_POOL_CONNECTIONS_PER_HOST == '': + AIOHTTP_POOL_CONNECTIONS_PER_HOST = None +else: + try: + AIOHTTP_POOL_CONNECTIONS_PER_HOST = int(AIOHTTP_POOL_CONNECTIONS_PER_HOST) + except ValueError: + AIOHTTP_POOL_CONNECTIONS_PER_HOST = None + +AIOHTTP_POOL_DNS_TTL = os.environ.get('AIOHTTP_POOL_DNS_TTL', '300') +try: + AIOHTTP_POOL_DNS_TTL = int(AIOHTTP_POOL_DNS_TTL) + if AIOHTTP_POOL_DNS_TTL < 0: + AIOHTTP_POOL_DNS_TTL = 300 +except ValueError: + AIOHTTP_POOL_DNS_TTL = 300 + +RAG_EMBEDDING_TIMEOUT = os.environ.get('RAG_EMBEDDING_TIMEOUT', '') + +if RAG_EMBEDDING_TIMEOUT == '': + RAG_EMBEDDING_TIMEOUT = None +else: + try: + RAG_EMBEDDING_TIMEOUT = int(RAG_EMBEDDING_TIMEOUT) + except Exception: + RAG_EMBEDDING_TIMEOUT = None + + +#################################### +# SENTENCE TRANSFORMERS +#################################### + + +SENTENCE_TRANSFORMERS_BACKEND = os.environ.get('SENTENCE_TRANSFORMERS_BACKEND', '') +if SENTENCE_TRANSFORMERS_BACKEND == '': + SENTENCE_TRANSFORMERS_BACKEND = 'torch' + + +SENTENCE_TRANSFORMERS_MODEL_KWARGS = os.environ.get('SENTENCE_TRANSFORMERS_MODEL_KWARGS', '') +if SENTENCE_TRANSFORMERS_MODEL_KWARGS == '': + SENTENCE_TRANSFORMERS_MODEL_KWARGS = None +else: + try: + SENTENCE_TRANSFORMERS_MODEL_KWARGS = json.loads(SENTENCE_TRANSFORMERS_MODEL_KWARGS) + except Exception: + SENTENCE_TRANSFORMERS_MODEL_KWARGS = None + + +SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = os.environ.get('SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND', '') +if SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND == '': + SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = 'torch' + + +SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = os.environ.get( + 'SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS', '' +) +if SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS == '': + SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None +else: + try: + SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = json.loads(SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS) + except Exception: + SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None + +# Whether to apply sigmoid normalization to CrossEncoder reranking scores. +# When enabled (default), scores are normalized to 0-1 range for proper +# relevance threshold behavior with MS MARCO models. +SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION = ( + os.environ.get('SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION', 'True').lower() == 'true' +) + +#################################### +# OFFLINE_MODE +#################################### + +ENABLE_VERSION_UPDATE_CHECK = os.environ.get('ENABLE_VERSION_UPDATE_CHECK', 'true').lower() == 'true' +OFFLINE_MODE = os.environ.get('OFFLINE_MODE', 'false').lower() == 'true' + +if OFFLINE_MODE: + os.environ['HF_HUB_OFFLINE'] = '1' + ENABLE_VERSION_UPDATE_CHECK = False + +#################################### +# AUDIT LOGGING +#################################### + + +ENABLE_AUDIT_STDOUT = os.getenv('ENABLE_AUDIT_STDOUT', 'False').lower() == 'true' +ENABLE_AUDIT_LOGS_FILE = os.getenv('ENABLE_AUDIT_LOGS_FILE', 'True').lower() == 'true' + +# Where to store log file +# Defaults to the DATA_DIR/audit.log. To set AUDIT_LOGS_FILE_PATH you need to +# provide the whole path, like: /app/audit.log +AUDIT_LOGS_FILE_PATH = os.getenv('AUDIT_LOGS_FILE_PATH', f'{DATA_DIR}/audit.log') +# Maximum size of a file before rotating into a new log file +AUDIT_LOG_FILE_ROTATION_SIZE = os.getenv('AUDIT_LOG_FILE_ROTATION_SIZE', '10MB') + +# Comma separated list of logger names to use for audit logging +# Default is "uvicorn.access" which is the access log for Uvicorn +# You can add more logger names to this list if you want to capture more logs +AUDIT_UVICORN_LOGGER_NAMES = os.getenv('AUDIT_UVICORN_LOGGER_NAMES', 'uvicorn.access').split(',') + +# METADATA | REQUEST | REQUEST_RESPONSE +AUDIT_LOG_LEVEL = os.getenv('AUDIT_LOG_LEVEL', 'NONE').upper() +try: + MAX_BODY_LOG_SIZE = int(os.environ.get('MAX_BODY_LOG_SIZE') or 2048) +except ValueError: + MAX_BODY_LOG_SIZE = 2048 + +# Comma separated list for urls to exclude from audit +AUDIT_EXCLUDED_PATHS = os.getenv('AUDIT_EXCLUDED_PATHS', '/chats,/chat,/folders').split(',') +AUDIT_EXCLUDED_PATHS = [path.strip() for path in AUDIT_EXCLUDED_PATHS] +AUDIT_EXCLUDED_PATHS = [path.lstrip('/') for path in AUDIT_EXCLUDED_PATHS] + +# Comma separated list of urls to include in audit (whitelist mode) +# When set, only these paths are audited and AUDIT_EXCLUDED_PATHS is ignored +AUDIT_INCLUDED_PATHS = os.getenv('AUDIT_INCLUDED_PATHS', '').split(',') +AUDIT_INCLUDED_PATHS = [path.strip() for path in AUDIT_INCLUDED_PATHS] +AUDIT_INCLUDED_PATHS = [path.lstrip('/') for path in AUDIT_INCLUDED_PATHS if path] + +# When enabled, GET requests are also audited (disabled by default to avoid log noise) +ENABLE_AUDIT_GET_REQUESTS = os.getenv('ENABLE_AUDIT_GET_REQUESTS', 'False').lower() == 'true' + + +#################################### +# OPENTELEMETRY +#################################### + +ENABLE_OTEL = os.environ.get('ENABLE_OTEL', 'False').lower() == 'true' +ENABLE_OTEL_TRACES = os.environ.get('ENABLE_OTEL_TRACES', 'False').lower() == 'true' +ENABLE_OTEL_METRICS = os.environ.get('ENABLE_OTEL_METRICS', 'False').lower() == 'true' +ENABLE_OTEL_LOGS = os.environ.get('ENABLE_OTEL_LOGS', 'False').lower() == 'true' + +OTEL_EXPORTER_OTLP_ENDPOINT = os.environ.get('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://localhost:4317') +OTEL_METRICS_EXPORTER_OTLP_ENDPOINT = os.environ.get('OTEL_METRICS_EXPORTER_OTLP_ENDPOINT', OTEL_EXPORTER_OTLP_ENDPOINT) +OTEL_LOGS_EXPORTER_OTLP_ENDPOINT = os.environ.get('OTEL_LOGS_EXPORTER_OTLP_ENDPOINT', OTEL_EXPORTER_OTLP_ENDPOINT) +OTEL_EXPORTER_OTLP_INSECURE = os.environ.get('OTEL_EXPORTER_OTLP_INSECURE', 'False').lower() == 'true' +OTEL_METRICS_EXPORTER_OTLP_INSECURE = ( + os.environ.get('OTEL_METRICS_EXPORTER_OTLP_INSECURE', str(OTEL_EXPORTER_OTLP_INSECURE)).lower() == 'true' +) +OTEL_LOGS_EXPORTER_OTLP_INSECURE = ( + os.environ.get('OTEL_LOGS_EXPORTER_OTLP_INSECURE', str(OTEL_EXPORTER_OTLP_INSECURE)).lower() == 'true' +) +OTEL_SERVICE_NAME = os.environ.get('OTEL_SERVICE_NAME', 'open-webui') +OTEL_RESOURCE_ATTRIBUTES = os.environ.get('OTEL_RESOURCE_ATTRIBUTES', '') # e.g. key1=val1,key2=val2 +OTEL_TRACES_SAMPLER = os.environ.get('OTEL_TRACES_SAMPLER', 'parentbased_always_on').lower() +OTEL_BASIC_AUTH_USERNAME = os.environ.get('OTEL_BASIC_AUTH_USERNAME', '') +OTEL_BASIC_AUTH_PASSWORD = os.environ.get('OTEL_BASIC_AUTH_PASSWORD', '') +OTEL_METRICS_EXPORT_INTERVAL_MILLIS = int(os.environ.get('OTEL_METRICS_EXPORT_INTERVAL_MILLIS', '10000')) + +OTEL_METRICS_BASIC_AUTH_USERNAME = os.environ.get('OTEL_METRICS_BASIC_AUTH_USERNAME', OTEL_BASIC_AUTH_USERNAME) +OTEL_METRICS_BASIC_AUTH_PASSWORD = os.environ.get('OTEL_METRICS_BASIC_AUTH_PASSWORD', OTEL_BASIC_AUTH_PASSWORD) +OTEL_LOGS_BASIC_AUTH_USERNAME = os.environ.get('OTEL_LOGS_BASIC_AUTH_USERNAME', OTEL_BASIC_AUTH_USERNAME) +OTEL_LOGS_BASIC_AUTH_PASSWORD = os.environ.get('OTEL_LOGS_BASIC_AUTH_PASSWORD', OTEL_BASIC_AUTH_PASSWORD) + +OTEL_OTLP_SPAN_EXPORTER = os.environ.get('OTEL_OTLP_SPAN_EXPORTER', 'grpc').lower() # grpc or http + +OTEL_METRICS_OTLP_SPAN_EXPORTER = os.environ.get( + 'OTEL_METRICS_OTLP_SPAN_EXPORTER', OTEL_OTLP_SPAN_EXPORTER +).lower() # grpc or http + +OTEL_LOGS_OTLP_SPAN_EXPORTER = os.environ.get( + 'OTEL_LOGS_OTLP_SPAN_EXPORTER', OTEL_OTLP_SPAN_EXPORTER +).lower() # grpc or http + +#################################### +# TOOLS/FUNCTIONS PIP OPTIONS +#################################### + +ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS = ( + os.environ.get('ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS', 'True').lower() == 'true' +) + +PIP_OPTIONS = os.getenv('PIP_OPTIONS', '').split() +PIP_PACKAGE_INDEX_OPTIONS = os.getenv('PIP_PACKAGE_INDEX_OPTIONS', '').split() + + +#################################### +# PROGRESSIVE WEB APP OPTIONS +#################################### + +EXTERNAL_PWA_MANIFEST_URL = os.environ.get('EXTERNAL_PWA_MANIFEST_URL') + +#################################### +# 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.environ.get('DEFAULT_GROUP_SHARE_PERMISSION', 'members').strip().lower() +DEFAULT_GROUP_SHARE_PERMISSION = 'members' if _default_group_share == 'members' else _default_group_share == 'true' diff --git a/backend/open_webui/functions.py b/backend/open_webui/functions.py new file mode 100644 index 0000000000000000000000000000000000000000..1e032759eadb50db1ae03ed1eef9e00136e858e9 --- /dev/null +++ b/backend/open_webui/functions.py @@ -0,0 +1,348 @@ +import logging +import sys +import inspect +import json +import asyncio + +from pydantic import BaseModel +from typing import AsyncGenerator, Generator, Iterator +from fastapi import ( + Depends, + FastAPI, + File, + Form, + HTTPException, + Request, + UploadFile, + status, +) +from starlette.responses import Response, StreamingResponse + + +from open_webui.constants import ERROR_MESSAGES +from open_webui.socket.main import ( + get_event_call, + get_event_emitter, +) + + +from open_webui.models.users import UserModel +from open_webui.models.functions import Functions +from open_webui.models.models import Models + +from open_webui.utils.plugin import ( + load_function_module_by_id, + get_function_module_from_cache, +) +from open_webui.utils.access_control import check_model_access + +from open_webui.env import GLOBAL_LOG_LEVEL, BYPASS_MODEL_ACCESS_CONTROL +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL + +from open_webui.utils.misc import ( + add_or_update_system_message, + get_last_user_message, + prepend_to_first_user_message_content, + openai_chat_chunk_message_template, + openai_chat_completion_message_template, +) +from open_webui.utils.payload import ( + apply_model_params_to_body_openai, + apply_system_prompt_to_body, +) + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) +log = logging.getLogger(__name__) + + +async def get_function_module_by_id(request: Request, pipe_id: str): + function_module, _, _ = await get_function_module_from_cache(request, pipe_id) + + if hasattr(function_module, 'valves') and hasattr(function_module, 'Valves'): + Valves = function_module.Valves + valves = await Functions.get_function_valves_by_id(pipe_id) + + if valves: + try: + function_module.valves = Valves(**{k: v for k, v in valves.items() if v is not None}) + except Exception as e: + log.exception(f'Error loading valves for function {pipe_id}: {e}') + raise e + else: + function_module.valves = Valves() + + return function_module + + +async def get_function_models(request): + pipes = await Functions.get_functions_by_type('pipe', active_only=True) + pipe_models = [] + + for pipe in pipes: + try: + function_module = await get_function_module_by_id(request, pipe.id) + + has_user_valves = False + if hasattr(function_module, 'UserValves'): + has_user_valves = True + + # Check if function is a manifold + if hasattr(function_module, 'pipes'): + sub_pipes = [] + + # Handle pipes being a list, sync function, or async function + try: + if callable(function_module.pipes): + if asyncio.iscoroutinefunction(function_module.pipes): + sub_pipes = await function_module.pipes() + else: + sub_pipes = function_module.pipes() + else: + sub_pipes = function_module.pipes + except Exception as e: + log.exception(e) + sub_pipes = [] + + log.debug(f"get_function_models: function '{pipe.id}' is a manifold of {sub_pipes}") + + for p in sub_pipes: + sub_pipe_id = f'{pipe.id}.{p["id"]}' + sub_pipe_name = p['name'] + + if hasattr(function_module, 'name'): + sub_pipe_name = f'{function_module.name}{sub_pipe_name}' + + pipe_flag = {'type': pipe.type} + + pipe_models.append( + { + 'id': sub_pipe_id, + 'name': sub_pipe_name, + 'object': 'model', + 'created': pipe.created_at, + 'owned_by': 'openai', + 'pipe': pipe_flag, + 'has_user_valves': has_user_valves, + } + ) + else: + pipe_flag = {'type': 'pipe'} + + log.debug( + f"get_function_models: function '{pipe.id}' is a single pipe {{ 'id': {pipe.id}, 'name': {pipe.name} }}" + ) + + pipe_models.append( + { + 'id': pipe.id, + 'name': pipe.name, + 'object': 'model', + 'created': pipe.created_at, + 'owned_by': 'openai', + 'pipe': pipe_flag, + 'has_user_valves': has_user_valves, + } + ) + except Exception as e: + log.exception(e) + continue + + return pipe_models + + +async def generate_function_chat_completion(request, form_data, user, models: dict = {}): + async def execute_pipe(pipe, params): + if inspect.iscoroutinefunction(pipe): + return await pipe(**params) + else: + return pipe(**params) + + async def get_message_content(res: str | Generator | AsyncGenerator) -> str: + if isinstance(res, str): + return res + if isinstance(res, Generator): + return ''.join(map(str, res)) + if isinstance(res, AsyncGenerator): + return ''.join([str(stream) async for stream in res]) + + def process_line(form_data: dict, line): + if isinstance(line, BaseModel): + line = line.model_dump_json() + line = f'data: {line}' + if isinstance(line, dict): + line = f'data: {json.dumps(line)}' + + try: + line = line.decode('utf-8') + except Exception: + pass + + if line.startswith('data:'): + return f'{line}\n\n' + else: + line = openai_chat_chunk_message_template(form_data['model'], line) + return f'data: {json.dumps(line)}\n\n' + + def get_pipe_id(form_data: dict) -> str: + pipe_id = form_data['model'] + if '.' in pipe_id: + pipe_id, _ = pipe_id.split('.', 1) + return pipe_id + + async def get_function_params(function_module, form_data, user, extra_params=None): + if extra_params is None: + extra_params = {} + + pipe_id = get_pipe_id(form_data) + + # Get the signature of the function + sig = inspect.signature(function_module.pipe) + params = {'body': form_data} | {k: v for k, v in extra_params.items() if k in sig.parameters} + + if '__user__' in params and hasattr(function_module, 'UserValves'): + user_valves = await Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id) + try: + params['__user__']['valves'] = function_module.UserValves(**user_valves) + except Exception as e: + log.exception(e) + params['__user__']['valves'] = function_module.UserValves() + + return params + + model_id = form_data.get('model') + model_info = await Models.get_model_by_id(model_id) + + metadata = form_data.pop('metadata', {}) + + files = metadata.get('files', []) + tool_ids = metadata.get('tool_ids', []) + # Check if tool_ids is None + if tool_ids is None: + tool_ids = [] + + __event_emitter__ = None + __event_call__ = None + __task__ = None + __task_body__ = None + + if metadata: + if all(k in metadata for k in ('session_id', 'chat_id', 'message_id')): + __event_emitter__ = await get_event_emitter(metadata) + __event_call__ = await get_event_call(metadata) + __task__ = metadata.get('task', None) + __task_body__ = metadata.get('task_body', None) + + oauth_token = None + try: + oauth_session_id = request.cookies.get('oauth_session_id', None) + if oauth_session_id: + oauth_token = await request.app.state.oauth_manager.get_oauth_token( + user.id, + oauth_session_id, + ) + + # Fallback: no cookie (automation, API key, etc.) — use most recent session + if oauth_token is None: + from open_webui.models.oauth_sessions import OAuthSessions + + sessions = await OAuthSessions.get_sessions_by_user_id(user.id) + if sessions: + best = max(sessions, key=lambda s: s.updated_at) + oauth_token = await request.app.state.oauth_manager.get_oauth_token( + user.id, + best.id, + ) + except Exception as e: + log.error(f'Error getting OAuth token: {e}') + + extra_params = { + '__event_emitter__': __event_emitter__, + '__event_call__': __event_call__, + '__chat_id__': metadata.get('chat_id', None), + '__session_id__': metadata.get('session_id', None), + '__message_id__': metadata.get('message_id', None), + '__task__': __task__, + '__task_body__': __task_body__, + '__files__': files, + '__user__': user.model_dump() if isinstance(user, UserModel) else {}, + '__metadata__': metadata, + '__oauth_token__': oauth_token, + '__request__': request, + } + extra_params['__tools__'] = metadata.get('tools', {}) + + if model_info: + if model_info.base_model_id: + form_data['model'] = model_info.base_model_id + + if not BYPASS_MODEL_ACCESS_CONTROL: + bypass = isinstance(user, UserModel) and user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL + await check_model_access(user if isinstance(user, UserModel) else UserModel(**user), model_info, bypass) + + params = model_info.params.model_dump() + + if params: + system = params.pop('system', None) + form_data = apply_model_params_to_body_openai(params, form_data) + form_data = apply_system_prompt_to_body(system, form_data, metadata, user) + + pipe_id = get_pipe_id(form_data) + function_module = await get_function_module_by_id(request, pipe_id) + + pipe = function_module.pipe + params = await get_function_params(function_module, form_data, user, extra_params) + + if form_data.get('stream', False): + + async def stream_content(): + try: + res = await execute_pipe(pipe, params) + + # Directly return if the response is a StreamingResponse + if isinstance(res, StreamingResponse): + async for data in res.body_iterator: + yield data + return + if isinstance(res, dict): + yield f'data: {json.dumps(res)}\n\n' + return + + except Exception as e: + log.error(f'Error: {e}') + yield f'data: {json.dumps({"error": {"detail": str(e)}})}\n\n' + return + + if isinstance(res, str): + message = openai_chat_chunk_message_template(form_data['model'], res) + yield f'data: {json.dumps(message)}\n\n' + + if isinstance(res, Iterator): + for line in res: + yield process_line(form_data, line) + + if isinstance(res, AsyncGenerator): + async for line in res: + yield process_line(form_data, line) + + if isinstance(res, str) or isinstance(res, Generator): + finish_message = openai_chat_chunk_message_template(form_data['model'], '') + finish_message['choices'][0]['finish_reason'] = 'stop' + yield f'data: {json.dumps(finish_message)}\n\n' + yield 'data: [DONE]' + + return StreamingResponse(stream_content(), media_type='text/event-stream') + else: + try: + res = await execute_pipe(pipe, params) + + except Exception as e: + log.error(f'Error: {e}') + return {'error': {'detail': str(e)}} + + if isinstance(res, StreamingResponse) or isinstance(res, dict): + return res + if isinstance(res, BaseModel): + return res.model_dump() + + message = await get_message_content(res) + return openai_chat_completion_message_template(form_data['model'], message) diff --git a/backend/open_webui/internal/db.py b/backend/open_webui/internal/db.py new file mode 100644 index 0000000000000000000000000000000000000000..c9e4f318e16f35ddc74504f91a81ef2a9162aea7 --- /dev/null +++ b/backend/open_webui/internal/db.py @@ -0,0 +1,409 @@ +import os +import json +import logging +from contextlib import asynccontextmanager, contextmanager +from typing import Any, Optional +from urllib.parse import parse_qs, urlencode, urlparse, urlunparse + +from open_webui.internal.wrappers import register_connection +from open_webui.env import ( + OPEN_WEBUI_DIR, + DATABASE_URL, + DATABASE_SCHEMA, + DATABASE_POOL_MAX_OVERFLOW, + DATABASE_POOL_RECYCLE, + DATABASE_POOL_SIZE, + DATABASE_POOL_TIMEOUT, + DATABASE_ENABLE_SQLITE_WAL, + DATABASE_ENABLE_SESSION_SHARING, + DATABASE_SQLITE_PRAGMA_SYNCHRONOUS, + DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT, + DATABASE_SQLITE_PRAGMA_CACHE_SIZE, + DATABASE_SQLITE_PRAGMA_TEMP_STORE, + DATABASE_SQLITE_PRAGMA_MMAP_SIZE, + DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT, + ENABLE_DB_MIGRATIONS, +) +from peewee_migrate import Router +from sqlalchemy import Dialect, create_engine, MetaData, event, types +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import scoped_session, sessionmaker, Session +from sqlalchemy.pool import QueuePool, NullPool +from sqlalchemy.sql.type_api import _T +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. +# ───────────────────────────────────────────────────────────────────── + + +def _pop_first(params: dict[str, list[str]], key: str) -> str | None: + """Pop a single-valued query param, returning ``None`` if absent.""" + values = params.pop(key, None) + return values[0] if values else None + + +def _is_postgres_url(url: str) -> bool: + """Return True if *url* looks like a PostgreSQL connection string.""" + return bool(url) and any(url.startswith(p) for p in ('postgresql://', 'postgresql+', 'postgres://')) + + +def extract_ssl_params_from_url(url: str) -> tuple[str, dict[str, str]]: + """Strip SSL query-string parameters from a PostgreSQL URL. + + Returns ``(url_without_ssl, ssl_dict)`` where *ssl_dict* maps + canonical libpq key names (``sslmode``, ``sslrootcert``, …) to + their values. Non-PostgreSQL URLs are returned unchanged with an + empty dict. + """ + if not _is_postgres_url(url): + return url, {} + + parsed = urlparse(url) + qp = parse_qs(parsed.query, keep_blank_values=True) + + # Prefer sslmode (libpq canonical) over the bare ``ssl`` key. + sslmode_val = _pop_first(qp, 'sslmode') + ssl_val = _pop_first(qp, 'ssl') + ssl_mode = sslmode_val or ssl_val + + ssl_dict: dict[str, str] = {} + if ssl_mode: + ssl_dict['sslmode'] = ssl_mode + for key in ('sslrootcert', 'sslcert', 'sslkey', 'sslcrl'): + val = _pop_first(qp, key) + if val: + ssl_dict[key] = val + + if not ssl_dict: + return url, ssl_dict + + cleaned_query = urlencode(qp, doseq=True) + return urlunparse(parsed._replace(query=cleaned_query)), ssl_dict + + +def reattach_ssl_params_to_url(url_without_ssl: str, ssl_dict: dict[str, str]) -> str: + """Re-append SSL query-string parameters to a cleaned PostgreSQL URL. + + Used for psycopg2/libpq consumers that expect ``sslmode`` and the + certificate-file keys in the connection string. + """ + if not ssl_dict: + return url_without_ssl + + parts = [f'{k}={v}' for k, v in ssl_dict.items() if v] + if not parts: + return url_without_ssl + + sep = '&' if '?' in url_without_ssl else '?' + return f'{url_without_ssl}{sep}{"&".join(parts)}' + + +# Backwards-compatible aliases for external callers. +extract_ssl_mode_from_url = extract_ssl_params_from_url +reattach_ssl_mode_to_url = reattach_ssl_params_to_url + + +class JSONField(types.TypeDecorator): + impl = types.Text + cache_ok = True + + def process_bind_param(self, value: Optional[_T], dialect: Dialect) -> Any: + return json.dumps(value) + + def process_result_value(self, value: Optional[_T], dialect: Dialect) -> Any: + if value is not None: + return json.loads(value) + + def copy(self, **kw: Any) -> Self: + return JSONField(self.impl.length) + + def db_value(self, value): + return json.dumps(value) + + def python_value(self, value): + if value is not None: + return json.loads(value) + + +# Workaround to handle the peewee migration +# This is required to ensure the peewee migration is handled before the alembic migration +def handle_peewee_migration(DATABASE_URL): + db = None + try: + # Normalize SSL params so psycopg2 always sees `sslmode=` (never `ssl=`) + # and cert-file params are preserved in the connection string. + url_without_ssl, ssl_params = extract_ssl_params_from_url(DATABASE_URL) + normalized_url = reattach_ssl_params_to_url(url_without_ssl, ssl_params) + + # Replace the postgresql:// with postgres:// to handle the peewee migration + db = register_connection(normalized_url.replace('postgresql://', 'postgres://')) + migrate_dir = OPEN_WEBUI_DIR / 'internal' / 'migrations' + router = Router(db, logger=log, migrate_dir=migrate_dir) + router.run() + db.close() + + except Exception as e: + log.error(f'Failed to initialize the database connection: {e}') + log.warning('Hint: If your database password contains special characters, you may need to URL-encode it.') + raise + finally: + # Properly closing the database connection + if db and not db.is_closed(): + db.close() + + # Assert if db connection has been closed + if db is not None: + assert db.is_closed(), 'Database connection is still open.' + + +if ENABLE_DB_MIGRATIONS: + handle_peewee_migration(DATABASE_URL) + + +# Normalize SSL params from the URL once; the sync engine needs them +# reattached in canonical libpq form for psycopg2. +_url_without_ssl, _ssl_dict = extract_ssl_params_from_url(DATABASE_URL) + +# For psycopg2 (sync engine), re-append sslmode + cert-file params. +SQLALCHEMY_DATABASE_URL = reattach_ssl_params_to_url(_url_without_ssl, _ssl_dict) if _ssl_dict else DATABASE_URL + + +def _make_async_url(url: str) -> str: + """Convert a sync database URL to its async driver equivalent. + + The async engine uses psycopg (v3) which speaks libpq natively, + so all standard connection-string parameters (``sslmode``, + ``options``, ``target_session_attrs``, etc.) are passed through + without any translation. + """ + if url.startswith('sqlite+sqlcipher://'): + raise ValueError( + 'sqlite+sqlcipher:// URLs are not supported with async engine. ' + 'Use standard sqlite:// or postgresql:// instead.' + ) + if url.startswith('sqlite:///') or url.startswith('sqlite://'): + return url.replace('sqlite://', 'sqlite+aiosqlite://', 1) + # psycopg v3 — auto-selects async mode with create_async_engine + if url.startswith('postgresql+psycopg2://'): + return url.replace('postgresql+psycopg2://', 'postgresql+psycopg://', 1) + if url.startswith('postgresql://'): + return url.replace('postgresql://', 'postgresql+psycopg://', 1) + if url.startswith('postgres://'): + return url.replace('postgres://', 'postgresql+psycopg://', 1) + # For other dialects, return as-is and let SQLAlchemy handle it + return url + + +# ============================================================ +# SYNC ENGINE (used only for: startup migrations, config loading, +# Alembic, peewee migration, health checks) +# ============================================================ + +# Handle SQLCipher URLs +if SQLALCHEMY_DATABASE_URL.startswith('sqlite+sqlcipher://'): + database_password = os.environ.get('DATABASE_PASSWORD') + if not database_password or database_password.strip() == '': + raise ValueError('DATABASE_PASSWORD is required when using sqlite+sqlcipher:// URLs') + + # Extract database path from SQLCipher URL + db_path = SQLALCHEMY_DATABASE_URL.replace('sqlite+sqlcipher://', '') + + # Create a custom creator function that uses sqlcipher3 + def create_sqlcipher_connection(): + import sqlcipher3 + + conn = sqlcipher3.connect(db_path, check_same_thread=False) + conn.execute(f"PRAGMA key = '{database_password}'") + return conn + + # The dummy "sqlite://" URL would cause SQLAlchemy to auto-select + # SingletonThreadPool, which non-deterministically closes in-use + # connections when thread count exceeds pool_size, leading to segfaults + # in the native sqlcipher3 C library. Use NullPool by default for safety, + # or QueuePool if DATABASE_POOL_SIZE is explicitly configured. + if isinstance(DATABASE_POOL_SIZE, int) and DATABASE_POOL_SIZE > 0: + engine = create_engine( + 'sqlite://', + creator=create_sqlcipher_connection, + pool_size=DATABASE_POOL_SIZE, + max_overflow=DATABASE_POOL_MAX_OVERFLOW, + pool_timeout=DATABASE_POOL_TIMEOUT, + pool_recycle=DATABASE_POOL_RECYCLE, + pool_pre_ping=True, + poolclass=QueuePool, + echo=False, + ) + else: + engine = create_engine( + 'sqlite://', + creator=create_sqlcipher_connection, + poolclass=NullPool, + echo=False, + ) + + log.info('Connected to encrypted SQLite database using SQLCipher') + +elif 'sqlite' in SQLALCHEMY_DATABASE_URL: + engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={'check_same_thread': False}) + + def _apply_sqlite_pragmas(dbapi_connection): + """Apply all configured SQLite PRAGMAs to a raw DBAPI connection.""" + cursor = dbapi_connection.cursor() + if DATABASE_ENABLE_SQLITE_WAL: + cursor.execute('PRAGMA journal_mode=WAL') + else: + cursor.execute('PRAGMA journal_mode=DELETE') + + # Each PRAGMA is skipped when its env var is empty, allowing opt-out. + if DATABASE_SQLITE_PRAGMA_SYNCHRONOUS: + cursor.execute(f'PRAGMA synchronous={DATABASE_SQLITE_PRAGMA_SYNCHRONOUS}') + if DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT: + cursor.execute(f'PRAGMA busy_timeout={DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT}') + if DATABASE_SQLITE_PRAGMA_CACHE_SIZE: + cursor.execute(f'PRAGMA cache_size={DATABASE_SQLITE_PRAGMA_CACHE_SIZE}') + if DATABASE_SQLITE_PRAGMA_TEMP_STORE: + cursor.execute(f'PRAGMA temp_store={DATABASE_SQLITE_PRAGMA_TEMP_STORE}') + if DATABASE_SQLITE_PRAGMA_MMAP_SIZE: + cursor.execute(f'PRAGMA mmap_size={DATABASE_SQLITE_PRAGMA_MMAP_SIZE}') + if DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT: + cursor.execute(f'PRAGMA journal_size_limit={DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT}') + cursor.close() + + def on_connect(dbapi_connection, connection_record): + _apply_sqlite_pragmas(dbapi_connection) + + event.listen(engine, 'connect', on_connect) +else: + if isinstance(DATABASE_POOL_SIZE, int): + if DATABASE_POOL_SIZE > 0: + engine = create_engine( + SQLALCHEMY_DATABASE_URL, + pool_size=DATABASE_POOL_SIZE, + max_overflow=DATABASE_POOL_MAX_OVERFLOW, + pool_timeout=DATABASE_POOL_TIMEOUT, + pool_recycle=DATABASE_POOL_RECYCLE, + pool_pre_ping=True, + poolclass=QueuePool, + ) + else: + engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True, poolclass=NullPool) + else: + engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True) + + +# Sync session — used ONLY for startup config loading (config.py runs at import time) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False) +metadata_obj = MetaData(schema=DATABASE_SCHEMA) +Base = declarative_base(metadata=metadata_obj) +ScopedSession = scoped_session(SessionLocal) + + +def get_session(): + """Sync session generator — used ONLY for startup/config operations.""" + db = SessionLocal() + try: + yield db + finally: + db.close() + + +get_db = contextmanager(get_session) + + +# ============================================================ +# 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. +# all work without any stripping or translation. +ASYNC_SQLALCHEMY_DATABASE_URL = _make_async_url(SQLALCHEMY_DATABASE_URL) + +if 'sqlite' in ASYNC_SQLALCHEMY_DATABASE_URL: + # Generous default — async coroutines + no session sharing = high connection demand. + _sqlite_pool_size = DATABASE_POOL_SIZE if isinstance(DATABASE_POOL_SIZE, int) and DATABASE_POOL_SIZE > 0 else 512 + async_engine = create_async_engine( + ASYNC_SQLALCHEMY_DATABASE_URL, + connect_args={'check_same_thread': False}, + pool_size=_sqlite_pool_size, + pool_timeout=DATABASE_POOL_TIMEOUT, + pool_recycle=DATABASE_POOL_RECYCLE, + pool_pre_ping=True, + ) + + @event.listens_for(async_engine.sync_engine, 'connect') + def _set_sqlite_pragmas(dbapi_connection, connection_record): + _apply_sqlite_pragmas(dbapi_connection) +else: + if isinstance(DATABASE_POOL_SIZE, int): + if DATABASE_POOL_SIZE > 0: + async_engine = create_async_engine( + ASYNC_SQLALCHEMY_DATABASE_URL, + pool_size=DATABASE_POOL_SIZE, + max_overflow=DATABASE_POOL_MAX_OVERFLOW, + pool_timeout=DATABASE_POOL_TIMEOUT, + pool_recycle=DATABASE_POOL_RECYCLE, + pool_pre_ping=True, + ) + else: + async_engine = create_async_engine( + ASYNC_SQLALCHEMY_DATABASE_URL, + pool_pre_ping=True, + poolclass=NullPool, + ) + else: + async_engine = create_async_engine( + ASYNC_SQLALCHEMY_DATABASE_URL, + pool_pre_ping=True, + ) + + +AsyncSessionLocal = async_sessionmaker( + bind=async_engine, + class_=AsyncSession, + autocommit=False, + autoflush=False, + expire_on_commit=False, +) + + +async def get_async_session(): + """Async session generator for FastAPI Depends().""" + async with AsyncSessionLocal() as db: + try: + yield db + finally: + await db.close() + + +@asynccontextmanager +async def get_async_db(): + """Async context manager for use outside of FastAPI dependency injection.""" + async with AsyncSessionLocal() as db: + try: + yield db + finally: + await db.close() + + +@asynccontextmanager +async def get_async_db_context(db: Optional[AsyncSession] = None): + """Async context manager that reuses an existing session if provided and session sharing is enabled.""" + if isinstance(db, AsyncSession) and DATABASE_ENABLE_SESSION_SHARING: + yield db + else: + async with get_async_db() as session: + yield session diff --git a/backend/open_webui/internal/migrations/001_initial_schema.py b/backend/open_webui/internal/migrations/001_initial_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..4268201ae783e3273493f07dfa84cf4e0766f575 --- /dev/null +++ b/backend/open_webui/internal/migrations/001_initial_schema.py @@ -0,0 +1,253 @@ +"""Peewee migrations -- 001_initial_schema.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + # We perform different migrations for SQLite and other databases + # This is because SQLite is very loose with enforcing its schema, and trying to migrate other databases like SQLite + # will require per-database SQL queries. + # Instead, we assume that because external DB support was added at a later date, it is safe to assume a newer base + # schema instead of trying to migrate from an older schema. + if isinstance(database, pw.SqliteDatabase): + migrate_sqlite(migrator, database, fake=fake) + else: + migrate_external(migrator, database, fake=fake) + + +def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False): + @migrator.create_model + class Auth(pw.Model): + id = pw.CharField(max_length=255, unique=True) + email = pw.CharField(max_length=255) + password = pw.CharField(max_length=255) + active = pw.BooleanField() + + class Meta: + table_name = 'auth' + + @migrator.create_model + class Chat(pw.Model): + id = pw.CharField(max_length=255, unique=True) + user_id = pw.CharField(max_length=255) + title = pw.CharField() + chat = pw.TextField() + timestamp = pw.BigIntegerField() + + class Meta: + table_name = 'chat' + + @migrator.create_model + class ChatIdTag(pw.Model): + id = pw.CharField(max_length=255, unique=True) + tag_name = pw.CharField(max_length=255) + chat_id = pw.CharField(max_length=255) + user_id = pw.CharField(max_length=255) + timestamp = pw.BigIntegerField() + + class Meta: + table_name = 'chatidtag' + + @migrator.create_model + class Document(pw.Model): + id = pw.AutoField() + collection_name = pw.CharField(max_length=255, unique=True) + name = pw.CharField(max_length=255, unique=True) + title = pw.CharField() + filename = pw.CharField() + content = pw.TextField(null=True) + user_id = pw.CharField(max_length=255) + timestamp = pw.BigIntegerField() + + class Meta: + table_name = 'document' + + @migrator.create_model + class Modelfile(pw.Model): + id = pw.AutoField() + tag_name = pw.CharField(max_length=255, unique=True) + user_id = pw.CharField(max_length=255) + modelfile = pw.TextField() + timestamp = pw.BigIntegerField() + + class Meta: + table_name = 'modelfile' + + @migrator.create_model + class Prompt(pw.Model): + id = pw.AutoField() + command = pw.CharField(max_length=255, unique=True) + user_id = pw.CharField(max_length=255) + title = pw.CharField() + content = pw.TextField() + timestamp = pw.BigIntegerField() + + class Meta: + table_name = 'prompt' + + @migrator.create_model + class Tag(pw.Model): + id = pw.CharField(max_length=255, unique=True) + name = pw.CharField(max_length=255) + user_id = pw.CharField(max_length=255) + data = pw.TextField(null=True) + + class Meta: + table_name = 'tag' + + @migrator.create_model + class User(pw.Model): + id = pw.CharField(max_length=255, unique=True) + name = pw.CharField(max_length=255) + email = pw.CharField(max_length=255) + role = pw.CharField(max_length=255) + profile_image_url = pw.CharField(max_length=255) + timestamp = pw.BigIntegerField() + + class Meta: + table_name = 'user' + + +def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False): + @migrator.create_model + class Auth(pw.Model): + id = pw.CharField(max_length=255, unique=True) + email = pw.CharField(max_length=255) + password = pw.TextField() + active = pw.BooleanField() + + class Meta: + table_name = 'auth' + + @migrator.create_model + class Chat(pw.Model): + id = pw.CharField(max_length=255, unique=True) + user_id = pw.CharField(max_length=255) + title = pw.TextField() + chat = pw.TextField() + timestamp = pw.BigIntegerField() + + class Meta: + table_name = 'chat' + + @migrator.create_model + class ChatIdTag(pw.Model): + id = pw.CharField(max_length=255, unique=True) + tag_name = pw.CharField(max_length=255) + chat_id = pw.CharField(max_length=255) + user_id = pw.CharField(max_length=255) + timestamp = pw.BigIntegerField() + + class Meta: + table_name = 'chatidtag' + + @migrator.create_model + class Document(pw.Model): + id = pw.AutoField() + collection_name = pw.CharField(max_length=255, unique=True) + name = pw.CharField(max_length=255, unique=True) + title = pw.TextField() + filename = pw.TextField() + content = pw.TextField(null=True) + user_id = pw.CharField(max_length=255) + timestamp = pw.BigIntegerField() + + class Meta: + table_name = 'document' + + @migrator.create_model + class Modelfile(pw.Model): + id = pw.AutoField() + tag_name = pw.CharField(max_length=255, unique=True) + user_id = pw.CharField(max_length=255) + modelfile = pw.TextField() + timestamp = pw.BigIntegerField() + + class Meta: + table_name = 'modelfile' + + @migrator.create_model + class Prompt(pw.Model): + id = pw.AutoField() + command = pw.CharField(max_length=255, unique=True) + user_id = pw.CharField(max_length=255) + title = pw.TextField() + content = pw.TextField() + timestamp = pw.BigIntegerField() + + class Meta: + table_name = 'prompt' + + @migrator.create_model + class Tag(pw.Model): + id = pw.CharField(max_length=255, unique=True) + name = pw.CharField(max_length=255) + user_id = pw.CharField(max_length=255) + data = pw.TextField(null=True) + + class Meta: + table_name = 'tag' + + @migrator.create_model + class User(pw.Model): + id = pw.CharField(max_length=255, unique=True) + name = pw.CharField(max_length=255) + email = pw.CharField(max_length=255) + role = pw.CharField(max_length=255) + profile_image_url = pw.TextField() + timestamp = pw.BigIntegerField() + + class Meta: + table_name = 'user' + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_model('user') + + migrator.remove_model('tag') + + migrator.remove_model('prompt') + + migrator.remove_model('modelfile') + + migrator.remove_model('document') + + migrator.remove_model('chatidtag') + + migrator.remove_model('chat') + + migrator.remove_model('auth') diff --git a/backend/open_webui/internal/migrations/002_add_local_sharing.py b/backend/open_webui/internal/migrations/002_add_local_sharing.py new file mode 100644 index 0000000000000000000000000000000000000000..e3e557602b210e600ec73d30361e0b516d91583f --- /dev/null +++ b/backend/open_webui/internal/migrations/002_add_local_sharing.py @@ -0,0 +1,45 @@ +"""Peewee migrations -- 002_add_local_sharing.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + migrator.add_fields('chat', share_id=pw.CharField(max_length=255, null=True, unique=True)) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_fields('chat', 'share_id') diff --git a/backend/open_webui/internal/migrations/003_add_auth_api_key.py b/backend/open_webui/internal/migrations/003_add_auth_api_key.py new file mode 100644 index 0000000000000000000000000000000000000000..acb63fc7280316572e53f49fe6fa9f04fdf267af --- /dev/null +++ b/backend/open_webui/internal/migrations/003_add_auth_api_key.py @@ -0,0 +1,45 @@ +"""Peewee migrations -- 002_add_local_sharing.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + migrator.add_fields('user', api_key=pw.CharField(max_length=255, null=True, unique=True)) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_fields('user', 'api_key') diff --git a/backend/open_webui/internal/migrations/004_add_archived.py b/backend/open_webui/internal/migrations/004_add_archived.py new file mode 100644 index 0000000000000000000000000000000000000000..abed1727b9babe6b36a8a073b6dea88011d16dc2 --- /dev/null +++ b/backend/open_webui/internal/migrations/004_add_archived.py @@ -0,0 +1,45 @@ +"""Peewee migrations -- 002_add_local_sharing.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + migrator.add_fields('chat', archived=pw.BooleanField(default=False)) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_fields('chat', 'archived') diff --git a/backend/open_webui/internal/migrations/005_add_updated_at.py b/backend/open_webui/internal/migrations/005_add_updated_at.py new file mode 100644 index 0000000000000000000000000000000000000000..bff311e2d4b031c34cfbf80d39a2408827394d28 --- /dev/null +++ b/backend/open_webui/internal/migrations/005_add_updated_at.py @@ -0,0 +1,125 @@ +"""Peewee migrations -- 002_add_local_sharing.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + if isinstance(database, pw.SqliteDatabase): + migrate_sqlite(migrator, database, fake=fake) + else: + migrate_external(migrator, database, fake=fake) + + +def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False): + # Adding fields created_at and updated_at to the 'chat' table + migrator.add_fields( + 'chat', + created_at=pw.DateTimeField(null=True), # Allow null for transition + updated_at=pw.DateTimeField(null=True), # Allow null for transition + ) + + # Populate the new fields from an existing 'timestamp' field + migrator.sql('UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL') + + # Now that the data has been copied, remove the original 'timestamp' field + migrator.remove_fields('chat', 'timestamp') + + # Update the fields to be not null now that they are populated + migrator.change_fields( + 'chat', + created_at=pw.DateTimeField(null=False), + updated_at=pw.DateTimeField(null=False), + ) + + +def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False): + # Adding fields created_at and updated_at to the 'chat' table + migrator.add_fields( + 'chat', + created_at=pw.BigIntegerField(null=True), # Allow null for transition + updated_at=pw.BigIntegerField(null=True), # Allow null for transition + ) + + # Populate the new fields from an existing 'timestamp' field + migrator.sql('UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL') + + # Now that the data has been copied, remove the original 'timestamp' field + migrator.remove_fields('chat', 'timestamp') + + # Update the fields to be not null now that they are populated + migrator.change_fields( + 'chat', + created_at=pw.BigIntegerField(null=False), + updated_at=pw.BigIntegerField(null=False), + ) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + if isinstance(database, pw.SqliteDatabase): + rollback_sqlite(migrator, database, fake=fake) + else: + rollback_external(migrator, database, fake=fake) + + +def rollback_sqlite(migrator: Migrator, database: pw.Database, *, fake=False): + # Recreate the timestamp field initially allowing null values for safe transition + migrator.add_fields('chat', timestamp=pw.DateTimeField(null=True)) + + # Copy the earliest created_at date back into the new timestamp field + # This assumes created_at was originally a copy of timestamp + migrator.sql('UPDATE chat SET timestamp = created_at') + + # Remove the created_at and updated_at fields + migrator.remove_fields('chat', 'created_at', 'updated_at') + + # Finally, alter the timestamp field to not allow nulls if that was the original setting + migrator.change_fields('chat', timestamp=pw.DateTimeField(null=False)) + + +def rollback_external(migrator: Migrator, database: pw.Database, *, fake=False): + # Recreate the timestamp field initially allowing null values for safe transition + migrator.add_fields('chat', timestamp=pw.BigIntegerField(null=True)) + + # Copy the earliest created_at date back into the new timestamp field + # This assumes created_at was originally a copy of timestamp + migrator.sql('UPDATE chat SET timestamp = created_at') + + # Remove the created_at and updated_at fields + migrator.remove_fields('chat', 'created_at', 'updated_at') + + # Finally, alter the timestamp field to not allow nulls if that was the original setting + migrator.change_fields('chat', timestamp=pw.BigIntegerField(null=False)) diff --git a/backend/open_webui/internal/migrations/006_migrate_timestamps_and_charfields.py b/backend/open_webui/internal/migrations/006_migrate_timestamps_and_charfields.py new file mode 100644 index 0000000000000000000000000000000000000000..86f90eb8806d5308b13e0451be13c2d6b7a2f0dd --- /dev/null +++ b/backend/open_webui/internal/migrations/006_migrate_timestamps_and_charfields.py @@ -0,0 +1,129 @@ +"""Peewee migrations -- 006_migrate_timestamps_and_charfields.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + # Alter the tables with timestamps + migrator.change_fields( + 'chatidtag', + timestamp=pw.BigIntegerField(), + ) + migrator.change_fields( + 'document', + timestamp=pw.BigIntegerField(), + ) + migrator.change_fields( + 'modelfile', + timestamp=pw.BigIntegerField(), + ) + migrator.change_fields( + 'prompt', + timestamp=pw.BigIntegerField(), + ) + migrator.change_fields( + 'user', + timestamp=pw.BigIntegerField(), + ) + # Alter the tables with varchar to text where necessary + migrator.change_fields( + 'auth', + password=pw.TextField(), + ) + migrator.change_fields( + 'chat', + title=pw.TextField(), + ) + migrator.change_fields( + 'document', + title=pw.TextField(), + filename=pw.TextField(), + ) + migrator.change_fields( + 'prompt', + title=pw.TextField(), + ) + migrator.change_fields( + 'user', + profile_image_url=pw.TextField(), + ) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + if isinstance(database, pw.SqliteDatabase): + # Alter the tables with timestamps + migrator.change_fields( + 'chatidtag', + timestamp=pw.DateField(), + ) + migrator.change_fields( + 'document', + timestamp=pw.DateField(), + ) + migrator.change_fields( + 'modelfile', + timestamp=pw.DateField(), + ) + migrator.change_fields( + 'prompt', + timestamp=pw.DateField(), + ) + migrator.change_fields( + 'user', + timestamp=pw.DateField(), + ) + migrator.change_fields( + 'auth', + password=pw.CharField(max_length=255), + ) + migrator.change_fields( + 'chat', + title=pw.CharField(), + ) + migrator.change_fields( + 'document', + title=pw.CharField(), + filename=pw.CharField(), + ) + migrator.change_fields( + 'prompt', + title=pw.CharField(), + ) + migrator.change_fields( + 'user', + profile_image_url=pw.CharField(), + ) diff --git a/backend/open_webui/internal/migrations/007_add_user_last_active_at.py b/backend/open_webui/internal/migrations/007_add_user_last_active_at.py new file mode 100644 index 0000000000000000000000000000000000000000..19a26c35154f3d28a53ddb69880c9c84eeae9be7 --- /dev/null +++ b/backend/open_webui/internal/migrations/007_add_user_last_active_at.py @@ -0,0 +1,78 @@ +"""Peewee migrations -- 002_add_local_sharing.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + # Adding fields created_at and updated_at to the 'user' table + migrator.add_fields( + 'user', + created_at=pw.BigIntegerField(null=True), # Allow null for transition + updated_at=pw.BigIntegerField(null=True), # Allow null for transition + last_active_at=pw.BigIntegerField(null=True), # Allow null for transition + ) + + # Populate the new fields from an existing 'timestamp' field + migrator.sql( + 'UPDATE "user" SET created_at = timestamp, updated_at = timestamp, last_active_at = timestamp WHERE timestamp IS NOT NULL' + ) + + # Now that the data has been copied, remove the original 'timestamp' field + migrator.remove_fields('user', 'timestamp') + + # Update the fields to be not null now that they are populated + migrator.change_fields( + 'user', + created_at=pw.BigIntegerField(null=False), + updated_at=pw.BigIntegerField(null=False), + last_active_at=pw.BigIntegerField(null=False), + ) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + # Recreate the timestamp field initially allowing null values for safe transition + migrator.add_fields('user', timestamp=pw.BigIntegerField(null=True)) + + # Copy the earliest created_at date back into the new timestamp field + # This assumes created_at was originally a copy of timestamp + migrator.sql('UPDATE "user" SET timestamp = created_at') + + # Remove the created_at and updated_at fields + migrator.remove_fields('user', 'created_at', 'updated_at', 'last_active_at') + + # Finally, alter the timestamp field to not allow nulls if that was the original setting + migrator.change_fields('user', timestamp=pw.BigIntegerField(null=False)) diff --git a/backend/open_webui/internal/migrations/008_add_memory.py b/backend/open_webui/internal/migrations/008_add_memory.py new file mode 100644 index 0000000000000000000000000000000000000000..f3af64fe95c3a72ab1cf644bc6ebcc2c4f1e3b93 --- /dev/null +++ b/backend/open_webui/internal/migrations/008_add_memory.py @@ -0,0 +1,52 @@ +"""Peewee migrations -- 002_add_local_sharing.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + @migrator.create_model + class Memory(pw.Model): + id = pw.CharField(max_length=255, unique=True) + user_id = pw.CharField(max_length=255) + content = pw.TextField(null=False) + updated_at = pw.BigIntegerField(null=False) + created_at = pw.BigIntegerField(null=False) + + class Meta: + table_name = 'memory' + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_model('memory') diff --git a/backend/open_webui/internal/migrations/009_add_models.py b/backend/open_webui/internal/migrations/009_add_models.py new file mode 100644 index 0000000000000000000000000000000000000000..45f4a3d1631dc224281feaf835db7093baaf949d --- /dev/null +++ b/backend/open_webui/internal/migrations/009_add_models.py @@ -0,0 +1,60 @@ +"""Peewee migrations -- 009_add_models.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + @migrator.create_model + class Model(pw.Model): + id = pw.TextField(unique=True) + user_id = pw.TextField() + base_model_id = pw.TextField(null=True) + + name = pw.TextField() + + meta = pw.TextField() + params = pw.TextField() + + created_at = pw.BigIntegerField(null=False) + updated_at = pw.BigIntegerField(null=False) + + class Meta: + table_name = 'model' + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_model('model') diff --git a/backend/open_webui/internal/migrations/010_migrate_modelfiles_to_models.py b/backend/open_webui/internal/migrations/010_migrate_modelfiles_to_models.py new file mode 100644 index 0000000000000000000000000000000000000000..e523d6a09823e5620c796e2ca9b9ac9110c3764c --- /dev/null +++ b/backend/open_webui/internal/migrations/010_migrate_modelfiles_to_models.py @@ -0,0 +1,130 @@ +"""Peewee migrations -- 009_add_models.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator +import json + +from open_webui.utils.misc import parse_ollama_modelfile + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + # Fetch data from 'modelfile' table and insert into 'model' table + migrate_modelfile_to_model(migrator, database) + # Drop the 'modelfile' table + migrator.remove_model('modelfile') + + +def migrate_modelfile_to_model(migrator: Migrator, database: pw.Database): + ModelFile = migrator.orm['modelfile'] + Model = migrator.orm['model'] + + modelfiles = ModelFile.select() + + for modelfile in modelfiles: + # Extract and transform data in Python + + modelfile.modelfile = json.loads(modelfile.modelfile) + meta = json.dumps( + { + 'description': modelfile.modelfile.get('desc'), + 'profile_image_url': modelfile.modelfile.get('imageUrl'), + 'ollama': {'modelfile': modelfile.modelfile.get('content')}, + 'suggestion_prompts': modelfile.modelfile.get('suggestionPrompts'), + 'categories': modelfile.modelfile.get('categories'), + 'user': {**modelfile.modelfile.get('user', {}), 'community': True}, + } + ) + + info = parse_ollama_modelfile(modelfile.modelfile.get('content')) + + # Insert the processed data into the 'model' table + Model.create( + id=f'ollama-{modelfile.tag_name}', + user_id=modelfile.user_id, + base_model_id=info.get('base_model_id'), + name=modelfile.modelfile.get('title'), + meta=meta, + params=json.dumps(info.get('params', {})), + created_at=modelfile.timestamp, + updated_at=modelfile.timestamp, + ) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + recreate_modelfile_table(migrator, database) + move_data_back_to_modelfile(migrator, database) + migrator.remove_model('model') + + +def recreate_modelfile_table(migrator: Migrator, database: pw.Database): + query = """ + CREATE TABLE IF NOT EXISTS modelfile ( + user_id TEXT, + tag_name TEXT, + modelfile JSON, + timestamp BIGINT + ) + """ + migrator.sql(query) + + +def move_data_back_to_modelfile(migrator: Migrator, database: pw.Database): + Model = migrator.orm['model'] + Modelfile = migrator.orm['modelfile'] + + models = Model.select() + + for model in models: + # Extract and transform data in Python + meta = json.loads(model.meta) + + modelfile_data = { + 'title': model.name, + 'desc': meta.get('description'), + 'imageUrl': meta.get('profile_image_url'), + 'content': meta.get('ollama', {}).get('modelfile'), + 'suggestionPrompts': meta.get('suggestion_prompts'), + 'categories': meta.get('categories'), + 'user': {k: v for k, v in meta.get('user', {}).items() if k != 'community'}, + } + + # Insert the processed data back into the 'modelfile' table + Modelfile.create( + user_id=model.user_id, + tag_name=model.id, + modelfile=modelfile_data, + timestamp=model.created_at, + ) diff --git a/backend/open_webui/internal/migrations/011_add_user_settings.py b/backend/open_webui/internal/migrations/011_add_user_settings.py new file mode 100644 index 0000000000000000000000000000000000000000..73d27392f7e5b14b86a2d8b304f2dd54fe72d9eb --- /dev/null +++ b/backend/open_webui/internal/migrations/011_add_user_settings.py @@ -0,0 +1,47 @@ +"""Peewee migrations -- 002_add_local_sharing.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + # Adding fields settings to the 'user' table + migrator.add_fields('user', settings=pw.TextField(null=True)) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + # Remove the settings field + migrator.remove_fields('user', 'settings') diff --git a/backend/open_webui/internal/migrations/012_add_tools.py b/backend/open_webui/internal/migrations/012_add_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..a488678c3c2c5d6ccd0b2c1d2a4ce7d52c698ec4 --- /dev/null +++ b/backend/open_webui/internal/migrations/012_add_tools.py @@ -0,0 +1,60 @@ +"""Peewee migrations -- 009_add_models.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + @migrator.create_model + class Tool(pw.Model): + id = pw.TextField(unique=True) + user_id = pw.TextField() + + name = pw.TextField() + content = pw.TextField() + specs = pw.TextField() + + meta = pw.TextField() + + created_at = pw.BigIntegerField(null=False) + updated_at = pw.BigIntegerField(null=False) + + class Meta: + table_name = 'tool' + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_model('tool') diff --git a/backend/open_webui/internal/migrations/013_add_user_info.py b/backend/open_webui/internal/migrations/013_add_user_info.py new file mode 100644 index 0000000000000000000000000000000000000000..db77cfff3ae443dea3840ad3284c843803610969 --- /dev/null +++ b/backend/open_webui/internal/migrations/013_add_user_info.py @@ -0,0 +1,47 @@ +"""Peewee migrations -- 002_add_local_sharing.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + # Adding fields info to the 'user' table + migrator.add_fields('user', info=pw.TextField(null=True)) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + # Remove the settings field + migrator.remove_fields('user', 'info') diff --git a/backend/open_webui/internal/migrations/014_add_files.py b/backend/open_webui/internal/migrations/014_add_files.py new file mode 100644 index 0000000000000000000000000000000000000000..9c01ac08c37f6cc51fdbe8d3598f53d93d4d5c85 --- /dev/null +++ b/backend/open_webui/internal/migrations/014_add_files.py @@ -0,0 +1,54 @@ +"""Peewee migrations -- 009_add_models.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + @migrator.create_model + class File(pw.Model): + id = pw.TextField(unique=True) + user_id = pw.TextField() + filename = pw.TextField() + meta = pw.TextField() + created_at = pw.BigIntegerField(null=False) + + class Meta: + table_name = 'file' + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_model('file') diff --git a/backend/open_webui/internal/migrations/015_add_functions.py b/backend/open_webui/internal/migrations/015_add_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..488e546ab1d49c9daff104b07f97b5039444a8c4 --- /dev/null +++ b/backend/open_webui/internal/migrations/015_add_functions.py @@ -0,0 +1,60 @@ +"""Peewee migrations -- 009_add_models.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + @migrator.create_model + class Function(pw.Model): + id = pw.TextField(unique=True) + user_id = pw.TextField() + + name = pw.TextField() + type = pw.TextField() + + content = pw.TextField() + meta = pw.TextField() + + created_at = pw.BigIntegerField(null=False) + updated_at = pw.BigIntegerField(null=False) + + class Meta: + table_name = 'function' + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_model('function') diff --git a/backend/open_webui/internal/migrations/016_add_valves_and_is_active.py b/backend/open_webui/internal/migrations/016_add_valves_and_is_active.py new file mode 100644 index 0000000000000000000000000000000000000000..57a2dfbd5b5f77bac0f787525abd9ecb1c37f5e4 --- /dev/null +++ b/backend/open_webui/internal/migrations/016_add_valves_and_is_active.py @@ -0,0 +1,49 @@ +"""Peewee migrations -- 009_add_models.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + migrator.add_fields('tool', valves=pw.TextField(null=True)) + migrator.add_fields('function', valves=pw.TextField(null=True)) + migrator.add_fields('function', is_active=pw.BooleanField(default=False)) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_fields('tool', 'valves') + migrator.remove_fields('function', 'valves') + migrator.remove_fields('function', 'is_active') diff --git a/backend/open_webui/internal/migrations/017_add_user_oauth_sub.py b/backend/open_webui/internal/migrations/017_add_user_oauth_sub.py new file mode 100644 index 0000000000000000000000000000000000000000..f998c742d1d3d9ac1dc7c1254b37727f9692fe56 --- /dev/null +++ b/backend/open_webui/internal/migrations/017_add_user_oauth_sub.py @@ -0,0 +1,44 @@ +"""Peewee migrations -- 017_add_user_oauth_sub.py. +Some examples (model - class or model name):: + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + migrator.add_fields( + 'user', + oauth_sub=pw.TextField(null=True, unique=True), + ) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_fields('user', 'oauth_sub') diff --git a/backend/open_webui/internal/migrations/018_add_function_is_global.py b/backend/open_webui/internal/migrations/018_add_function_is_global.py new file mode 100644 index 0000000000000000000000000000000000000000..7f7cd4f725feba91daa5898fc878e61de18950ff --- /dev/null +++ b/backend/open_webui/internal/migrations/018_add_function_is_global.py @@ -0,0 +1,48 @@ +"""Peewee migrations -- 017_add_user_oauth_sub.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + migrator.add_fields( + 'function', + is_global=pw.BooleanField(default=False), + ) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_fields('function', 'is_global') diff --git a/backend/open_webui/internal/wrappers.py b/backend/open_webui/internal/wrappers.py new file mode 100644 index 0000000000000000000000000000000000000000..3d54d02e3ab931b76cebb558b40fd5726e14ccec --- /dev/null +++ b/backend/open_webui/internal/wrappers.py @@ -0,0 +1,84 @@ +import logging +import os +from contextvars import ContextVar + +from peewee import * +from peewee import InterfaceError as PeeWeeInterfaceError +from peewee import PostgresqlDatabase +from playhouse.db_url import connect, parse +from playhouse.shortcuts import ReconnectMixin + +log = logging.getLogger(__name__) + +db_state_default = {'closed': None, 'conn': None, 'ctx': None, 'transactions': None} +db_state = ContextVar('db_state', default=db_state_default.copy()) + + +class PeeweeConnectionState(object): + def __init__(self, **kwargs): + super().__setattr__('_state', db_state) + super().__init__(**kwargs) + + def __setattr__(self, name, value): + self._state.get()[name] = value + + def __getattr__(self, name): + value = self._state.get()[name] + return value + + +class CustomReconnectMixin(ReconnectMixin): + reconnect_errors = ( + # psycopg2 + (OperationalError, 'termin'), + (InterfaceError, 'closed'), + # peewee + (PeeWeeInterfaceError, 'closed'), + ) + + +class ReconnectingPostgresqlDatabase(CustomReconnectMixin, PostgresqlDatabase): + pass + + +def register_connection(db_url): + # Check if using SQLCipher protocol + if db_url.startswith('sqlite+sqlcipher://'): + database_password = os.environ.get('DATABASE_PASSWORD') + if not database_password or database_password.strip() == '': + raise ValueError('DATABASE_PASSWORD is required when using sqlite+sqlcipher:// URLs') + from playhouse.sqlcipher_ext import SqlCipherDatabase + + # Parse the database path from SQLCipher URL + # Convert sqlite+sqlcipher:///path/to/db.sqlite to /path/to/db.sqlite + db_path = db_url.replace('sqlite+sqlcipher://', '') + + # Use Peewee's native SqlCipherDatabase with encryption + db = SqlCipherDatabase(db_path, passphrase=database_password) + db.autoconnect = True + db.reuse_if_open = True + log.info('Connected to encrypted SQLite database using SQLCipher') + + else: + # Standard database connection (existing logic) + db = connect(db_url, unquote_user=True, unquote_password=True) + if isinstance(db, PostgresqlDatabase): + # Enable autoconnect for SQLite databases, managed by Peewee + db.autoconnect = True + db.reuse_if_open = True + log.info('Connected to PostgreSQL database') + + # Get the connection details + connection = parse(db_url, unquote_user=True, unquote_password=True) + + # Use our custom database class that supports reconnection + db = ReconnectingPostgresqlDatabase(**connection) + db.connect(reuse_if_open=True) + elif isinstance(db, SqliteDatabase): + # Enable autoconnect for SQLite databases, managed by Peewee + db.autoconnect = True + db.reuse_if_open = True + log.info('Connected to SQLite database') + else: + raise ValueError('Unsupported database connection') + return db diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py new file mode 100644 index 0000000000000000000000000000000000000000..af570af0af6cf956d7f59c8fe00017053664d2da --- /dev/null +++ b/backend/open_webui/main.py @@ -0,0 +1,2802 @@ +import asyncio +import inspect +import json +import logging +import mimetypes +import os +import shutil +import sys +import time +import random +import re +from uuid import uuid4 + + +from contextlib import asynccontextmanager +from urllib.parse import urlencode, parse_qs, urlparse +from pydantic import BaseModel +from sqlalchemy import text + +from typing import Optional +from aiocache import cached +import aiohttp +import anyio.to_thread + +from redis import Redis + + +from fastapi import ( + Depends, + FastAPI, + File, + Form, + HTTPException, + Request, + UploadFile, + status, + applications, + BackgroundTasks, +) +from fastapi.openapi.docs import get_swagger_ui_html + +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse, JSONResponse, RedirectResponse +from fastapi.staticfiles import StaticFiles + +from starlette_compress import CompressMiddleware + +from starlette.exceptions import HTTPException as StarletteHTTPException +from starlette.middleware.sessions import SessionMiddleware +from starlette.responses import Response, StreamingResponse +from starlette.datastructures import Headers + +from starsessions import ( + SessionMiddleware as StarSessionsMiddleware, + SessionAutoloadMiddleware, +) +from starsessions.stores.redis import RedisStore + +from open_webui.utils import logger +from open_webui.utils.asgi_middleware import ( + AuthTokenMiddleware, + CommitSessionMiddleware, + RedirectMiddleware, + WebsocketUpgradeGuardMiddleware, +) +from open_webui.utils.audit import AuditLevel, AuditLoggingMiddleware +from open_webui.utils.logger import start_logger +from open_webui.utils.session_pool import get_session +from open_webui.socket.main import ( + MODELS, + app as socket_app, + periodic_usage_pool_cleanup, + periodic_session_pool_cleanup, + get_event_emitter, + get_models_in_use, + get_user_id_from_session_pool, +) +from open_webui.routers import ( + analytics, + audio, + images, + ollama, + openai, + retrieval, + pipelines, + tasks, + auths, + channels, + chats, + notes, + folders, + configs, + groups, + files, + functions, + memories, + models, + knowledge, + prompts, + evaluations, + skills, + tools, + users, + utils, + scim, + terminals, + automations, + calendar, +) + +from open_webui.routers.retrieval import ( + get_embedding_function, + get_reranking_function, + get_ef, + get_rf, +) + + +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import ScopedSession, engine, get_async_session + +from open_webui.models.functions import Functions +from open_webui.models.models import Models +from open_webui.models.users import UserModel, Users +from open_webui.models.chats import Chats, ChatForm + +from open_webui.config import ( + # Ollama + ENABLE_OLLAMA_API, + OLLAMA_BASE_URLS, + OLLAMA_API_CONFIGS, + # OpenAI + ENABLE_OPENAI_API, + OPENAI_API_BASE_URLS, + OPENAI_API_KEYS, + OPENAI_API_CONFIGS, + # Direct Connections + ENABLE_DIRECT_CONNECTIONS, + # Model list + ENABLE_BASE_MODELS_CACHE, + # Thread pool size for FastAPI/AnyIO + THREAD_POOL_SIZE, + # Tool Server Configs + TOOL_SERVER_CONNECTIONS, + # Terminal Server + TERMINAL_SERVER_CONNECTIONS, + # Code Execution + ENABLE_CODE_EXECUTION, + CODE_EXECUTION_ENGINE, + CODE_EXECUTION_JUPYTER_URL, + CODE_EXECUTION_JUPYTER_AUTH, + CODE_EXECUTION_JUPYTER_AUTH_TOKEN, + CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, + CODE_EXECUTION_JUPYTER_TIMEOUT, + ENABLE_CODE_INTERPRETER, + CODE_INTERPRETER_ENGINE, + CODE_INTERPRETER_PROMPT_TEMPLATE, + CODE_INTERPRETER_JUPYTER_URL, + CODE_INTERPRETER_JUPYTER_AUTH, + CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, + CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, + CODE_INTERPRETER_JUPYTER_TIMEOUT, + ENABLE_MEMORIES, + # Image + AUTOMATIC1111_API_AUTH, + AUTOMATIC1111_BASE_URL, + AUTOMATIC1111_PARAMS, + COMFYUI_BASE_URL, + COMFYUI_API_KEY, + COMFYUI_WORKFLOW, + COMFYUI_WORKFLOW_NODES, + ENABLE_IMAGE_GENERATION, + ENABLE_IMAGE_PROMPT_GENERATION, + IMAGE_GENERATION_ENGINE, + IMAGE_GENERATION_MODEL, + IMAGE_SIZE, + IMAGE_STEPS, + IMAGES_OPENAI_API_BASE_URL, + IMAGES_OPENAI_API_VERSION, + IMAGES_OPENAI_API_KEY, + IMAGES_OPENAI_API_PARAMS, + IMAGES_GEMINI_API_BASE_URL, + IMAGES_GEMINI_API_KEY, + IMAGES_GEMINI_ENDPOINT_METHOD, + ENABLE_IMAGE_EDIT, + IMAGE_EDIT_ENGINE, + IMAGE_EDIT_MODEL, + IMAGE_EDIT_SIZE, + IMAGES_EDIT_OPENAI_API_BASE_URL, + IMAGES_EDIT_OPENAI_API_KEY, + IMAGES_EDIT_OPENAI_API_VERSION, + IMAGES_EDIT_GEMINI_API_BASE_URL, + IMAGES_EDIT_GEMINI_API_KEY, + IMAGES_EDIT_COMFYUI_BASE_URL, + IMAGES_EDIT_COMFYUI_API_KEY, + IMAGES_EDIT_COMFYUI_WORKFLOW, + IMAGES_EDIT_COMFYUI_WORKFLOW_NODES, + # Audio + AUDIO_STT_ENGINE, + AUDIO_STT_MODEL, + AUDIO_STT_SUPPORTED_CONTENT_TYPES, + AUDIO_STT_OPENAI_API_BASE_URL, + AUDIO_STT_OPENAI_API_KEY, + AUDIO_STT_AZURE_API_KEY, + AUDIO_STT_AZURE_REGION, + AUDIO_STT_AZURE_LOCALES, + AUDIO_STT_AZURE_BASE_URL, + AUDIO_STT_AZURE_MAX_SPEAKERS, + AUDIO_STT_MISTRAL_API_KEY, + AUDIO_STT_MISTRAL_API_BASE_URL, + AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS, + AUDIO_TTS_ENGINE, + AUDIO_TTS_MODEL, + AUDIO_TTS_VOICE, + AUDIO_TTS_OPENAI_API_BASE_URL, + AUDIO_TTS_OPENAI_API_KEY, + AUDIO_TTS_OPENAI_PARAMS, + AUDIO_TTS_API_KEY, + AUDIO_TTS_SPLIT_ON, + AUDIO_TTS_AZURE_SPEECH_REGION, + AUDIO_TTS_AZURE_SPEECH_BASE_URL, + AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT, + AUDIO_TTS_MISTRAL_API_KEY, + AUDIO_TTS_MISTRAL_API_BASE_URL, + PLAYWRIGHT_WS_URL, + PLAYWRIGHT_TIMEOUT, + FIRECRAWL_API_BASE_URL, + FIRECRAWL_API_KEY, + FIRECRAWL_TIMEOUT, + WEB_LOADER_ENGINE, + WEB_LOADER_CONCURRENT_REQUESTS, + WEB_LOADER_TIMEOUT, + WHISPER_MODEL, + WHISPER_VAD_FILTER, + WHISPER_LANGUAGE, + DEEPGRAM_API_KEY, + WHISPER_MODEL_AUTO_UPDATE, + WHISPER_MODEL_DIR, + # Retrieval + RAG_TEMPLATE, + DEFAULT_RAG_TEMPLATE, + RAG_FULL_CONTEXT, + BYPASS_EMBEDDING_AND_RETRIEVAL, + RAG_EMBEDDING_MODEL, + RAG_EMBEDDING_MODEL_AUTO_UPDATE, + RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE, + RAG_RERANKING_ENGINE, + RAG_RERANKING_MODEL, + RAG_EXTERNAL_RERANKER_URL, + RAG_EXTERNAL_RERANKER_API_KEY, + RAG_EXTERNAL_RERANKER_TIMEOUT, + RAG_RERANKING_BATCH_SIZE, + RAG_RERANKING_MODEL_AUTO_UPDATE, + RAG_RERANKING_MODEL_TRUST_REMOTE_CODE, + RAG_EMBEDDING_ENGINE, + RAG_EMBEDDING_BATCH_SIZE, + ENABLE_ASYNC_EMBEDDING, + RAG_EMBEDDING_CONCURRENT_REQUESTS, + RAG_TOP_K, + RAG_TOP_K_RERANKER, + RAG_RELEVANCE_THRESHOLD, + RAG_HYBRID_BM25_WEIGHT, + RAG_ALLOWED_FILE_EXTENSIONS, + RAG_FILE_MAX_COUNT, + RAG_FILE_MAX_SIZE, + FILE_IMAGE_COMPRESSION_WIDTH, + FILE_IMAGE_COMPRESSION_HEIGHT, + RAG_OPENAI_API_BASE_URL, + RAG_OPENAI_API_KEY, + RAG_AZURE_OPENAI_BASE_URL, + RAG_AZURE_OPENAI_API_KEY, + RAG_AZURE_OPENAI_API_VERSION, + RAG_OLLAMA_BASE_URL, + RAG_OLLAMA_API_KEY, + CHUNK_OVERLAP, + CHUNK_MIN_SIZE_TARGET, + CHUNK_SIZE, + CONTENT_EXTRACTION_ENGINE, + DATALAB_MARKER_API_KEY, + DATALAB_MARKER_API_BASE_URL, + DATALAB_MARKER_ADDITIONAL_CONFIG, + DATALAB_MARKER_SKIP_CACHE, + DATALAB_MARKER_FORCE_OCR, + DATALAB_MARKER_PAGINATE, + DATALAB_MARKER_STRIP_EXISTING_OCR, + DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, + DATALAB_MARKER_FORMAT_LINES, + DATALAB_MARKER_OUTPUT_FORMAT, + MINERU_API_MODE, + MINERU_API_URL, + MINERU_API_KEY, + MINERU_API_TIMEOUT, + MINERU_PARAMS, + DATALAB_MARKER_USE_LLM, + EXTERNAL_DOCUMENT_LOADER_URL, + EXTERNAL_DOCUMENT_LOADER_API_KEY, + TIKA_SERVER_URL, + DOCLING_SERVER_URL, + DOCLING_API_KEY, + DOCLING_PARAMS, + DOCUMENT_INTELLIGENCE_ENDPOINT, + DOCUMENT_INTELLIGENCE_KEY, + DOCUMENT_INTELLIGENCE_MODEL, + MISTRAL_OCR_API_BASE_URL, + MISTRAL_OCR_API_KEY, + PADDLEOCR_VL_BASE_URL, + PADDLEOCR_VL_TOKEN, + RAG_TEXT_SPLITTER, + ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER, + TIKTOKEN_ENCODING_NAME, + PDF_EXTRACT_IMAGES, + PDF_LOADER_MODE, + YOUTUBE_LOADER_LANGUAGE, + YOUTUBE_LOADER_PROXY_URL, + # Retrieval (Web Search) + ENABLE_WEB_SEARCH, + WEB_SEARCH_ENGINE, + BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL, + BYPASS_WEB_SEARCH_WEB_LOADER, + WEB_SEARCH_RESULT_COUNT, + WEB_SEARCH_CONCURRENT_REQUESTS, + WEB_FETCH_MAX_CONTENT_LENGTH, + WEB_SEARCH_TRUST_ENV, + WEB_SEARCH_DOMAIN_FILTER_LIST, + OLLAMA_CLOUD_WEB_SEARCH_API_KEY, + JINA_API_KEY, + JINA_API_BASE_URL, + SEARCHAPI_API_KEY, + SEARCHAPI_ENGINE, + SERPAPI_API_KEY, + SERPAPI_ENGINE, + SEARXNG_QUERY_URL, + SEARXNG_LANGUAGE, + YACY_QUERY_URL, + YACY_USERNAME, + YACY_PASSWORD, + SERPER_API_KEY, + SERPLY_API_KEY, + DDGS_BACKEND, + SERPSTACK_API_KEY, + SERPSTACK_HTTPS, + TAVILY_API_KEY, + TAVILY_EXTRACT_DEPTH, + BING_SEARCH_V7_ENDPOINT, + BING_SEARCH_V7_SUBSCRIPTION_KEY, + BRAVE_SEARCH_API_KEY, + EXA_API_KEY, + PERPLEXITY_API_KEY, + PERPLEXITY_MODEL, + PERPLEXITY_SEARCH_CONTEXT_USAGE, + PERPLEXITY_SEARCH_API_URL, + SOUGOU_API_SID, + SOUGOU_API_SK, + KAGI_SEARCH_API_KEY, + MOJEEK_SEARCH_API_KEY, + BOCHA_SEARCH_API_KEY, + GOOGLE_PSE_API_KEY, + GOOGLE_PSE_ENGINE_ID, + GOOGLE_DRIVE_CLIENT_ID, + GOOGLE_DRIVE_API_KEY, + ENABLE_ONEDRIVE_INTEGRATION, + ONEDRIVE_CLIENT_ID_PERSONAL, + ONEDRIVE_CLIENT_ID_BUSINESS, + ONEDRIVE_SHAREPOINT_URL, + ONEDRIVE_SHAREPOINT_TENANT_ID, + ENABLE_ONEDRIVE_PERSONAL, + ENABLE_ONEDRIVE_BUSINESS, + ENABLE_RAG_HYBRID_SEARCH, + ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS, + ENABLE_RAG_LOCAL_WEB_FETCH, + ENABLE_WEB_LOADER_SSL_VERIFICATION, + ENABLE_GOOGLE_DRIVE_INTEGRATION, + UPLOAD_DIR, + EXTERNAL_WEB_SEARCH_URL, + EXTERNAL_WEB_SEARCH_API_KEY, + EXTERNAL_WEB_LOADER_URL, + EXTERNAL_WEB_LOADER_API_KEY, + YANDEX_WEB_SEARCH_URL, + YANDEX_WEB_SEARCH_API_KEY, + YANDEX_WEB_SEARCH_CONFIG, + YOUCOM_API_KEY, + # WebUI + WEBUI_AUTH, + WEBUI_NAME, + WEBUI_BANNERS, + WEBHOOK_URL, + ADMIN_EMAIL, + SHOW_ADMIN_DETAILS, + JWT_EXPIRES_IN, + ENABLE_SIGNUP, + ENABLE_LOGIN_FORM, + ENABLE_PASSWORD_CHANGE_FORM, + ENABLE_API_KEYS, + ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS, + API_KEYS_ALLOWED_ENDPOINTS, + ENABLE_FOLDERS, + FOLDER_MAX_FILE_COUNT, + ENABLE_AUTOMATIONS, + AUTOMATION_MAX_COUNT, + AUTOMATION_MIN_INTERVAL, + ENABLE_CHANNELS, + ENABLE_CALENDAR, + ENABLE_NOTES, + ENABLE_USER_STATUS, + ENABLE_COMMUNITY_SHARING, + ENABLE_MESSAGE_RATING, + ENABLE_USER_WEBHOOKS, + ENABLE_EVALUATION_ARENA_MODELS, + BYPASS_ADMIN_ACCESS_CONTROL, + USER_PERMISSIONS, + DEFAULT_USER_ROLE, + DEFAULT_GROUP_ID, + PENDING_USER_OVERLAY_CONTENT, + PENDING_USER_OVERLAY_TITLE, + DEFAULT_PROMPT_SUGGESTIONS, + DEFAULT_MODELS, + DEFAULT_PINNED_MODELS, + DEFAULT_ARENA_MODEL, + MODEL_ORDER_LIST, + DEFAULT_MODEL_METADATA, + DEFAULT_MODEL_PARAMS, + EVALUATION_ARENA_MODELS, + # WebUI (OAuth) + ENABLE_OAUTH_ROLE_MANAGEMENT, + OAUTH_SUB_CLAIM, + OAUTH_ROLES_CLAIM, + OAUTH_EMAIL_CLAIM, + OAUTH_PICTURE_CLAIM, + OAUTH_USERNAME_CLAIM, + OAUTH_ALLOWED_ROLES, + OAUTH_ADMIN_ROLES, + # WebUI (LDAP) + ENABLE_LDAP, + LDAP_SERVER_LABEL, + LDAP_SERVER_HOST, + LDAP_SERVER_PORT, + LDAP_ATTRIBUTE_FOR_MAIL, + LDAP_ATTRIBUTE_FOR_USERNAME, + LDAP_SEARCH_FILTERS, + LDAP_SEARCH_BASE, + LDAP_APP_DN, + LDAP_APP_PASSWORD, + LDAP_USE_TLS, + LDAP_CA_CERT_FILE, + LDAP_VALIDATE_CERT, + LDAP_CIPHERS, + # LDAP Group Management + ENABLE_LDAP_GROUP_MANAGEMENT, + ENABLE_LDAP_GROUP_CREATION, + LDAP_ATTRIBUTE_FOR_GROUPS, + # Misc + ENV, + CACHE_DIR, + STATIC_DIR, + FRONTEND_BUILD_DIR, + CORS_ALLOW_ORIGIN, + DEFAULT_LOCALE, + OAUTH_PROVIDERS, + WEBUI_URL, + RESPONSE_WATERMARK, + # Admin + ENABLE_ADMIN_CHAT_ACCESS, + ENABLE_ADMIN_ANALYTICS, + BYPASS_ADMIN_ACCESS_CONTROL, + ENABLE_ADMIN_EXPORT, + # Tasks + TASK_MODEL, + TASK_MODEL_EXTERNAL, + ENABLE_TAGS_GENERATION, + ENABLE_TITLE_GENERATION, + ENABLE_FOLLOW_UP_GENERATION, + ENABLE_SEARCH_QUERY_GENERATION, + ENABLE_RETRIEVAL_QUERY_GENERATION, + ENABLE_AUTOCOMPLETE_GENERATION, + TITLE_GENERATION_PROMPT_TEMPLATE, + FOLLOW_UP_GENERATION_PROMPT_TEMPLATE, + TAGS_GENERATION_PROMPT_TEMPLATE, + IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE, + TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, + VOICE_MODE_PROMPT_TEMPLATE, + QUERY_GENERATION_PROMPT_TEMPLATE, + AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE, + AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH, + AppConfig, + reset_config, + async_reset_config, +) +from open_webui.env import ( + ENABLE_CUSTOM_MODEL_FALLBACK, + LICENSE_KEY, + AUDIT_EXCLUDED_PATHS, + AUDIT_INCLUDED_PATHS, + ENABLE_AUDIT_GET_REQUESTS, + AUDIT_LOG_LEVEL, + CHANGELOG, + REDIS_URL, + REDIS_CLUSTER, + REDIS_KEY_PREFIX, + REDIS_SENTINEL_HOSTS, + REDIS_SENTINEL_PORT, + GLOBAL_LOG_LEVEL, + MAX_BODY_LOG_SIZE, + SAFE_MODE, + VERSION, + DEPLOYMENT_ID, + INSTANCE_ID, + WEBUI_BUILD_HASH, + WEBUI_SECRET_KEY, + WEBUI_SESSION_COOKIE_SAME_SITE, + WEBUI_SESSION_COOKIE_SECURE, + ENABLE_SIGNUP_PASSWORD_CONFIRMATION, + WEBUI_AUTH_TRUSTED_EMAIL_HEADER, + WEBUI_AUTH_TRUSTED_NAME_HEADER, + WEBUI_AUTH_SIGNOUT_REDIRECT_URL, + # SCIM + ENABLE_SCIM, + SCIM_TOKEN, + ENABLE_COMPRESSION_MIDDLEWARE, + ENABLE_WEBSOCKET_SUPPORT, + BYPASS_MODEL_ACCESS_CONTROL, + RESET_CONFIG_ON_START, + ENABLE_VERSION_UPDATE_CHECK, + ENABLE_OTEL, + EXTERNAL_PWA_MANIFEST_URL, + AIOHTTP_CLIENT_SESSION_SSL, + ENABLE_STAR_SESSIONS_MIDDLEWARE, + ENABLE_PUBLIC_ACTIVE_USERS_COUNT, + # Admin Account Runtime Creation + WEBUI_ADMIN_EMAIL, + WEBUI_ADMIN_PASSWORD, + WEBUI_ADMIN_NAME, + ENABLE_EASTER_EGGS, + LOG_FORMAT, + # OAuth Back-Channel Logout + ENABLE_OAUTH_BACKCHANNEL_LOGOUT, +) + + +from open_webui.utils.models import ( + get_all_models, + get_all_base_models, + check_model_access, + get_filtered_models, +) +from open_webui.utils.chat import ( + generate_chat_completion as chat_completion_handler, + chat_completed as chat_completed_handler, +) +from open_webui.utils.actions import chat_action as chat_action_handler +from open_webui.utils.embeddings import generate_embeddings +from open_webui.utils.middleware import ( + build_chat_response_context, + process_chat_payload, + process_chat_response, +) +from open_webui.utils.tools import set_tool_servers, set_terminal_servers + +from open_webui.utils.auth import ( + get_license_data, + get_http_authorization_cred, + decode_token, + get_admin_user, + get_verified_user, + create_admin_user, +) +from open_webui.utils.plugin import install_tool_and_function_dependencies +from open_webui.utils.oauth import ( + get_oauth_client_info_with_dynamic_client_registration, + get_oauth_client_info_with_static_credentials, + encrypt_data, + decrypt_data, + resolve_oauth_client_info, + OAuthManager, + OAuthClientManager, + OAuthClientInformationFull, +) +from open_webui.utils.security_headers import SecurityHeadersMiddleware +from open_webui.utils.redis import get_redis_connection + +from open_webui.tasks import ( + redis_task_command_listener, + list_task_ids_by_item_id, + create_task, + stop_task, + stop_item_tasks, + list_tasks, +) # Import from tasks.py + +from open_webui.utils.redis import get_sentinels_from_env + + +from open_webui.constants import ERROR_MESSAGES, TASKS + +if SAFE_MODE: + print('SAFE MODE ENABLED') + # Functions.deactivate_all_functions() is awaited in lifespan below + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) +log = logging.getLogger(__name__) + + +class SPAStaticFiles(StaticFiles): + async def get_response(self, path: str, scope): + try: + return await super().get_response(path, scope) + except (HTTPException, StarletteHTTPException) as ex: + if ex.status_code == 404: + if path.endswith('.js'): + # Return 404 for javascript files + raise ex + else: + return await super().get_response('index.html', scope) + else: + raise ex + + +if LOG_FORMAT != 'json': + print(rf""" + ██████╗ ██████╗ ███████╗███╗ ██╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗██╗ +██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██║ ██║██╔════╝██╔══██╗██║ ██║██║ +██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ █╗ ██║█████╗ ██████╔╝██║ ██║██║ +██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║███╗██║██╔══╝ ██╔══██╗██║ ██║██║ +╚██████╔╝██║ ███████╗██║ ╚████║ ╚███╔███╔╝███████╗██████╔╝╚██████╔╝██║ + ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚══╝╚══╝ ╚══════╝╚═════╝ ╚═════╝ ╚═╝ + + +v{VERSION} - building the best AI user interface. +{f'Commit: {WEBUI_BUILD_HASH}' if WEBUI_BUILD_HASH != 'dev-build' else ''} +https://github.com/open-webui/open-webui +""") + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Store reference to main event loop for sync->async calls (e.g., embedding generation) + # This allows sync functions to schedule work on the main loop without blocking health checks + app.state.main_loop = asyncio.get_running_loop() + + app.state.instance_id = INSTANCE_ID + start_logger() + + if RESET_CONFIG_ON_START: + await async_reset_config() + + if LICENSE_KEY: + get_license_data(app, LICENSE_KEY) + + # Create admin account from env vars if specified and no users exist + if WEBUI_ADMIN_EMAIL and WEBUI_ADMIN_PASSWORD: + if await create_admin_user(WEBUI_ADMIN_EMAIL, WEBUI_ADMIN_PASSWORD, WEBUI_ADMIN_NAME): + # Disable signup since we now have an admin + app.state.config.ENABLE_SIGNUP = False + + if SAFE_MODE: + await Functions.deactivate_all_functions() + + # This should be blocking (sync) so functions are not deactivated on first /get_models calls + # when the first user lands on the / route. + log.info('Installing external dependencies of functions and tools...') + await install_tool_and_function_dependencies() + + app.state.redis = get_redis_connection( + redis_url=REDIS_URL, + redis_sentinels=get_sentinels_from_env(REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT), + redis_cluster=REDIS_CLUSTER, + async_mode=True, + ) + + if app.state.redis is not None: + app.state.redis_task_command_listener = asyncio.create_task(redis_task_command_listener(app)) + + if THREAD_POOL_SIZE and THREAD_POOL_SIZE > 0: + limiter = anyio.to_thread.current_default_thread_limiter() + limiter.total_tokens = THREAD_POOL_SIZE + + asyncio.create_task(periodic_usage_pool_cleanup()) + asyncio.create_task(periodic_session_pool_cleanup()) + + from open_webui.utils.automations import scheduler_worker_loop + + asyncio.create_task(scheduler_worker_loop(app)) + + if app.state.config.ENABLE_BASE_MODELS_CACHE: + try: + await get_all_models( + Request( + # Creating a mock request object to pass to get_all_models + { + 'type': 'http', + 'asgi.version': '3.0', + 'asgi.spec_version': '2.0', + 'method': 'GET', + 'path': '/internal', + 'query_string': b'', + 'headers': Headers({}).raw, + 'client': ('127.0.0.1', 12345), + 'server': ('127.0.0.1', 80), + 'scheme': 'http', + 'app': app, + } + ), + None, + ) + except Exception as e: + log.warning(f'Failed to pre-fetch models at startup: {e}') + + # Pre-fetch tool server specs so the first request doesn't pay the latency cost + if len(app.state.config.TOOL_SERVER_CONNECTIONS) > 0: + log.info('Initializing tool servers...') + try: + mock_request = Request( + { + 'type': 'http', + 'asgi.version': '3.0', + 'asgi.spec_version': '2.0', + 'method': 'GET', + 'path': '/internal', + 'query_string': b'', + 'headers': Headers({}).raw, + 'client': ('127.0.0.1', 12345), + 'server': ('127.0.0.1', 80), + 'scheme': 'http', + 'app': app, + } + ) + await set_tool_servers(mock_request) + log.info(f'Initialized {len(app.state.TOOL_SERVERS)} tool server(s)') + + await set_terminal_servers(mock_request) + log.info(f'Initialized {len(app.state.TERMINAL_SERVERS)} terminal server(s)') + except Exception as e: + log.warning(f'Failed to initialize tool/terminal servers at startup: {e}') + + # Mark application as ready to accept traffic from a startup perspective. + app.state.startup_complete = True + + yield + + # Shutdown: clean up shared resources + from open_webui.utils.session_pool import close_session + + await close_session() + + if hasattr(app.state, 'redis_task_command_listener'): + app.state.redis_task_command_listener.cancel() + + +app = FastAPI( + title='Open WebUI', + docs_url='/docs' if ENV == 'dev' else None, + openapi_url='/openapi.json' if ENV == 'dev' else None, + redoc_url=None, + lifespan=lifespan, +) + +# Used by readiness checks to gate traffic until startup work is done. +app.state.startup_complete = False + +# For Open WebUI OIDC/OAuth2 +oauth_manager = OAuthManager(app) +app.state.oauth_manager = oauth_manager + +# For Integrations +oauth_client_manager = OAuthClientManager(app) +app.state.oauth_client_manager = oauth_client_manager + +app.state.instance_id = None +app.state.config = AppConfig( + redis_url=REDIS_URL, + redis_sentinels=get_sentinels_from_env(REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT), + redis_cluster=REDIS_CLUSTER, + redis_key_prefix=REDIS_KEY_PREFIX, +) +app.state.redis = None + +app.state.WEBUI_NAME = WEBUI_NAME +app.state.LICENSE_METADATA = None + + +######################################## +# +# OPENTELEMETRY +# +######################################## + +if ENABLE_OTEL: + from open_webui.utils.telemetry.setup import setup as setup_opentelemetry + + setup_opentelemetry(app=app, db_engine=engine) + + +######################################## +# +# OLLAMA +# +######################################## + + +app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API +app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS +app.state.config.OLLAMA_API_CONFIGS = OLLAMA_API_CONFIGS + +app.state.OLLAMA_MODELS = {} + +######################################## +# +# OPENAI +# +######################################## + +app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API +app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS +app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS +app.state.config.OPENAI_API_CONFIGS = OPENAI_API_CONFIGS + +app.state.OPENAI_MODELS = {} + +######################################## +# +# TOOL SERVERS +# +######################################## + +app.state.config.TOOL_SERVER_CONNECTIONS = TOOL_SERVER_CONNECTIONS +app.state.TOOL_SERVERS = [] + +######################################## +# +# TERMINAL SERVER +# +######################################## + +app.state.config.TERMINAL_SERVER_CONNECTIONS = TERMINAL_SERVER_CONNECTIONS +app.state.TERMINAL_SERVERS = [] + +######################################## +# +# DIRECT CONNECTIONS +# +######################################## + +app.state.config.ENABLE_DIRECT_CONNECTIONS = ENABLE_DIRECT_CONNECTIONS + +######################################## +# +# SCIM +# +######################################## + +app.state.ENABLE_SCIM = ENABLE_SCIM +app.state.SCIM_TOKEN = SCIM_TOKEN + +######################################## +# +# MODELS +# +######################################## + +app.state.config.ENABLE_BASE_MODELS_CACHE = ENABLE_BASE_MODELS_CACHE +app.state.BASE_MODELS = [] + +######################################## +# +# WEBUI +# +######################################## + +app.state.config.WEBUI_URL = WEBUI_URL +app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP +app.state.config.ENABLE_LOGIN_FORM = ENABLE_LOGIN_FORM +app.state.config.ENABLE_PASSWORD_CHANGE_FORM = ENABLE_PASSWORD_CHANGE_FORM + +app.state.config.ENABLE_API_KEYS = ENABLE_API_KEYS +app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS = ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS +app.state.config.API_KEYS_ALLOWED_ENDPOINTS = API_KEYS_ALLOWED_ENDPOINTS + +app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN + +app.state.config.SHOW_ADMIN_DETAILS = SHOW_ADMIN_DETAILS +app.state.config.ADMIN_EMAIL = ADMIN_EMAIL + + +app.state.config.DEFAULT_MODELS = DEFAULT_MODELS +app.state.config.DEFAULT_PINNED_MODELS = DEFAULT_PINNED_MODELS +app.state.config.MODEL_ORDER_LIST = MODEL_ORDER_LIST +app.state.config.DEFAULT_MODEL_METADATA = DEFAULT_MODEL_METADATA +app.state.config.DEFAULT_MODEL_PARAMS = DEFAULT_MODEL_PARAMS + + +app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS +app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE +app.state.config.DEFAULT_GROUP_ID = DEFAULT_GROUP_ID + +app.state.config.PENDING_USER_OVERLAY_CONTENT = PENDING_USER_OVERLAY_CONTENT +app.state.config.PENDING_USER_OVERLAY_TITLE = PENDING_USER_OVERLAY_TITLE + +app.state.config.RESPONSE_WATERMARK = RESPONSE_WATERMARK + +app.state.config.USER_PERMISSIONS = USER_PERMISSIONS +app.state.config.WEBHOOK_URL = WEBHOOK_URL +app.state.config.BANNERS = WEBUI_BANNERS + + +app.state.config.ENABLE_FOLDERS = ENABLE_FOLDERS +app.state.config.FOLDER_MAX_FILE_COUNT = FOLDER_MAX_FILE_COUNT +app.state.config.ENABLE_AUTOMATIONS = ENABLE_AUTOMATIONS +app.state.config.AUTOMATION_MAX_COUNT = AUTOMATION_MAX_COUNT +app.state.config.AUTOMATION_MIN_INTERVAL = AUTOMATION_MIN_INTERVAL +app.state.config.ENABLE_CHANNELS = ENABLE_CHANNELS +app.state.config.ENABLE_CALENDAR = ENABLE_CALENDAR +app.state.config.ENABLE_NOTES = ENABLE_NOTES +app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING +app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING +app.state.config.ENABLE_USER_WEBHOOKS = ENABLE_USER_WEBHOOKS +app.state.config.ENABLE_USER_STATUS = ENABLE_USER_STATUS + +app.state.config.ENABLE_EVALUATION_ARENA_MODELS = ENABLE_EVALUATION_ARENA_MODELS +app.state.config.EVALUATION_ARENA_MODELS = EVALUATION_ARENA_MODELS + +# Migrate legacy access_control → access_grants on boot +from open_webui.utils.access_control import migrate_access_control + +connections = app.state.config.TOOL_SERVER_CONNECTIONS +if any('access_control' in c.get('config', {}) for c in connections): + for connection in connections: + migrate_access_control(connection.get('config', {})) + app.state.config.TOOL_SERVER_CONNECTIONS = connections + +arena_models = app.state.config.EVALUATION_ARENA_MODELS +if any('access_control' in m.get('meta', {}) for m in arena_models): + for model in arena_models: + migrate_access_control(model.get('meta', {})) + app.state.config.EVALUATION_ARENA_MODELS = arena_models + +app.state.config.OAUTH_SUB_CLAIM = OAUTH_SUB_CLAIM +app.state.config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM +app.state.config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM +app.state.config.OAUTH_EMAIL_CLAIM = OAUTH_EMAIL_CLAIM + +app.state.config.ENABLE_OAUTH_ROLE_MANAGEMENT = ENABLE_OAUTH_ROLE_MANAGEMENT +app.state.config.OAUTH_ROLES_CLAIM = OAUTH_ROLES_CLAIM +app.state.config.OAUTH_ALLOWED_ROLES = OAUTH_ALLOWED_ROLES +app.state.config.OAUTH_ADMIN_ROLES = OAUTH_ADMIN_ROLES + +app.state.config.ENABLE_LDAP = ENABLE_LDAP +app.state.config.LDAP_SERVER_LABEL = LDAP_SERVER_LABEL +app.state.config.LDAP_SERVER_HOST = LDAP_SERVER_HOST +app.state.config.LDAP_SERVER_PORT = LDAP_SERVER_PORT +app.state.config.LDAP_ATTRIBUTE_FOR_MAIL = LDAP_ATTRIBUTE_FOR_MAIL +app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = LDAP_ATTRIBUTE_FOR_USERNAME +app.state.config.LDAP_APP_DN = LDAP_APP_DN +app.state.config.LDAP_APP_PASSWORD = LDAP_APP_PASSWORD +app.state.config.LDAP_SEARCH_BASE = LDAP_SEARCH_BASE +app.state.config.LDAP_SEARCH_FILTERS = LDAP_SEARCH_FILTERS +app.state.config.LDAP_USE_TLS = LDAP_USE_TLS +app.state.config.LDAP_CA_CERT_FILE = LDAP_CA_CERT_FILE +app.state.config.LDAP_VALIDATE_CERT = LDAP_VALIDATE_CERT +app.state.config.LDAP_CIPHERS = LDAP_CIPHERS + +# For LDAP Group Management +app.state.config.ENABLE_LDAP_GROUP_MANAGEMENT = ENABLE_LDAP_GROUP_MANAGEMENT +app.state.config.ENABLE_LDAP_GROUP_CREATION = ENABLE_LDAP_GROUP_CREATION +app.state.config.LDAP_ATTRIBUTE_FOR_GROUPS = LDAP_ATTRIBUTE_FOR_GROUPS + + +app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER +app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER +app.state.WEBUI_AUTH_SIGNOUT_REDIRECT_URL = WEBUI_AUTH_SIGNOUT_REDIRECT_URL +app.state.EXTERNAL_PWA_MANIFEST_URL = EXTERNAL_PWA_MANIFEST_URL + +app.state.USER_COUNT = None + +app.state.TOOLS = {} +app.state.TOOL_CONTENTS = {} + +app.state.FUNCTIONS = {} +app.state.FUNCTION_CONTENTS = {} + +######################################## +# +# RETRIEVAL +# +######################################## + + +app.state.config.TOP_K = RAG_TOP_K +app.state.config.TOP_K_RERANKER = RAG_TOP_K_RERANKER +app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD +app.state.config.HYBRID_BM25_WEIGHT = RAG_HYBRID_BM25_WEIGHT + + +app.state.config.ALLOWED_FILE_EXTENSIONS = RAG_ALLOWED_FILE_EXTENSIONS +app.state.config.FILE_MAX_SIZE = RAG_FILE_MAX_SIZE +app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT +app.state.config.FILE_IMAGE_COMPRESSION_WIDTH = FILE_IMAGE_COMPRESSION_WIDTH +app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT = FILE_IMAGE_COMPRESSION_HEIGHT + + +app.state.config.RAG_FULL_CONTEXT = RAG_FULL_CONTEXT +app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL = BYPASS_EMBEDDING_AND_RETRIEVAL +app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH +app.state.config.ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS = ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS +app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION = ENABLE_WEB_LOADER_SSL_VERIFICATION + +app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE +app.state.config.DATALAB_MARKER_API_KEY = DATALAB_MARKER_API_KEY +app.state.config.DATALAB_MARKER_API_BASE_URL = DATALAB_MARKER_API_BASE_URL +app.state.config.DATALAB_MARKER_ADDITIONAL_CONFIG = DATALAB_MARKER_ADDITIONAL_CONFIG +app.state.config.DATALAB_MARKER_SKIP_CACHE = DATALAB_MARKER_SKIP_CACHE +app.state.config.DATALAB_MARKER_FORCE_OCR = DATALAB_MARKER_FORCE_OCR +app.state.config.DATALAB_MARKER_PAGINATE = DATALAB_MARKER_PAGINATE +app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR = DATALAB_MARKER_STRIP_EXISTING_OCR +app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION = DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION +app.state.config.DATALAB_MARKER_FORMAT_LINES = DATALAB_MARKER_FORMAT_LINES +app.state.config.DATALAB_MARKER_USE_LLM = DATALAB_MARKER_USE_LLM +app.state.config.DATALAB_MARKER_OUTPUT_FORMAT = DATALAB_MARKER_OUTPUT_FORMAT +app.state.config.EXTERNAL_DOCUMENT_LOADER_URL = EXTERNAL_DOCUMENT_LOADER_URL +app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY = EXTERNAL_DOCUMENT_LOADER_API_KEY +app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL +app.state.config.DOCLING_SERVER_URL = DOCLING_SERVER_URL +app.state.config.DOCLING_API_KEY = DOCLING_API_KEY +app.state.config.DOCLING_PARAMS = DOCLING_PARAMS +app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT +app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY +app.state.config.DOCUMENT_INTELLIGENCE_MODEL = DOCUMENT_INTELLIGENCE_MODEL +app.state.config.MISTRAL_OCR_API_BASE_URL = MISTRAL_OCR_API_BASE_URL +app.state.config.MISTRAL_OCR_API_KEY = MISTRAL_OCR_API_KEY +app.state.config.PADDLEOCR_VL_BASE_URL = PADDLEOCR_VL_BASE_URL +app.state.config.PADDLEOCR_VL_TOKEN = PADDLEOCR_VL_TOKEN +app.state.config.MINERU_API_MODE = MINERU_API_MODE +app.state.config.MINERU_API_URL = MINERU_API_URL +app.state.config.MINERU_API_KEY = MINERU_API_KEY +app.state.config.MINERU_API_TIMEOUT = MINERU_API_TIMEOUT +app.state.config.MINERU_PARAMS = MINERU_PARAMS + +app.state.config.TEXT_SPLITTER = RAG_TEXT_SPLITTER +app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER = ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER + +app.state.config.TIKTOKEN_ENCODING_NAME = TIKTOKEN_ENCODING_NAME + +app.state.config.CHUNK_SIZE = CHUNK_SIZE +app.state.config.CHUNK_MIN_SIZE_TARGET = CHUNK_MIN_SIZE_TARGET +app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP + + +app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE +app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL +app.state.config.RAG_EMBEDDING_BATCH_SIZE = RAG_EMBEDDING_BATCH_SIZE +app.state.config.ENABLE_ASYNC_EMBEDDING = ENABLE_ASYNC_EMBEDDING +app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS = RAG_EMBEDDING_CONCURRENT_REQUESTS + +app.state.config.RAG_RERANKING_ENGINE = RAG_RERANKING_ENGINE +app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL +app.state.config.RAG_EXTERNAL_RERANKER_URL = RAG_EXTERNAL_RERANKER_URL +app.state.config.RAG_EXTERNAL_RERANKER_API_KEY = RAG_EXTERNAL_RERANKER_API_KEY +app.state.config.RAG_EXTERNAL_RERANKER_TIMEOUT = RAG_EXTERNAL_RERANKER_TIMEOUT +app.state.config.RAG_RERANKING_BATCH_SIZE = RAG_RERANKING_BATCH_SIZE + +app.state.config.RAG_TEMPLATE = RAG_TEMPLATE + +app.state.config.RAG_OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL +app.state.config.RAG_OPENAI_API_KEY = RAG_OPENAI_API_KEY + +app.state.config.RAG_AZURE_OPENAI_BASE_URL = RAG_AZURE_OPENAI_BASE_URL +app.state.config.RAG_AZURE_OPENAI_API_KEY = RAG_AZURE_OPENAI_API_KEY +app.state.config.RAG_AZURE_OPENAI_API_VERSION = RAG_AZURE_OPENAI_API_VERSION + +app.state.config.RAG_OLLAMA_BASE_URL = RAG_OLLAMA_BASE_URL +app.state.config.RAG_OLLAMA_API_KEY = RAG_OLLAMA_API_KEY + +app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES +app.state.config.PDF_LOADER_MODE = PDF_LOADER_MODE + +app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE +app.state.config.YOUTUBE_LOADER_PROXY_URL = YOUTUBE_LOADER_PROXY_URL + + +app.state.config.ENABLE_WEB_SEARCH = ENABLE_WEB_SEARCH +app.state.config.WEB_SEARCH_ENGINE = WEB_SEARCH_ENGINE +app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST = WEB_SEARCH_DOMAIN_FILTER_LIST +app.state.config.WEB_SEARCH_RESULT_COUNT = WEB_SEARCH_RESULT_COUNT +app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS = WEB_SEARCH_CONCURRENT_REQUESTS +app.state.config.WEB_FETCH_MAX_CONTENT_LENGTH = WEB_FETCH_MAX_CONTENT_LENGTH + +app.state.config.WEB_LOADER_ENGINE = WEB_LOADER_ENGINE +app.state.config.WEB_LOADER_CONCURRENT_REQUESTS = WEB_LOADER_CONCURRENT_REQUESTS +app.state.config.WEB_LOADER_TIMEOUT = WEB_LOADER_TIMEOUT + +app.state.config.WEB_SEARCH_TRUST_ENV = WEB_SEARCH_TRUST_ENV +app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL +app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER = BYPASS_WEB_SEARCH_WEB_LOADER + +app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION +app.state.config.ENABLE_ONEDRIVE_INTEGRATION = ENABLE_ONEDRIVE_INTEGRATION + +app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY = OLLAMA_CLOUD_WEB_SEARCH_API_KEY +app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL +app.state.config.SEARXNG_LANGUAGE = SEARXNG_LANGUAGE +app.state.config.YACY_QUERY_URL = YACY_QUERY_URL +app.state.config.YACY_USERNAME = YACY_USERNAME +app.state.config.YACY_PASSWORD = YACY_PASSWORD +app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY +app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID +app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY +app.state.config.KAGI_SEARCH_API_KEY = KAGI_SEARCH_API_KEY +app.state.config.MOJEEK_SEARCH_API_KEY = MOJEEK_SEARCH_API_KEY +app.state.config.BOCHA_SEARCH_API_KEY = BOCHA_SEARCH_API_KEY +app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY +app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS +app.state.config.SERPER_API_KEY = SERPER_API_KEY +app.state.config.SERPLY_API_KEY = SERPLY_API_KEY +app.state.config.DDGS_BACKEND = DDGS_BACKEND +app.state.config.TAVILY_API_KEY = TAVILY_API_KEY +app.state.config.SEARCHAPI_API_KEY = SEARCHAPI_API_KEY +app.state.config.SEARCHAPI_ENGINE = SEARCHAPI_ENGINE +app.state.config.SERPAPI_API_KEY = SERPAPI_API_KEY +app.state.config.SERPAPI_ENGINE = SERPAPI_ENGINE +app.state.config.JINA_API_KEY = JINA_API_KEY +app.state.config.JINA_API_BASE_URL = JINA_API_BASE_URL +app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT +app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY +app.state.config.EXA_API_KEY = EXA_API_KEY +app.state.config.PERPLEXITY_API_KEY = PERPLEXITY_API_KEY +app.state.config.PERPLEXITY_MODEL = PERPLEXITY_MODEL +app.state.config.PERPLEXITY_SEARCH_CONTEXT_USAGE = PERPLEXITY_SEARCH_CONTEXT_USAGE +app.state.config.PERPLEXITY_SEARCH_API_URL = PERPLEXITY_SEARCH_API_URL +app.state.config.SOUGOU_API_SID = SOUGOU_API_SID +app.state.config.SOUGOU_API_SK = SOUGOU_API_SK +app.state.config.EXTERNAL_WEB_SEARCH_URL = EXTERNAL_WEB_SEARCH_URL +app.state.config.EXTERNAL_WEB_SEARCH_API_KEY = EXTERNAL_WEB_SEARCH_API_KEY +app.state.config.EXTERNAL_WEB_LOADER_URL = EXTERNAL_WEB_LOADER_URL +app.state.config.EXTERNAL_WEB_LOADER_API_KEY = EXTERNAL_WEB_LOADER_API_KEY +app.state.config.YANDEX_WEB_SEARCH_URL = YANDEX_WEB_SEARCH_URL +app.state.config.YANDEX_WEB_SEARCH_API_KEY = YANDEX_WEB_SEARCH_API_KEY +app.state.config.YANDEX_WEB_SEARCH_CONFIG = YANDEX_WEB_SEARCH_CONFIG +app.state.config.YOUCOM_API_KEY = YOUCOM_API_KEY + + +app.state.config.PLAYWRIGHT_WS_URL = PLAYWRIGHT_WS_URL +app.state.config.PLAYWRIGHT_TIMEOUT = PLAYWRIGHT_TIMEOUT +app.state.config.FIRECRAWL_API_BASE_URL = FIRECRAWL_API_BASE_URL +app.state.config.FIRECRAWL_API_KEY = FIRECRAWL_API_KEY +app.state.config.FIRECRAWL_TIMEOUT = FIRECRAWL_TIMEOUT +app.state.config.TAVILY_EXTRACT_DEPTH = TAVILY_EXTRACT_DEPTH + +app.state.EMBEDDING_FUNCTION = None +app.state.RERANKING_FUNCTION = None +app.state.ef = None +app.state.rf = None + +app.state.YOUTUBE_LOADER_TRANSLATION = None + + +try: + app.state.ef = get_ef(app.state.config.RAG_EMBEDDING_ENGINE, app.state.config.RAG_EMBEDDING_MODEL) + if app.state.config.ENABLE_RAG_HYBRID_SEARCH and not app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL: + app.state.rf = get_rf( + app.state.config.RAG_RERANKING_ENGINE, + app.state.config.RAG_RERANKING_MODEL, + app.state.config.RAG_EXTERNAL_RERANKER_URL, + app.state.config.RAG_EXTERNAL_RERANKER_API_KEY, + app.state.config.RAG_EXTERNAL_RERANKER_TIMEOUT, + ) + else: + app.state.rf = None +except Exception as e: + log.error(f'Error updating models: {e}') + pass + + +app.state.EMBEDDING_FUNCTION = get_embedding_function( + app.state.config.RAG_EMBEDDING_ENGINE, + app.state.config.RAG_EMBEDDING_MODEL, + embedding_function=app.state.ef, + url=( + app.state.config.RAG_OPENAI_API_BASE_URL + if app.state.config.RAG_EMBEDDING_ENGINE == 'openai' + else ( + app.state.config.RAG_OLLAMA_BASE_URL + if app.state.config.RAG_EMBEDDING_ENGINE == 'ollama' + else app.state.config.RAG_AZURE_OPENAI_BASE_URL + ) + ), + key=( + app.state.config.RAG_OPENAI_API_KEY + if app.state.config.RAG_EMBEDDING_ENGINE == 'openai' + else ( + app.state.config.RAG_OLLAMA_API_KEY + if app.state.config.RAG_EMBEDDING_ENGINE == 'ollama' + else app.state.config.RAG_AZURE_OPENAI_API_KEY + ) + ), + embedding_batch_size=app.state.config.RAG_EMBEDDING_BATCH_SIZE, + azure_api_version=( + app.state.config.RAG_AZURE_OPENAI_API_VERSION + if app.state.config.RAG_EMBEDDING_ENGINE == 'azure_openai' + else None + ), + enable_async=app.state.config.ENABLE_ASYNC_EMBEDDING, + concurrent_requests=app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, +) + +app.state.RERANKING_FUNCTION = get_reranking_function( + app.state.config.RAG_RERANKING_ENGINE, + app.state.config.RAG_RERANKING_MODEL, + reranking_function=app.state.rf, + reranking_batch_size=app.state.config.RAG_RERANKING_BATCH_SIZE, +) + +######################################## +# +# CODE EXECUTION +# +######################################## + +app.state.config.ENABLE_CODE_EXECUTION = ENABLE_CODE_EXECUTION +app.state.config.CODE_EXECUTION_ENGINE = CODE_EXECUTION_ENGINE +app.state.config.CODE_EXECUTION_JUPYTER_URL = CODE_EXECUTION_JUPYTER_URL +app.state.config.CODE_EXECUTION_JUPYTER_AUTH = CODE_EXECUTION_JUPYTER_AUTH +app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = CODE_EXECUTION_JUPYTER_AUTH_TOKEN +app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = CODE_EXECUTION_JUPYTER_AUTH_PASSWORD +app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT = CODE_EXECUTION_JUPYTER_TIMEOUT + +app.state.config.ENABLE_CODE_INTERPRETER = ENABLE_CODE_INTERPRETER +app.state.config.CODE_INTERPRETER_ENGINE = CODE_INTERPRETER_ENGINE +app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = CODE_INTERPRETER_PROMPT_TEMPLATE + +app.state.config.CODE_INTERPRETER_JUPYTER_URL = CODE_INTERPRETER_JUPYTER_URL +app.state.config.CODE_INTERPRETER_JUPYTER_AUTH = CODE_INTERPRETER_JUPYTER_AUTH +app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = CODE_INTERPRETER_JUPYTER_AUTH_TOKEN +app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD +app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT = CODE_INTERPRETER_JUPYTER_TIMEOUT + +######################################## +# +# IMAGES +# +######################################## + +app.state.config.IMAGE_GENERATION_ENGINE = IMAGE_GENERATION_ENGINE +app.state.config.ENABLE_IMAGE_GENERATION = ENABLE_IMAGE_GENERATION +app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = ENABLE_IMAGE_PROMPT_GENERATION +app.state.config.ENABLE_MEMORIES = ENABLE_MEMORIES + +app.state.config.IMAGE_GENERATION_MODEL = IMAGE_GENERATION_MODEL +app.state.config.IMAGE_SIZE = IMAGE_SIZE +app.state.config.IMAGE_STEPS = IMAGE_STEPS + +app.state.config.IMAGES_OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL +app.state.config.IMAGES_OPENAI_API_VERSION = IMAGES_OPENAI_API_VERSION +app.state.config.IMAGES_OPENAI_API_KEY = IMAGES_OPENAI_API_KEY +app.state.config.IMAGES_OPENAI_API_PARAMS = IMAGES_OPENAI_API_PARAMS + +app.state.config.IMAGES_GEMINI_API_BASE_URL = IMAGES_GEMINI_API_BASE_URL +app.state.config.IMAGES_GEMINI_API_KEY = IMAGES_GEMINI_API_KEY +app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD = IMAGES_GEMINI_ENDPOINT_METHOD + +app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL +app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH +app.state.config.AUTOMATIC1111_PARAMS = AUTOMATIC1111_PARAMS + +app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL +app.state.config.COMFYUI_API_KEY = COMFYUI_API_KEY +app.state.config.COMFYUI_WORKFLOW = COMFYUI_WORKFLOW +app.state.config.COMFYUI_WORKFLOW_NODES = COMFYUI_WORKFLOW_NODES + + +app.state.config.ENABLE_IMAGE_EDIT = ENABLE_IMAGE_EDIT +app.state.config.IMAGE_EDIT_ENGINE = IMAGE_EDIT_ENGINE +app.state.config.IMAGE_EDIT_MODEL = IMAGE_EDIT_MODEL +app.state.config.IMAGE_EDIT_SIZE = IMAGE_EDIT_SIZE +app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL = IMAGES_EDIT_OPENAI_API_BASE_URL +app.state.config.IMAGES_EDIT_OPENAI_API_KEY = IMAGES_EDIT_OPENAI_API_KEY +app.state.config.IMAGES_EDIT_OPENAI_API_VERSION = IMAGES_EDIT_OPENAI_API_VERSION +app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL = IMAGES_EDIT_GEMINI_API_BASE_URL +app.state.config.IMAGES_EDIT_GEMINI_API_KEY = IMAGES_EDIT_GEMINI_API_KEY +app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL = IMAGES_EDIT_COMFYUI_BASE_URL +app.state.config.IMAGES_EDIT_COMFYUI_API_KEY = IMAGES_EDIT_COMFYUI_API_KEY +app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW = IMAGES_EDIT_COMFYUI_WORKFLOW +app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = IMAGES_EDIT_COMFYUI_WORKFLOW_NODES + + +######################################## +# +# AUDIO +# +######################################## + +app.state.config.STT_ENGINE = AUDIO_STT_ENGINE +app.state.config.STT_MODEL = AUDIO_STT_MODEL +app.state.config.STT_SUPPORTED_CONTENT_TYPES = AUDIO_STT_SUPPORTED_CONTENT_TYPES + +app.state.config.STT_OPENAI_API_BASE_URL = AUDIO_STT_OPENAI_API_BASE_URL +app.state.config.STT_OPENAI_API_KEY = AUDIO_STT_OPENAI_API_KEY + +app.state.config.WHISPER_MODEL = WHISPER_MODEL +app.state.config.DEEPGRAM_API_KEY = DEEPGRAM_API_KEY + +app.state.config.AUDIO_STT_AZURE_API_KEY = AUDIO_STT_AZURE_API_KEY +app.state.config.AUDIO_STT_AZURE_REGION = AUDIO_STT_AZURE_REGION +app.state.config.AUDIO_STT_AZURE_LOCALES = AUDIO_STT_AZURE_LOCALES +app.state.config.AUDIO_STT_AZURE_BASE_URL = AUDIO_STT_AZURE_BASE_URL +app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = AUDIO_STT_AZURE_MAX_SPEAKERS + +app.state.config.AUDIO_STT_MISTRAL_API_KEY = AUDIO_STT_MISTRAL_API_KEY +app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL = AUDIO_STT_MISTRAL_API_BASE_URL +app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS + +app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE + +app.state.config.TTS_MODEL = AUDIO_TTS_MODEL +app.state.config.TTS_VOICE = AUDIO_TTS_VOICE + +app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL +app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY +app.state.config.TTS_OPENAI_PARAMS = AUDIO_TTS_OPENAI_PARAMS + +app.state.config.TTS_API_KEY = AUDIO_TTS_API_KEY +app.state.config.TTS_SPLIT_ON = AUDIO_TTS_SPLIT_ON + + +app.state.config.TTS_AZURE_SPEECH_REGION = AUDIO_TTS_AZURE_SPEECH_REGION +app.state.config.TTS_AZURE_SPEECH_BASE_URL = AUDIO_TTS_AZURE_SPEECH_BASE_URL +app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT + +app.state.config.TTS_MISTRAL_API_KEY = AUDIO_TTS_MISTRAL_API_KEY +app.state.config.TTS_MISTRAL_API_BASE_URL = AUDIO_TTS_MISTRAL_API_BASE_URL + + +app.state.faster_whisper_model = None +app.state.speech_synthesiser = None +app.state.speech_speaker_embeddings_dataset = None + + +######################################## +# +# TASKS +# +######################################## + + +app.state.config.TASK_MODEL = TASK_MODEL +app.state.config.TASK_MODEL_EXTERNAL = TASK_MODEL_EXTERNAL + + +app.state.config.ENABLE_SEARCH_QUERY_GENERATION = ENABLE_SEARCH_QUERY_GENERATION +app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = ENABLE_RETRIEVAL_QUERY_GENERATION +app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = ENABLE_AUTOCOMPLETE_GENERATION +app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION +app.state.config.ENABLE_TITLE_GENERATION = ENABLE_TITLE_GENERATION +app.state.config.ENABLE_FOLLOW_UP_GENERATION = ENABLE_FOLLOW_UP_GENERATION + + +app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE +app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = TAGS_GENERATION_PROMPT_TEMPLATE +app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE +app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = FOLLOW_UP_GENERATION_PROMPT_TEMPLATE + +app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE +app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE = QUERY_GENERATION_PROMPT_TEMPLATE +app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE +app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH +app.state.config.VOICE_MODE_PROMPT_TEMPLATE = VOICE_MODE_PROMPT_TEMPLATE + + +######################################## +# +# WEBUI +# +######################################## + +app.state.MODELS = MODELS + +# Add the middleware to the app +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. +app.add_middleware(RedirectMiddleware) +app.add_middleware(SecurityHeadersMiddleware) +app.add_middleware(CommitSessionMiddleware) +app.add_middleware(AuthTokenMiddleware, fastapi_app=app) +app.add_middleware(WebsocketUpgradeGuardMiddleware) + + +app.add_middleware( + CORSMiddleware, + allow_origins=CORS_ALLOW_ORIGIN, + allow_credentials=True, + allow_methods=['*'], + allow_headers=['*'], +) + + +app.mount('/ws', socket_app) + + +app.include_router(ollama.router, prefix='/ollama', tags=['ollama']) +app.include_router(openai.router, prefix='/openai', tags=['openai']) + + +app.include_router(pipelines.router, prefix='/api/v1/pipelines', tags=['pipelines']) +app.include_router(tasks.router, prefix='/api/v1/tasks', tags=['tasks']) +app.include_router(images.router, prefix='/api/v1/images', tags=['images']) + +app.include_router(audio.router, prefix='/api/v1/audio', tags=['audio']) +app.include_router(retrieval.router, prefix='/api/v1/retrieval', tags=['retrieval']) + +app.include_router(configs.router, prefix='/api/v1/configs', tags=['configs']) + +app.include_router(auths.router, prefix='/api/v1/auths', tags=['auths']) +app.include_router(users.router, prefix='/api/v1/users', tags=['users']) + + +app.include_router(channels.router, prefix='/api/v1/channels', tags=['channels']) +app.include_router(chats.router, prefix='/api/v1/chats', tags=['chats']) +app.include_router(notes.router, prefix='/api/v1/notes', tags=['notes']) + + +app.include_router(models.router, prefix='/api/v1/models', tags=['models']) +app.include_router(knowledge.router, prefix='/api/v1/knowledge', tags=['knowledge']) +app.include_router(prompts.router, prefix='/api/v1/prompts', tags=['prompts']) +app.include_router(tools.router, prefix='/api/v1/tools', tags=['tools']) +app.include_router(skills.router, prefix='/api/v1/skills', tags=['skills']) + +app.include_router(memories.router, prefix='/api/v1/memories', tags=['memories']) +app.include_router(folders.router, prefix='/api/v1/folders', tags=['folders']) +app.include_router(groups.router, prefix='/api/v1/groups', tags=['groups']) +app.include_router(files.router, prefix='/api/v1/files', tags=['files']) +app.include_router(functions.router, prefix='/api/v1/functions', tags=['functions']) +app.include_router(evaluations.router, prefix='/api/v1/evaluations', tags=['evaluations']) +if ENABLE_ADMIN_ANALYTICS: + app.include_router(analytics.router, prefix='/api/v1/analytics', tags=['analytics']) +app.include_router(utils.router, prefix='/api/v1/utils', tags=['utils']) +app.include_router(terminals.router, prefix='/api/v1/terminals', tags=['terminals']) +app.include_router(automations.router, prefix='/api/v1/automations', tags=['automations']) +app.include_router(calendar.router, prefix='/api/v1/calendars', tags=['calendars']) + +# SCIM 2.0 API for identity management +if ENABLE_SCIM: + app.include_router(scim.router, prefix='/api/v1/scim/v2', tags=['scim']) + + +try: + audit_level = AuditLevel(AUDIT_LOG_LEVEL) +except ValueError as e: + logger.error(f'Invalid audit level: {AUDIT_LOG_LEVEL}. Error: {e}') + audit_level = AuditLevel.NONE + +if audit_level != AuditLevel.NONE: + app.add_middleware( + AuditLoggingMiddleware, + audit_level=audit_level, + excluded_paths=AUDIT_EXCLUDED_PATHS, + included_paths=AUDIT_INCLUDED_PATHS, + audit_get_requests=ENABLE_AUDIT_GET_REQUESTS, + max_body_size=MAX_BODY_LOG_SIZE, + ) +################################## +# +# Chat Endpoints +# +################################## + + +@app.get('/api/models') +@app.get('/api/v1/models') # Experimental: Compatibility with OpenAI API +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) + + models = [] + for model in all_models: + # Filter out filter pipelines + if 'pipeline' in model and model['pipeline'].get('type', None) == 'filter': + continue + + # Remove profile image URL to reduce payload size + if model.get('info', {}).get('meta', {}).get('profile_image_url'): + model['info']['meta'].pop('profile_image_url', None) + + try: + model_tags = [tag.get('name') for tag in model.get('info', {}).get('meta', {}).get('tags', [])] + tags = [tag.get('name') for tag in model.get('tags', [])] + + tags = list(set(model_tags + tags)) + model['tags'] = [{'name': tag} for tag in tags] + except Exception as e: + log.debug(f'Error processing model tags: {e}') + model['tags'] = [] + pass + + models.append(model) + + model_order_list = request.app.state.config.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 + models.sort( + key=lambda model: ( + model_order_dict.get(model.get('id', ''), float('inf')), + (model.get('name', '') or ''), + ) + ) + + models = await get_filtered_models(models, user) + + log.debug( + f'/api/models returned filtered models accessible to the user: {json.dumps([model.get("id") for model in models])}' + ) + return {'data': models} + + +@app.get('/api/models/base') +async def get_base_models(request: Request, user=Depends(get_admin_user)): + models = await get_all_base_models(request, user=user) + return {'data': models} + + +################################## +# Embeddings +################################## + + +@app.post('/api/embeddings') +@app.post('/api/v1/embeddings') # Experimental: Compatibility with OpenAI API +async def embeddings(request: Request, form_data: dict, user=Depends(get_verified_user)): + """ + OpenAI-compatible embeddings endpoint. + + This handler: + - Performs user/model checks and dispatches to the correct backend. + - Supports OpenAI, Ollama, arena models, pipelines, and any compatible provider. + + Args: + request (Request): Request context. + form_data (dict): OpenAI-like payload (e.g., {"model": "...", "input": [...]}) + user (UserModel): Authenticated user. + + Returns: + dict: OpenAI-compatible embeddings response. + """ + # Make sure models are loaded in app state + if not request.app.state.MODELS: + await get_all_models(request, user=user) + # Use generic dispatcher in utils.embeddings + return await generate_embeddings(request, form_data, user) + + +@app.post('/api/chat/completions') +@app.post('/api/v1/chat/completions') # Experimental: Compatibility with OpenAI API +async def chat_completion( + request: Request, + form_data: dict, + user=Depends(get_verified_user), +): + if not request.app.state.MODELS: + await get_all_models(request, user=user) + + model_id = form_data.get('model', None) + model_item = form_data.pop('model_item', {}) + tasks = form_data.pop('background_tasks', None) + + metadata = {} + try: + model_info = None + if not model_item.get('direct', False): + if model_id not in request.app.state.MODELS: + raise Exception('Model not found') + + model = request.app.state.MODELS[model_id] + model_info = await Models.get_model_by_id(model_id) + + # Check if user has access to the model + if not BYPASS_MODEL_ACCESS_CONTROL and (user.role != 'admin' or not BYPASS_ADMIN_ACCESS_CONTROL): + try: + await check_model_access(user, model) + except Exception as e: + raise e + else: + model = model_item + + request.state.direct = True + request.state.model = model + + # Model params: global defaults as base, per-model overrides win + default_model_params = getattr(request.app.state.config, 'DEFAULT_MODEL_PARAMS', None) or {} + model_info_params = { + **default_model_params, + **(model_info.params.model_dump() if model_info and model_info.params else {}), + } + + # Check base model existence for custom models + if model_info and model_info.base_model_id: + base_model_id = model_info.base_model_id + if base_model_id not in request.app.state.MODELS: + if ENABLE_CUSTOM_MODEL_FALLBACK: + default_models = (request.app.state.config.DEFAULT_MODELS or '').split(',') + + fallback_model_id = default_models[0].strip() if default_models[0] else None + + if fallback_model_id and fallback_model_id in request.app.state.MODELS: + # Update model and form_data so routing uses the fallback model's type + model = request.app.state.MODELS[fallback_model_id] + form_data['model'] = fallback_model_id + else: + raise Exception('Model not found') + else: + raise Exception('Model not found') + + # Chat Params + stream_delta_chunk_size = form_data.get('params', {}).get('stream_delta_chunk_size') + reasoning_tags = form_data.get('params', {}).get('reasoning_tags') + + # Model Params + if model_info_params.get('stream_response') is not None: + form_data['stream'] = model_info_params.get('stream_response') + + if model_info_params.get('stream_delta_chunk_size'): + stream_delta_chunk_size = model_info_params.get('stream_delta_chunk_size') + + if model_info_params.get('reasoning_tags') is not None: + reasoning_tags = model_info_params.get('reasoning_tags') + + # parent_id signals intent: + # null → new chat (root message, no parent) + # value → follow-up (user message's parentId = prev assistant) + # absent → legacy caller, no chat management + is_new_chat = 'parent_id' in form_data and form_data['parent_id'] is None and not form_data.get('chat_id') + parent_id = form_data.pop('parent_id', None) + form_data.pop('new_chat', None) # Legacy field + + # Multi-model: {model_id: assistant_message_id} + # Single-model fallback: built from 'model' + 'id' + message_ids = form_data.pop('message_ids', None) + if not message_ids: + message_ids = {model_id: form_data.pop('id', None)} + else: + form_data.pop('id', None) + + user_message = form_data.pop('user_message', None) or form_data.pop('parent_message', None) + metadata = { + 'user_id': user.id, + 'chat_id': form_data.pop('chat_id', None), + 'user_message': user_message, + 'user_message_id': user_message.get('id') if user_message else None, + 'session_id': form_data.pop('session_id', None), + 'folder_id': form_data.pop('folder_id', None), + 'filter_ids': form_data.pop('filter_ids', []), + 'tool_ids': form_data.get('tool_ids', None), + 'tool_servers': form_data.pop('tool_servers', None), + 'files': form_data.get('files', None), + 'features': form_data.get('features', {}), + 'variables': form_data.get('variables', {}), + 'model': model, + 'direct': model_item.get('direct', False), + 'params': { + 'stream_delta_chunk_size': stream_delta_chunk_size, + 'reasoning_tags': reasoning_tags, + 'function_calling': ( + 'native' + if ( + form_data.get('params', {}).get('function_calling') == 'native' + or model_info_params.get('function_calling') == 'native' + ) + else 'default' + ), + }, + } + + if is_new_chat: + metadata['chat_id'] = str(uuid4()) + + if metadata.get('chat_id') and user: + chat_id = metadata['chat_id'] + if not chat_id.startswith('local:'): # temporary chats are not stored + if is_new_chat: + # Build the full history upfront with ALL assistant placeholders + user_message = metadata.get('user_message') or {} + user_message_id = user_message.get('id') if user_message else None + + history_messages = {} + all_assistant_ids = [assistant_id for assistant_id in message_ids.values() if assistant_id] + + if user_message_id and user_message: + user_message['childrenIds'] = all_assistant_ids + history_messages[user_message_id] = user_message + + for target_model_id, assistant_message_id in message_ids.items(): + if assistant_message_id: + history_messages[assistant_message_id] = { + 'id': assistant_message_id, + 'parentId': user_message_id, + 'childrenIds': [], + 'role': 'assistant', + 'content': '', + 'done': False, + 'model': target_model_id, + 'timestamp': int(time.time()), + } + + await Chats.insert_new_chat( + chat_id, + user.id, + ChatForm( + chat={ + 'id': chat_id, + 'title': 'New Chat', + 'models': list(message_ids.keys()), + 'history': { + 'currentId': all_assistant_ids[0] if all_assistant_ids else user_message_id, + 'messages': history_messages, + }, + 'messages': [ + {'role': 'user', 'content': user_message.get('content', '')}, + ] + if user_message_id + else [], + 'tags': [], + 'timestamp': int(time.time() * 1000), + }, + folder_id=metadata.get('folder_id'), + ), + ) + + # Insert chat files from user message if any + user_message_files = user_message.get('files', []) + if user_message_files: + try: + await Chats.insert_chat_files( + chat_id, + user_message_id, + [ + file_item.get('id') + for file_item in user_message_files + if file_item.get('type') == 'file' + ], + user.id, + ) + except Exception as e: + log.debug(f'Error inserting chat files: {e}') + pass + else: + # Existing chat — verify ownership + if not await Chats.is_chat_owner(chat_id, user.id) and user.role != 'admin': + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + # Save user message to DB + user_message = metadata.get('user_message') or {} + if user_message and user_message.get('id'): + await Chats.upsert_message_to_chat_by_id_and_message_id( + chat_id, + user_message['id'], + user_message, + ) + + # Link grandparent → user message (childrenIds) + grandparent_id = user_message.get('parentId') + if grandparent_id: + grandparent = await Chats.get_message_by_id_and_message_id(chat_id, grandparent_id) + if grandparent: + child_ids = grandparent.get('childrenIds', []) + if user_message['id'] not in child_ids: + child_ids.append(user_message['id']) + await Chats.upsert_message_to_chat_by_id_and_message_id( + chat_id, grandparent_id, {'childrenIds': child_ids} + ) + + # Insert chat files from user message if any + user_message_files = user_message.get('files', []) + if user_message_files: + try: + await Chats.insert_chat_files( + chat_id, + user_message.get('id'), + [ + file_item.get('id') + for file_item in user_message_files + if file_item.get('type') == 'file' + ], + user.id, + ) + except Exception as e: + log.debug(f'Error inserting chat files: {e}') + pass + + # Save ALL assistant placeholders + user_message_id = metadata.get('user_message_id') + all_assistant_ids = [assistant_id for assistant_id in message_ids.values() if assistant_id] + + # Link user message → all assistant messages (childrenIds) + if user_message_id and all_assistant_ids: + existing_user_message = await Chats.get_message_by_id_and_message_id(chat_id, user_message_id) + if existing_user_message: + child_ids = existing_user_message.get('childrenIds', []) + for assistant_id in all_assistant_ids: + if assistant_id not in child_ids: + child_ids.append(assistant_id) + await Chats.upsert_message_to_chat_by_id_and_message_id( + chat_id, + user_message_id, + {'childrenIds': child_ids}, + ) + + # Save each assistant placeholder + for target_model_id, assistant_message_id in message_ids.items(): + if assistant_message_id: + await Chats.upsert_message_to_chat_by_id_and_message_id( + chat_id, + assistant_message_id, + { + 'id': assistant_message_id, + 'parentId': user_message_id, + 'childrenIds': [], + 'role': 'assistant', + 'content': '', + 'done': False, + 'model': target_model_id, + 'timestamp': int(time.time()), + }, + ) + + request.state.metadata = metadata + form_data['metadata'] = metadata + + except HTTPException: + raise + except Exception as e: + log.warning(f'Error processing chat metadata: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + + async def process_chat(request, form_data, user, metadata, model, tasks=None): + try: + form_data, metadata, events = await process_chat_payload(request, form_data, user, metadata, model) + + response = await chat_completion_handler(request, form_data, user) + + # When the upstream provider returns an error (e.g. HTTP 400 + # content-filter, quota exceeded), generate_chat_completion + # returns a JSONResponse instead of raising. Detect this and + # raise so the except-block below emits chat:message:error + + # chat:tasks:cancel, unblocking the frontend. + if isinstance(response, JSONResponse) and response.status_code >= 400: + try: + error_body = json.loads(response.body.decode('utf-8', 'replace')) + detail = error_body.get('error', error_body) if isinstance(error_body, dict) else error_body + if isinstance(detail, dict): + detail = detail.get('message', detail.get('detail', str(detail))) + except Exception: + detail = f'Provider returned HTTP {response.status_code}' + raise Exception(detail) + + ctx = await build_chat_response_context(request, form_data, user, model, metadata, tasks, events) + + return await process_chat_response(response, ctx) + except asyncio.CancelledError: + log.info('Chat processing was cancelled') + try: + + async def emit_cancel_event(): + event_emitter = await get_event_emitter(metadata) + if event_emitter: + await event_emitter({'type': 'chat:tasks:cancel'}) + + await asyncio.shield(emit_cancel_event()) + except Exception: + pass + raise # re-raise to ensure proper task cancellation handling + except Exception as e: + error_detail = e.detail if isinstance(e, HTTPException) else str(e) + log.error('Error processing chat payload: %s', error_detail) + if metadata.get('chat_id') and metadata.get('message_id'): + # Update the chat message with the error + try: + if not metadata['chat_id'].startswith('local:'): + await Chats.upsert_message_to_chat_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], + { + 'parentId': metadata.get('user_message_id', None), + 'error': {'content': error_detail}, + }, + ) + + event_emitter = await get_event_emitter(metadata) + if event_emitter: + await event_emitter( + { + 'type': 'chat:message:error', + 'data': {'error': {'content': error_detail}}, + } + ) + await event_emitter( + {'type': 'chat:tasks:cancel'}, + ) + + except Exception: + pass + else: + # No chat_id/message_id → legacy/direct API path with no + # WebSocket error channel. We must surface the error as + # a proper HTTP response; without this the function would + # return None which FastAPI serializes as null. #23924 + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=error_detail, + ) + finally: + # MCP cleanup — MUST run in the SAME asyncio task as + # connect() because the MCP SDK's streamablehttp_client + # uses anyio task groups whose cancel scopes enforce + # same-task exit. Do NOT wrap in asyncio.shield() or + # asyncio.wait_for() — both create a new task. + try: + if mcp_clients := metadata.get('mcp_clients'): + for client in reversed(list(mcp_clients.values())): + try: + await client.disconnect() + except Exception as e: + log.debug(f'Error disconnecting MCP client: {e}') + except asyncio.CancelledError: + # Let the client close asynchronously by GC + pass + except Exception as e: + log.debug(f'Error cleaning up MCP clients: {e}') + except asyncio.CancelledError: + pass + + try: + if metadata.get('chat_id'): + + async def emit_inactive_event(): + try: + event_emitter = await get_event_emitter(metadata, update_db=False) + if event_emitter: + await event_emitter({'type': 'chat:active', 'data': {'active': False}}) + except Exception: + pass + + try: + # Shield the event emission so it finishes even if the main task is cancelled + await asyncio.shield(emit_inactive_event()) + except asyncio.CancelledError: + pass + except Exception: + pass + + # Fan out: one task per model + if metadata.get('session_id') and metadata.get('chat_id'): + task_ids = [] + chat_id = metadata['chat_id'] + + for idx, (target_model_id, assistant_message_id) in enumerate(message_ids.items()): + if not assistant_message_id: + continue + + # Per-model metadata: own message_id + model + per_model_metadata = { + **metadata, + 'message_id': assistant_message_id, + } + + # Per-model form_data: own model + model_form_data = { + **form_data, + 'model': target_model_id, + 'metadata': per_model_metadata, + } + + # Resolve the model object for this specific model + resolved_model = request.app.state.MODELS.get(target_model_id, model) + + # Only the first model runs title/tags generation; + # subsequent models only run follow-ups. + task_id, _ = await create_task( + request.app.state.redis, + process_chat( + request, + model_form_data, + user, + per_model_metadata, + resolved_model, + tasks + if idx == 0 + else { + k: v + for k, v in (tasks or {}).items() + if k not in (TASKS.TITLE_GENERATION, TASKS.TAGS_GENERATION) + } + or None, + ), + id=chat_id, + ) + task_ids.append(task_id) + + # Emit chat:active=true + if task_ids: + event_emitter = await get_event_emitter( + {**metadata, 'message_id': list(message_ids.values())[0]}, + update_db=False, + ) + if event_emitter: + await event_emitter({'type': 'chat:active', 'data': {'active': True}}) + + return { + 'status': True, + 'task_ids': task_ids, + 'chat_id': chat_id, + } + else: + # Legacy/direct: single model, synchronous + metadata['message_id'] = list(message_ids.values())[0] + return await process_chat(request, form_data, user, metadata, model, tasks) + + +# Alias for chat_completion (Legacy) +generate_chat_completions = chat_completion +generate_chat_completion = chat_completion + +# Expose as app.state so internal callers (e.g. automations) can +# use the full pipeline without importing from main.py (avoids circular deps). +app.state.CHAT_COMPLETION_HANDLER = chat_completion + + +################################## +# +# Anthropic Messages API Compatible Endpoint +# +################################## + + +from open_webui.utils.anthropic import ( + convert_anthropic_to_openai_payload, + convert_openai_to_anthropic_response, + openai_stream_to_anthropic_stream, +) + + +@app.post('/api/message') +@app.post('/api/v1/messages') # Anthropic Messages API compatible endpoint +async def generate_messages( + request: Request, + form_data: dict, + user=Depends(get_verified_user), +): + """ + Anthropic Messages API compatible endpoint. + + Accepts the Anthropic Messages API format, converts internally to OpenAI + Chat Completions format, routes through the existing chat completion + pipeline, then converts the response back to Anthropic Messages format. + + Supports both streaming and non-streaming requests. + All models configured in Open WebUI are accessible via this endpoint. + + Authentication: Supports both standard Authorization header and + Anthropic's x-api-key header (via middleware translation). + """ + # Convert Anthropic payload to OpenAI format + requested_model = form_data.get('model', '') + + openai_payload = convert_anthropic_to_openai_payload(form_data) + + # Route through the existing chat_completion handler + response = await chat_completion(request, openai_payload, user) + + # Convert response back to Anthropic format + if isinstance(response, StreamingResponse): + # Streaming response: wrap the generator to convert SSE format + return StreamingResponse( + openai_stream_to_anthropic_stream(response.body_iterator, model=requested_model), + media_type='text/event-stream', + headers={ + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, + ) + elif isinstance(response, dict): + return convert_openai_to_anthropic_response(response, model=requested_model) + else: + # Passthrough for error responses (JSONResponse, PlainTextResponse, etc.) + return response + + +@app.post('/api/chat/completed') +async def chat_completed(request: Request, form_data: dict, user=Depends(get_verified_user)): + """Deprecated: outlet filters now run inline during chat completion. + Kept for backward compatibility with external integrations.""" + try: + model_item = form_data.pop('model_item', {}) + + if model_item.get('direct', False): + request.state.direct = True + request.state.model = model_item + + return await chat_completed_handler(request, form_data, user) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + + +@app.post('/api/chat/actions/{action_id}') +async def chat_action(request: Request, action_id: str, form_data: dict, user=Depends(get_verified_user)): + try: + model_item = form_data.pop('model_item', {}) + + if model_item.get('direct', False): + request.state.direct = True + request.state.model = model_item + + return await chat_action_handler(request, action_id, form_data, user) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + + +@app.post('/api/tasks/stop/{task_id}') +async def stop_task_endpoint(request: Request, task_id: str, user=Depends(get_admin_user)): + try: + result = await stop_task(request.app.state.redis, task_id) + return result + except ValueError as e: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) + + +@app.get('/api/tasks') +async def list_tasks_endpoint(request: Request, user=Depends(get_admin_user)): + return {'tasks': await list_tasks(request.app.state.redis)} + + +@app.get('/api/tasks/chat/{chat_id:path}') +async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)): + if chat_id.startswith('local:'): + socket_id = chat_id[len('local:') :] + owner_id = get_user_id_from_session_pool(socket_id) + if owner_id != user.id and user.role != 'admin': + return {'task_ids': []} + else: + chat = await Chats.get_chat_by_id(chat_id) + if chat is None or (chat.user_id != user.id and user.role != 'admin'): + return {'task_ids': []} + + task_ids = await list_task_ids_by_item_id(request.app.state.redis, chat_id) + + log.debug(f'Task IDs for chat {chat_id}: {task_ids}') + return {'task_ids': task_ids} + + +@app.post('/api/tasks/chat/{chat_id:path}/stop') +async def stop_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)): + if chat_id.startswith('local:'): + socket_id = chat_id[len('local:') :] + owner_id = get_user_id_from_session_pool(socket_id) + if owner_id != user.id and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + else: + chat = await Chats.get_chat_by_id(chat_id) + if chat is None or (chat.user_id != user.id and user.role != 'admin'): + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + result = await stop_item_tasks(request.app.state.redis, chat_id) + return result + + +################################## +# +# Config Endpoints +# +################################## + + +@app.get('/api/config') +async def get_app_config(request: Request): + user = None + token = None + + auth_header = request.headers.get('Authorization') + if auth_header: + cred = get_http_authorization_cred(auth_header) + if cred: + token = cred.credentials + + if not token and 'token' in request.cookies: + token = request.cookies.get('token') + + if token: + try: + data = decode_token(token) + except Exception as e: + log.debug(e) + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Invalid token', + ) + if data is not None and 'id' in data: + user = await Users.get_user_by_id(data['id']) + + user_count = await Users.get_num_users() + onboarding = False + + if user is None: + onboarding = user_count == 0 + + return { + **({'onboarding': True} if onboarding else {}), + 'status': True, + 'name': app.state.WEBUI_NAME, + 'version': VERSION, + 'default_locale': str(DEFAULT_LOCALE), + 'oauth': {'providers': {name: config.get('name', name) for name, config in OAUTH_PROVIDERS.items()}}, + 'features': { + 'auth': WEBUI_AUTH, + 'auth_trusted_header': bool(app.state.AUTH_TRUSTED_EMAIL_HEADER), + 'enable_signup_password_confirmation': ENABLE_SIGNUP_PASSWORD_CONFIRMATION, + 'enable_ldap': app.state.config.ENABLE_LDAP, + 'enable_api_keys': app.state.config.ENABLE_API_KEYS, + 'enable_signup': app.state.config.ENABLE_SIGNUP, + 'enable_login_form': app.state.config.ENABLE_LOGIN_FORM, + 'enable_password_change_form': app.state.config.ENABLE_PASSWORD_CHANGE_FORM, + 'enable_websocket': ENABLE_WEBSOCKET_SUPPORT, + 'enable_version_update_check': ENABLE_VERSION_UPDATE_CHECK, + 'enable_public_active_users_count': ENABLE_PUBLIC_ACTIVE_USERS_COUNT, + 'enable_easter_eggs': ENABLE_EASTER_EGGS, + **( + { + 'enable_direct_connections': app.state.config.ENABLE_DIRECT_CONNECTIONS, + 'enable_folders': app.state.config.ENABLE_FOLDERS, + 'folder_max_file_count': app.state.config.FOLDER_MAX_FILE_COUNT, + 'enable_channels': app.state.config.ENABLE_CHANNELS, + 'enable_calendar': app.state.config.ENABLE_CALENDAR, + 'enable_automations': app.state.config.ENABLE_AUTOMATIONS, + 'enable_notes': app.state.config.ENABLE_NOTES, + 'enable_web_search': app.state.config.ENABLE_WEB_SEARCH, + 'enable_code_execution': app.state.config.ENABLE_CODE_EXECUTION, + 'enable_code_interpreter': app.state.config.ENABLE_CODE_INTERPRETER, + 'enable_image_generation': app.state.config.ENABLE_IMAGE_GENERATION, + 'enable_autocomplete_generation': app.state.config.ENABLE_AUTOCOMPLETE_GENERATION, + 'enable_community_sharing': app.state.config.ENABLE_COMMUNITY_SHARING, + 'enable_message_rating': app.state.config.ENABLE_MESSAGE_RATING, + 'enable_user_webhooks': app.state.config.ENABLE_USER_WEBHOOKS, + 'enable_user_status': app.state.config.ENABLE_USER_STATUS, + 'enable_admin_export': ENABLE_ADMIN_EXPORT, + 'enable_admin_chat_access': ENABLE_ADMIN_CHAT_ACCESS, + 'enable_admin_analytics': ENABLE_ADMIN_ANALYTICS, + 'enable_google_drive_integration': app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, + 'enable_onedrive_integration': app.state.config.ENABLE_ONEDRIVE_INTEGRATION, + 'enable_memories': app.state.config.ENABLE_MEMORIES, + **( + { + 'enable_onedrive_personal': ENABLE_ONEDRIVE_PERSONAL, + 'enable_onedrive_business': ENABLE_ONEDRIVE_BUSINESS, + } + if app.state.config.ENABLE_ONEDRIVE_INTEGRATION + else {} + ), + } + if user is not None + else {} + ), + }, + **( + { + 'default_models': app.state.config.DEFAULT_MODELS, + 'default_pinned_models': app.state.config.DEFAULT_PINNED_MODELS, + 'default_prompt_suggestions': app.state.config.DEFAULT_PROMPT_SUGGESTIONS, + 'user_count': user_count, + 'code': { + 'engine': app.state.config.CODE_EXECUTION_ENGINE, + 'interpreter_engine': app.state.config.CODE_INTERPRETER_ENGINE, + }, + 'audio': { + 'tts': { + 'engine': app.state.config.TTS_ENGINE, + 'voice': app.state.config.TTS_VOICE, + 'split_on': app.state.config.TTS_SPLIT_ON, + }, + 'stt': { + 'engine': app.state.config.STT_ENGINE, + }, + }, + 'file': { + 'max_size': app.state.config.FILE_MAX_SIZE, + 'max_count': app.state.config.FILE_MAX_COUNT, + 'image_compression': { + 'width': app.state.config.FILE_IMAGE_COMPRESSION_WIDTH, + 'height': app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT, + }, + }, + 'permissions': {**app.state.config.USER_PERMISSIONS}, + 'google_drive': { + 'client_id': GOOGLE_DRIVE_CLIENT_ID.value, + 'api_key': GOOGLE_DRIVE_API_KEY.value, + }, + 'onedrive': { + 'client_id_personal': ONEDRIVE_CLIENT_ID_PERSONAL, + 'client_id_business': ONEDRIVE_CLIENT_ID_BUSINESS, + 'sharepoint_url': ONEDRIVE_SHAREPOINT_URL.value, + 'sharepoint_tenant_id': ONEDRIVE_SHAREPOINT_TENANT_ID.value, + }, + 'ui': { + 'pending_user_overlay_title': app.state.config.PENDING_USER_OVERLAY_TITLE, + 'pending_user_overlay_content': app.state.config.PENDING_USER_OVERLAY_CONTENT, + 'response_watermark': app.state.config.RESPONSE_WATERMARK, + }, + 'license_metadata': app.state.LICENSE_METADATA, + **( + { + 'active_entries': app.state.USER_COUNT, + } + if user.role == 'admin' + else {} + ), + } + if user is not None and (user.role in ['admin', 'user']) + else { + **( + { + 'ui': { + 'pending_user_overlay_title': app.state.config.PENDING_USER_OVERLAY_TITLE, + 'pending_user_overlay_content': app.state.config.PENDING_USER_OVERLAY_CONTENT, + } + } + if user and user.role == 'pending' + else {} + ), + **( + { + 'metadata': { + 'login_footer': app.state.LICENSE_METADATA.get('login_footer', ''), + 'auth_logo_position': app.state.LICENSE_METADATA.get('auth_logo_position', ''), + } + } + if app.state.LICENSE_METADATA + else {} + ), + } + ), + } + + +class UrlForm(BaseModel): + url: str + + +@app.get('/api/webhook') +async def get_webhook_url(user=Depends(get_admin_user)): + return { + 'url': app.state.config.WEBHOOK_URL, + } + + +@app.post('/api/webhook') +async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)): + app.state.config.WEBHOOK_URL = form_data.url + app.state.WEBHOOK_URL = app.state.config.WEBHOOK_URL + return {'url': app.state.config.WEBHOOK_URL} + + +@app.get('/api/version') +async def get_app_version(): + return { + 'version': VERSION, + 'deployment_id': DEPLOYMENT_ID, + } + + +@app.get('/api/version/updates') +async def get_app_latest_release_version(user=Depends(get_verified_user)): + if not ENABLE_VERSION_UPDATE_CHECK: + log.debug(f'Version update check is disabled, returning current version as latest version') + return {'current': VERSION, 'latest': VERSION} + try: + timeout = aiohttp.ClientTimeout(total=1) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.get( + 'https://api.github.com/repos/open-webui/open-webui/releases/latest', + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() + latest_version = data['tag_name'] + + return {'current': VERSION, 'latest': latest_version[1:]} + except Exception as e: + log.debug(e) + return {'current': VERSION, 'latest': VERSION} + + +@app.get('/api/changelog') +async def get_app_changelog(): + return {key: CHANGELOG[key] for idx, key in enumerate(CHANGELOG) if idx < 5} + + +@app.get('/api/usage') +async def get_current_usage(user=Depends(get_verified_user)): + """ + Get current usage statistics for Open WebUI. + This is an experimental endpoint and subject to change. + """ + try: + # If public visibility is disabled, only allow admins to access this endpoint + if not ENABLE_PUBLIC_ACTIVE_USERS_COUNT and user.role != 'admin': + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail='Access denied. Only administrators can view usage statistics.', + ) + + return { + 'model_ids': get_models_in_use(), + 'user_count': await Users.get_active_user_count(), + } + except HTTPException: + raise + except Exception as e: + log.error(f'Error getting usage statistics: {e}') + raise HTTPException(status_code=500, detail='Internal Server Error') + + +############################ +# OAuth Login & Callback +############################ + + +# Initialize OAuth client manager with any MCP tool servers using OAuth 2.1 +if len(app.state.config.TOOL_SERVER_CONNECTIONS) > 0: + for tool_server_connection in app.state.config.TOOL_SERVER_CONNECTIONS: + if tool_server_connection.get('type', 'openapi') == 'mcp': + server_id = tool_server_connection.get('info', {}).get('id') + auth_type = tool_server_connection.get('auth_type', 'none') + + if server_id and auth_type in ('oauth_2.1', 'oauth_2.1_static'): + try: + oauth_client_info = resolve_oauth_client_info(tool_server_connection) + app.state.oauth_client_manager.add_client( + f'mcp:{server_id}', + OAuthClientInformationFull(**oauth_client_info), + ) + except Exception as e: + log.error(f'Error adding OAuth client for MCP tool server {server_id}: {e}') + pass + +try: + if ENABLE_STAR_SESSIONS_MIDDLEWARE: + redis_session_store = RedisStore( + url=REDIS_URL, + prefix=(f'{REDIS_KEY_PREFIX}:session:' if REDIS_KEY_PREFIX else 'session:'), + ) + + app.add_middleware(SessionAutoloadMiddleware) + app.add_middleware( + StarSessionsMiddleware, + store=redis_session_store, + cookie_name='owui-session', + cookie_same_site=WEBUI_SESSION_COOKIE_SAME_SITE, + cookie_https_only=WEBUI_SESSION_COOKIE_SECURE, + ) + log.info('Using Redis for session') + else: + raise ValueError('No Redis URL provided') +except Exception as e: + app.add_middleware( + SessionMiddleware, + secret_key=WEBUI_SECRET_KEY, + session_cookie='owui-session', + same_site=WEBUI_SESSION_COOKIE_SAME_SITE, + https_only=WEBUI_SESSION_COOKIE_SECURE, + ) + + +async def register_client(request, client_id: str) -> bool: + server_type, server_id = client_id.split(':', 1) + + connection = None + connection_idx = None + + for idx, conn in enumerate(request.app.state.config.TOOL_SERVER_CONNECTIONS or []): + if conn.get('type', 'openapi') == server_type: + info = conn.get('info', {}) + if info.get('id') == server_id: + connection = conn + connection_idx = idx + break + + if connection is None or connection_idx is None: + log.warning(f'Unable to locate MCP tool server configuration for client {client_id} during re-registration') + return False + + server_url = connection.get('url') + auth_type = connection.get('auth_type', 'none') + oauth_server_key = (connection.get('config') or {}).get('oauth_server_key') + + try: + if auth_type == 'oauth_2.1_static': + # Static credentials: rebuild from admin-provided credentials + fresh metadata + info = connection.get('info', {}) + oauth_client_id = info.get('oauth_client_id') or '' + oauth_client_secret = info.get('oauth_client_secret') or '' + if not oauth_client_id or not oauth_client_secret: + # Fall back to blob for backward compatibility + existing_client_info = info.get('oauth_client_info', '') + if not existing_client_info: + log.error(f'No stored OAuth client info for static client {client_id}') + return False + existing_data = decrypt_data(existing_client_info) + oauth_client_id = oauth_client_id or existing_data.get('client_id', '') + oauth_client_secret = oauth_client_secret or existing_data.get('client_secret', '') + oauth_client_info = await get_oauth_client_info_with_static_credentials( + request, + client_id, + server_url, + oauth_client_id=oauth_client_id, + oauth_client_secret=oauth_client_secret, + ) + else: + oauth_client_info = await get_oauth_client_info_with_dynamic_client_registration( + request, + client_id, + server_url, + oauth_server_key, + ) + except Exception as e: + log.error(f'OAuth client re-registration failed for {client_id}: {e}') + return False + + try: + connections = request.app.state.config.TOOL_SERVER_CONNECTIONS + connections[connection_idx] = { + **connection, + 'info': { + **connection.get('info', {}), + 'oauth_client_info': encrypt_data(oauth_client_info.model_dump(mode='json')), + }, + } + # Re-assign the full list to trigger AppConfig.__setattr__ → PersistentConfig.save() + # (in-place list mutation via list[idx] = ... does not trigger __setattr__) + request.app.state.config.TOOL_SERVER_CONNECTIONS = connections + except Exception as e: + log.error(f'Failed to persist updated OAuth client info for tool server {client_id}: {e}') + return False + + oauth_client_manager.remove_client(client_id) + oauth_client_manager.add_client(client_id, oauth_client_info) + log.info(f'Re-registered OAuth client {client_id} for tool server') + return True + + +@app.get('/oauth/clients/{client_id}/authorize') +async def oauth_client_authorize( + client_id: str, + request: Request, + response: Response, + user=Depends(get_verified_user), +): + # ensure_valid_client_registration + client = oauth_client_manager.get_client(client_id) + client_info = oauth_client_manager.get_client_info(client_id) + if client is None or client_info is None: + raise HTTPException(status.HTTP_404_NOT_FOUND) + + if not await oauth_client_manager._preflight_authorization_url(client, client_info): + log.info( + 'Detected invalid OAuth client %s; attempting re-registration', + client_id, + ) + + registered = await register_client(request, client_id) + if not registered: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Failed to re-register OAuth client', + ) + + client = oauth_client_manager.get_client(client_id) + client_info = oauth_client_manager.get_client_info(client_id) + if client is None or client_info is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='OAuth client unavailable after re-registration', + ) + + if not await oauth_client_manager._preflight_authorization_url(client, client_info): + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='OAuth client registration is still invalid after re-registration', + ) + + return await oauth_client_manager.handle_authorize(request, client_id=client_id) + + +@app.get('/oauth/clients/{client_id}/callback') +async def oauth_client_callback( + client_id: str, + request: Request, + response: Response, + user=Depends(get_verified_user), +): + return await oauth_client_manager.handle_callback( + request, + client_id=client_id, + user_id=user.id if user else None, + response=response, + ) + + +@app.get('/oauth/{provider}/login') +async def oauth_login(provider: str, request: Request): + return await oauth_manager.handle_login(request, provider) + + +# OAuth login logic is as follows: +# 1. Attempt to find a user with matching subject ID, tied to the provider +# 2. If OAUTH_MERGE_ACCOUNTS_BY_EMAIL is true, find a user with the email address provided via OAuth +# - This is considered insecure in general, as OAuth providers do not always verify email addresses +# 3. If there is no user, and ENABLE_OAUTH_SIGNUP is true, create a user +# - Email addresses are considered unique, so we fail registration if the email address is already taken +@app.get('/oauth/{provider}/login/callback') +@app.get('/oauth/{provider}/callback') # Legacy endpoint +async def oauth_login_callback( + provider: str, + request: Request, + response: Response, + db: AsyncSession = Depends(get_async_session), +): + return await oauth_manager.handle_callback(request, provider, response, db=db) + + +############################ +# OIDC Back-Channel Logout +############################ + + +@app.post('/oauth/backchannel-logout') +async def oauth_backchannel_logout( + request: Request, + db: AsyncSession = Depends(get_async_session), +): + if not ENABLE_OAUTH_BACKCHANNEL_LOGOUT: + raise HTTPException(status_code=404) + return await oauth_manager.handle_backchannel_logout(request, db=db) + + +@app.get('/manifest.json') +async def get_manifest_json(): + if app.state.EXTERNAL_PWA_MANIFEST_URL: + session = await get_session() + async with session.get( + app.state.EXTERNAL_PWA_MANIFEST_URL, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + return await r.json() + else: + return { + 'name': app.state.WEBUI_NAME, + 'short_name': app.state.WEBUI_NAME, + 'description': f'{app.state.WEBUI_NAME} is an open, extensible, user-friendly interface for AI that adapts to your workflow.', + 'start_url': '/', + 'display': 'standalone', + 'background_color': '#343541', + 'icons': [ + { + 'src': '/static/logo.png', + 'type': 'image/png', + 'sizes': '500x500', + 'purpose': 'any', + }, + { + 'src': '/static/logo.png', + 'type': 'image/png', + 'sizes': '500x500', + 'purpose': 'maskable', + }, + ], + 'share_target': { + 'action': '/', + 'method': 'GET', + 'params': {'text': 'shared'}, + }, + } + + +@app.get('/opensearch.xml') +async def get_opensearch_xml(): + xml_content = rf""" + + {app.state.WEBUI_NAME} + Search {app.state.WEBUI_NAME} + UTF-8 + {app.state.config.WEBUI_URL}/static/favicon.png + + {app.state.config.WEBUI_URL} + + """ + return Response(content=xml_content, media_type='application/xml') + + +@app.get('/health') +async def healthcheck(): + return {'status': True} + + +@app.get('/ready') +async def readiness_check(): + """ + Returns 200 only when the application is ready to accept traffic. + """ + + # Ensure application startup work has completed + if not getattr(app.state, 'startup_complete', False): + log.info('Readiness check failed: startup not complete') + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail='Startup not complete', + ) + + # Check database connectivity + try: + ScopedSession.execute(text('SELECT 1;')).all() + except Exception as e: + log.warning(f'Readiness check DB ping failed: {e!r}') + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail='Database not ready', + ) + + # Check Redis connectivity if configured + redis = app.state.redis + if redis is not None: + try: + pong = await redis.ping() + if pong is False: + raise Exception('Redis PING returned False') + except Exception as e: + log.warning(f'Readiness check Redis ping failed: {e!r}') + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail='Redis not ready', + ) + + return {'status': True} + + +@app.get('/health/db') +async def healthcheck_with_db(): + ScopedSession.execute(text('SELECT 1;')).all() + return {'status': True} + + +app.mount('/static', StaticFiles(directory=STATIC_DIR), name='static') + + +@app.get('/cache/{path:path}') +async def serve_cache_file( + path: str, + user=Depends(get_verified_user), +): + file_path = os.path.abspath(os.path.join(CACHE_DIR, path)) + # prevent path traversal + if not file_path.startswith(os.path.abspath(CACHE_DIR)): + raise HTTPException(status_code=404, detail='File not found') + if not os.path.isfile(file_path): + raise HTTPException(status_code=404, detail='File not found') + return FileResponse(file_path) + + +def swagger_ui_html(*args, **kwargs): + return get_swagger_ui_html( + *args, + **kwargs, + swagger_js_url='/static/swagger-ui/swagger-ui-bundle.js', + swagger_css_url='/static/swagger-ui/swagger-ui.css', + swagger_favicon_url='/static/swagger-ui/favicon.png', + ) + + +applications.get_swagger_ui_html = swagger_ui_html + +if os.path.exists(FRONTEND_BUILD_DIR): + mimetypes.add_type('text/javascript', '.js') + app.mount( + '/', + SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True), + name='spa-static-files', + ) +else: + log.warning(f"Frontend build directory not found at '{FRONTEND_BUILD_DIR}'. Serving API only.") diff --git a/backend/open_webui/migrations/README b/backend/open_webui/migrations/README new file mode 100644 index 0000000000000000000000000000000000000000..f1d93dff9dbd52e0a9fc8ce4d68e0784c07da697 --- /dev/null +++ b/backend/open_webui/migrations/README @@ -0,0 +1,4 @@ +Generic single-database configuration. + +Create new migrations with +DATABASE_URL= alembic revision --autogenerate -m "a description" diff --git a/backend/open_webui/migrations/env.py b/backend/open_webui/migrations/env.py new file mode 100644 index 0000000000000000000000000000000000000000..961a92becf3d3946e0a741c4f28fe90e68b33b26 --- /dev/null +++ b/backend/open_webui/migrations/env.py @@ -0,0 +1,120 @@ +import logging +from logging.config import fileConfig + +from alembic import context +from open_webui.models.auths import Auth +from open_webui.models.calendar import Calendar, CalendarEvent, CalendarEventAttendee # noqa: F401 +from open_webui.env import DATABASE_URL, DATABASE_PASSWORD, LOG_FORMAT +from open_webui.internal.db import extract_ssl_params_from_url, reattach_ssl_params_to_url +from sqlalchemy import engine_from_config, pool, create_engine + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name, disable_existing_loggers=False) + +# Re-apply JSON formatter after fileConfig replaces handlers. +if LOG_FORMAT == 'json': + from open_webui.env import JSONFormatter + + for handler in logging.root.handlers: + handler.setFormatter(JSONFormatter()) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = Auth.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + +DB_URL = DATABASE_URL + +# Normalize SSL query params for psycopg2 (Alembic uses psycopg2 for sync migrations). +url_without_ssl, ssl_params = extract_ssl_params_from_url(DB_URL) +DB_URL = reattach_ssl_params_to_url(url_without_ssl, ssl_params) if ssl_params else DB_URL + +if DB_URL: + config.set_main_option('sqlalchemy.url', DB_URL.replace('%', '%%')) + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option('sqlalchemy.url') + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={'paramstyle': 'named'}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + # Handle SQLCipher URLs + if DB_URL and DB_URL.startswith('sqlite+sqlcipher://'): + if not DATABASE_PASSWORD or DATABASE_PASSWORD.strip() == '': + raise ValueError('DATABASE_PASSWORD is required when using sqlite+sqlcipher:// URLs') + + # Extract database path from SQLCipher URL + db_path = DB_URL.replace('sqlite+sqlcipher://', '') + if db_path.startswith('/'): + db_path = db_path[1:] # Remove leading slash for relative paths + + # Create a custom creator function that uses sqlcipher3 + def create_sqlcipher_connection(): + import sqlcipher3 + + conn = sqlcipher3.connect(db_path, check_same_thread=False) + conn.execute(f"PRAGMA key = '{DATABASE_PASSWORD}'") + return conn + + connectable = create_engine( + 'sqlite://', # Dummy URL since we're using creator + creator=create_sqlcipher_connection, + echo=False, + ) + else: + # Standard database connection (existing logic) + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix='sqlalchemy.', + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/open_webui/migrations/script.py.mako b/backend/open_webui/migrations/script.py.mako new file mode 100644 index 0000000000000000000000000000000000000000..bcf5567fd6fc395a16232863276022fe0ad6bb2f --- /dev/null +++ b/backend/open_webui/migrations/script.py.mako @@ -0,0 +1,27 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import open_webui.internal.db +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/backend/open_webui/migrations/util.py b/backend/open_webui/migrations/util.py new file mode 100644 index 0000000000000000000000000000000000000000..6ea2a5f4bbb7c90fe4ccad2cfca00abbe00a500e --- /dev/null +++ b/backend/open_webui/migrations/util.py @@ -0,0 +1,15 @@ +from alembic import op +from sqlalchemy import Inspector + + +def get_existing_tables(): + con = op.get_bind() + inspector = Inspector.from_engine(con) + tables = set(inspector.get_table_names()) + return tables + + +def get_revision_id(): + import uuid + + return str(uuid.uuid4()).replace('-', '')[:12] diff --git a/backend/open_webui/migrations/versions/018012973d35_add_indexes.py b/backend/open_webui/migrations/versions/018012973d35_add_indexes.py new file mode 100644 index 0000000000000000000000000000000000000000..c5016e1a8b133a5ca5f0ca61f8059c77308d4622 --- /dev/null +++ b/backend/open_webui/migrations/versions/018012973d35_add_indexes.py @@ -0,0 +1,46 @@ +"""Add indexes + +Revision ID: 018012973d35 +Revises: d31026856c01 +Create Date: 2025-08-13 03:00:00.000000 + +""" + +from alembic import op +import sqlalchemy as sa + +revision = '018012973d35' +down_revision = 'd31026856c01' +branch_labels = None +depends_on = None + + +def upgrade(): + # Chat table indexes + op.create_index('folder_id_idx', 'chat', ['folder_id']) + op.create_index('user_id_pinned_idx', 'chat', ['user_id', 'pinned']) + op.create_index('user_id_archived_idx', 'chat', ['user_id', 'archived']) + op.create_index('updated_at_user_id_idx', 'chat', ['updated_at', 'user_id']) + op.create_index('folder_id_user_id_idx', 'chat', ['folder_id', 'user_id']) + + # Tag table index + op.create_index('user_id_idx', 'tag', ['user_id']) + + # Function table index + op.create_index('is_global_idx', 'function', ['is_global']) + + +def downgrade(): + # Chat table indexes + op.drop_index('folder_id_idx', table_name='chat') + op.drop_index('user_id_pinned_idx', table_name='chat') + op.drop_index('user_id_archived_idx', table_name='chat') + op.drop_index('updated_at_user_id_idx', table_name='chat') + op.drop_index('folder_id_user_id_idx', table_name='chat') + + # Tag table index + op.drop_index('user_id_idx', table_name='tag') + + # Function table index + + op.drop_index('is_global_idx', table_name='function') diff --git a/backend/open_webui/migrations/versions/1af9b942657b_migrate_tags.py b/backend/open_webui/migrations/versions/1af9b942657b_migrate_tags.py new file mode 100644 index 0000000000000000000000000000000000000000..caffb7e3b4cca63ab71582baab407b0af4f2334a --- /dev/null +++ b/backend/open_webui/migrations/versions/1af9b942657b_migrate_tags.py @@ -0,0 +1,140 @@ +"""Migrate tags + +Revision ID: 1af9b942657b +Revises: 242a2047eae0 +Create Date: 2024-10-09 21:02:35.241684 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.sql import table, select, update, column +from sqlalchemy.engine.reflection import Inspector + +import json + +revision = '1af9b942657b' +down_revision = '242a2047eae0' +branch_labels = None +depends_on = None + + +def upgrade(): + # Setup an inspection on the existing table to avoid issues + conn = op.get_bind() + inspector = Inspector.from_engine(conn) + + # Clean up potential leftover temp table from previous failures + conn.execute(sa.text('DROP TABLE IF EXISTS _alembic_tmp_tag')) + + # Check if the 'tag' table exists + tables = inspector.get_table_names() + + # Step 1: Modify Tag table using batch mode for SQLite support + if 'tag' in tables: + # Get the current columns in the 'tag' table + columns = [col['name'] for col in inspector.get_columns('tag')] + + # Get any existing unique constraints on the 'tag' table + current_constraints = inspector.get_unique_constraints('tag') + + with op.batch_alter_table('tag', schema=None) as batch_op: + # Check if the unique constraint already exists + if not any(constraint['name'] == 'uq_id_user_id' for constraint in current_constraints): + # Create unique constraint if it doesn't exist + batch_op.create_unique_constraint('uq_id_user_id', ['id', 'user_id']) + + # Check if the 'data' column exists before trying to drop it + if 'data' in columns: + batch_op.drop_column('data') + + # Check if the 'meta' column needs to be created + if 'meta' not in columns: + # Add the 'meta' column if it doesn't already exist + batch_op.add_column(sa.Column('meta', sa.JSON(), nullable=True)) + + tag = table( + 'tag', + column('id', sa.String()), + column('name', sa.String()), + column('user_id', sa.String()), + column('meta', sa.JSON()), + ) + + # Step 2: Migrate tags + conn = op.get_bind() + result = conn.execute(sa.select(tag.c.id, tag.c.name, tag.c.user_id)) + + tag_updates = {} + for row in result: + new_id = row.name.replace(' ', '_').lower() + tag_updates[row.id] = new_id + + for tag_id, new_tag_id in tag_updates.items(): + print(f'Updating tag {tag_id} to {new_tag_id}') + if new_tag_id == 'pinned': + # delete tag + delete_stmt = sa.delete(tag).where(tag.c.id == tag_id) + conn.execute(delete_stmt) + else: + # Check if the new_tag_id already exists in the database + existing_tag_query = sa.select(tag.c.id).where(tag.c.id == new_tag_id) + existing_tag_result = conn.execute(existing_tag_query).fetchone() + + if existing_tag_result: + # Handle duplicate case: the new_tag_id already exists + print(f'Tag {new_tag_id} already exists. Removing current tag with ID {tag_id} to avoid duplicates.') + # Option 1: Delete the current tag if an update to new_tag_id would cause duplication + delete_stmt = sa.delete(tag).where(tag.c.id == tag_id) + conn.execute(delete_stmt) + else: + update_stmt = sa.update(tag).where(tag.c.id == tag_id) + update_stmt = update_stmt.values(id=new_tag_id) + conn.execute(update_stmt) + + # Add columns `pinned` and `meta` to 'chat' + op.add_column('chat', sa.Column('pinned', sa.Boolean(), nullable=True)) + op.add_column('chat', sa.Column('meta', sa.JSON(), nullable=False, server_default='{}')) + + chatidtag = table('chatidtag', column('chat_id', sa.String()), column('tag_name', sa.String())) + chat = table( + 'chat', + column('id', sa.String()), + column('pinned', sa.Boolean()), + column('meta', sa.JSON()), + ) + + # Fetch existing tags + conn = op.get_bind() + result = conn.execute(sa.select(chatidtag.c.chat_id, chatidtag.c.tag_name)) + + chat_updates = {} + for row in result: + chat_id = row.chat_id + tag_name = row.tag_name.replace(' ', '_').lower() + + if tag_name == 'pinned': + # Specifically handle 'pinned' tag + if chat_id not in chat_updates: + chat_updates[chat_id] = {'pinned': True, 'meta': {}} + else: + chat_updates[chat_id]['pinned'] = True + else: + if chat_id not in chat_updates: + chat_updates[chat_id] = {'pinned': False, 'meta': {'tags': [tag_name]}} + else: + tags = chat_updates[chat_id]['meta'].get('tags', []) + tags.append(tag_name) + + chat_updates[chat_id]['meta']['tags'] = list(set(tags)) + + # Update chats based on accumulated changes + for chat_id, updates in chat_updates.items(): + update_stmt = sa.update(chat).where(chat.c.id == chat_id) + update_stmt = update_stmt.values(meta=updates.get('meta', {}), pinned=updates.get('pinned', False)) + conn.execute(update_stmt) + pass + + +def downgrade(): + pass diff --git a/backend/open_webui/migrations/versions/242a2047eae0_update_chat_table.py b/backend/open_webui/migrations/versions/242a2047eae0_update_chat_table.py new file mode 100644 index 0000000000000000000000000000000000000000..7fadb05a9255ce8621c46016b844a0ad82b99657 --- /dev/null +++ b/backend/open_webui/migrations/versions/242a2047eae0_update_chat_table.py @@ -0,0 +1,97 @@ +"""Update chat table + +Revision ID: 242a2047eae0 +Revises: 6a39f3d8e55c +Create Date: 2024-10-09 21:02:35.241684 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.sql import table, select, update + +import json + +revision = '242a2047eae0' +down_revision = '6a39f3d8e55c' +branch_labels = None +depends_on = None + + +def upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + + columns = inspector.get_columns('chat') + column_dict = {col['name']: col for col in columns} + + chat_column = column_dict.get('chat') + old_chat_exists = 'old_chat' in column_dict + + if chat_column: + if isinstance(chat_column['type'], sa.Text): + print("Converting 'chat' column to JSON") + + if old_chat_exists: + print("Dropping old 'old_chat' column") + op.drop_column('chat', 'old_chat') + + # Step 1: Rename current 'chat' column to 'old_chat' + print("Renaming 'chat' column to 'old_chat'") + op.alter_column('chat', 'chat', new_column_name='old_chat', existing_type=sa.Text()) + + # Step 2: Add new 'chat' column of type JSON + print("Adding new 'chat' column of type JSON") + op.add_column('chat', sa.Column('chat', sa.JSON(), nullable=True)) + else: + # If the column is already JSON, no need to do anything + pass + + # Step 3: Migrate data from 'old_chat' to 'chat' + chat_table = table( + 'chat', + sa.Column('id', sa.String(), primary_key=True), + sa.Column('old_chat', sa.Text()), + sa.Column('chat', sa.JSON()), + ) + + # - Selecting all data from the table + connection = op.get_bind() + results = connection.execute(select(chat_table.c.id, chat_table.c.old_chat)) + for row in results: + try: + # Convert text JSON to actual JSON object, assuming the text is in JSON format + json_data = json.loads(row.old_chat) + except json.JSONDecodeError: + json_data = None # Handle cases where the text cannot be converted to JSON + + connection.execute(sa.update(chat_table).where(chat_table.c.id == row.id).values(chat=json_data)) + + # Step 4: Drop 'old_chat' column + print("Dropping 'old_chat' column") + op.drop_column('chat', 'old_chat') + + +def downgrade(): + # Step 1: Add 'old_chat' column back as Text + op.add_column('chat', sa.Column('old_chat', sa.Text(), nullable=True)) + + # Step 2: Convert 'chat' JSON data back to text and store in 'old_chat' + chat_table = table( + 'chat', + sa.Column('id', sa.String(), primary_key=True), + sa.Column('chat', sa.JSON()), + sa.Column('old_chat', sa.Text()), + ) + + connection = op.get_bind() + results = connection.execute(select(chat_table.c.id, chat_table.c.chat)) + for row in results: + text_data = json.dumps(row.chat) if row.chat is not None else None + connection.execute(sa.update(chat_table).where(chat_table.c.id == row.id).values(old_chat=text_data)) + + # Step 3: Remove the new 'chat' JSON column + op.drop_column('chat', 'chat') + + # Step 4: Rename 'old_chat' back to 'chat' + op.alter_column('chat', 'old_chat', new_column_name='chat', existing_type=sa.Text()) diff --git a/backend/open_webui/migrations/versions/2f1211949ecc_update_message_and_channel_member_table.py b/backend/open_webui/migrations/versions/2f1211949ecc_update_message_and_channel_member_table.py new file mode 100644 index 0000000000000000000000000000000000000000..51a8e329f1c68d0cfe918b6b054cd656b2cc7a88 --- /dev/null +++ b/backend/open_webui/migrations/versions/2f1211949ecc_update_message_and_channel_member_table.py @@ -0,0 +1,94 @@ +"""Update messages and channel member table + +Revision ID: 2f1211949ecc +Revises: 37f288994c47 +Create Date: 2025-11-27 03:07:56.200231 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import open_webui.internal.db + +# revision identifiers, used by Alembic. +revision: str = '2f1211949ecc' +down_revision: Union[str, None] = '37f288994c47' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # New columns to be added to channel_member table + op.add_column('channel_member', sa.Column('status', sa.Text(), nullable=True)) + op.add_column( + 'channel_member', + sa.Column( + 'is_active', + sa.Boolean(), + nullable=False, + default=True, + server_default=sa.sql.expression.true(), + ), + ) + + op.add_column( + 'channel_member', + sa.Column( + 'is_channel_muted', + sa.Boolean(), + nullable=False, + default=False, + server_default=sa.sql.expression.false(), + ), + ) + op.add_column( + 'channel_member', + sa.Column( + 'is_channel_pinned', + sa.Boolean(), + nullable=False, + default=False, + server_default=sa.sql.expression.false(), + ), + ) + + op.add_column('channel_member', sa.Column('data', sa.JSON(), nullable=True)) + op.add_column('channel_member', sa.Column('meta', sa.JSON(), nullable=True)) + + op.add_column('channel_member', sa.Column('joined_at', sa.BigInteger(), nullable=False)) + op.add_column('channel_member', sa.Column('left_at', sa.BigInteger(), nullable=True)) + + op.add_column('channel_member', sa.Column('last_read_at', sa.BigInteger(), nullable=True)) + + op.add_column('channel_member', sa.Column('updated_at', sa.BigInteger(), nullable=True)) + + # New columns to be added to message table + op.add_column( + 'message', + sa.Column( + 'is_pinned', + sa.Boolean(), + nullable=False, + default=False, + server_default=sa.sql.expression.false(), + ), + ) + op.add_column('message', sa.Column('pinned_at', sa.BigInteger(), nullable=True)) + op.add_column('message', sa.Column('pinned_by', sa.Text(), nullable=True)) + + +def downgrade() -> None: + op.drop_column('channel_member', 'updated_at') + op.drop_column('channel_member', 'last_read_at') + + op.drop_column('channel_member', 'meta') + op.drop_column('channel_member', 'data') + + op.drop_column('channel_member', 'is_channel_pinned') + op.drop_column('channel_member', 'is_channel_muted') + + op.drop_column('message', 'pinned_by') + op.drop_column('message', 'pinned_at') + op.drop_column('message', 'is_pinned') diff --git a/backend/open_webui/migrations/versions/374d2f66af06_add_prompt_history_table.py b/backend/open_webui/migrations/versions/374d2f66af06_add_prompt_history_table.py new file mode 100644 index 0000000000000000000000000000000000000000..c412107032bd88fa23525461169f32a1af92d8c4 --- /dev/null +++ b/backend/open_webui/migrations/versions/374d2f66af06_add_prompt_history_table.py @@ -0,0 +1,245 @@ +"""Add prompt history table + +Revision ID: 374d2f66af06 +Revises: c440947495f3 +Create Date: 2026-01-23 17:15:00.000000 + +""" + +from typing import Sequence, Union +import uuid + +from alembic import op +import sqlalchemy as sa + +revision: str = '374d2f66af06' +down_revision: Union[str, None] = 'c440947495f3' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + conn = op.get_bind() + + # Step 1: Read existing data from OLD table (schema likely command as PK) + # We use batch_alter previously, but we want to move to new table. + # We need to assume the OLD structure. + + old_prompt_table = sa.table( + 'prompt', + sa.column('command', sa.Text()), + sa.column('user_id', sa.Text()), + sa.column('title', sa.Text()), + sa.column('content', sa.Text()), + sa.column('timestamp', sa.BigInteger()), + sa.column('access_control', sa.JSON()), + ) + + # Check if table exists/read data + try: + existing_prompts = conn.execute( + sa.select( + old_prompt_table.c.command, + old_prompt_table.c.user_id, + old_prompt_table.c.title, + old_prompt_table.c.content, + old_prompt_table.c.timestamp, + old_prompt_table.c.access_control, + ) + ).fetchall() + except Exception: + # Fallback if table doesn't exist (new install) + existing_prompts = [] + + # Step 2: Create new prompt table with 'id' as PRIMARY KEY + op.create_table( + 'prompt_new', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('command', sa.String(), unique=True, index=True), + sa.Column('user_id', sa.String(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('access_control', sa.JSON(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False, server_default='1'), + sa.Column('version_id', sa.Text(), nullable=True), + sa.Column('tags', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + ) + + # Step 3: Create prompt_history table + op.create_table( + 'prompt_history', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('prompt_id', sa.Text(), nullable=False, index=True), + sa.Column('parent_id', sa.Text(), nullable=True), + sa.Column('snapshot', sa.JSON(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('commit_message', sa.Text(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + ) + + # Step 4: Migrate data + prompt_new_table = sa.table( + 'prompt_new', + sa.column('id', sa.Text()), + sa.column('command', sa.String()), + sa.column('user_id', sa.String()), + sa.column('name', sa.Text()), + sa.column('content', sa.Text()), + sa.column('data', sa.JSON()), + sa.column('meta', sa.JSON()), + sa.column('access_control', sa.JSON()), + sa.column('is_active', sa.Boolean()), + sa.column('version_id', sa.Text()), + sa.column('tags', sa.JSON()), + sa.column('created_at', sa.BigInteger()), + sa.column('updated_at', sa.BigInteger()), + ) + + prompt_history_table = sa.table( + 'prompt_history', + sa.column('id', sa.Text()), + sa.column('prompt_id', sa.Text()), + sa.column('parent_id', sa.Text()), + sa.column('snapshot', sa.JSON()), + sa.column('user_id', sa.Text()), + sa.column('commit_message', sa.Text()), + sa.column('created_at', sa.BigInteger()), + ) + + for row in existing_prompts: + command = row[0] + user_id = row[1] + title = row[2] + content = row[3] + timestamp = row[4] + access_control = row[5] + + new_uuid = str(uuid.uuid4()) + history_uuid = str(uuid.uuid4()) + clean_command = command[1:] if command and command.startswith('/') else command + + # Insert into prompt_new + conn.execute( + sa.insert(prompt_new_table).values( + id=new_uuid, + command=clean_command, + user_id=user_id, + name=title, + content=content, + data={}, + meta={}, + access_control=access_control, + is_active=True, + version_id=history_uuid, + tags=[], + created_at=timestamp, + updated_at=timestamp, + ) + ) + + # Create initial history entry + conn.execute( + sa.insert(prompt_history_table).values( + id=history_uuid, + prompt_id=new_uuid, + parent_id=None, + snapshot={ + 'name': title, + 'content': content, + 'command': clean_command, + 'data': {}, + 'meta': {}, + 'access_control': access_control, + }, + user_id=user_id, + commit_message=None, + created_at=timestamp, + ) + ) + + # Step 5: Replace old table with new one + op.drop_table('prompt') + op.rename_table('prompt_new', 'prompt') + + +def downgrade() -> None: + conn = op.get_bind() + + # Step 1: Read new data + prompt_table = sa.table( + 'prompt', + sa.column('command', sa.String()), + sa.column('name', sa.Text()), + sa.column('created_at', sa.BigInteger()), + sa.column('user_id', sa.Text()), + sa.column('content', sa.Text()), + sa.column('access_control', sa.JSON()), + ) + + try: + current_data = conn.execute( + sa.select( + prompt_table.c.command, + prompt_table.c.name, + prompt_table.c.created_at, + prompt_table.c.user_id, + prompt_table.c.content, + prompt_table.c.access_control, + ) + ).fetchall() + except Exception: + current_data = [] + + # Step 2: Drop history and table + op.drop_table('prompt_history') + op.drop_table('prompt') + + # Step 3: Recreate old table (command as PK?) + # Assuming old schema: + op.create_table( + 'prompt', + sa.Column('command', sa.String(), primary_key=True), + sa.Column('user_id', sa.String()), + sa.Column('title', sa.Text()), + sa.Column('content', sa.Text()), + sa.Column('timestamp', sa.BigInteger()), + sa.Column('access_control', sa.JSON()), + sa.Column('id', sa.Integer(), nullable=True), + ) + + # Step 4: Restore data + old_prompt_table = sa.table( + 'prompt', + sa.column('command', sa.String()), + sa.column('user_id', sa.String()), + sa.column('title', sa.Text()), + sa.column('content', sa.Text()), + sa.column('timestamp', sa.BigInteger()), + sa.column('access_control', sa.JSON()), + ) + + for row in current_data: + command = row[0] + name = row[1] + created_at = row[2] + user_id = row[3] + content = row[4] + access_control = row[5] + + # Restore leading / + old_command = '/' + command if command and not command.startswith('/') else command + + conn.execute( + sa.insert(old_prompt_table).values( + command=old_command, + user_id=user_id, + title=name, + content=content, + timestamp=created_at, + access_control=access_control, + ) + ) diff --git a/backend/open_webui/migrations/versions/3781e22d8b01_update_message_table.py b/backend/open_webui/migrations/versions/3781e22d8b01_update_message_table.py new file mode 100644 index 0000000000000000000000000000000000000000..170137f23c6129ed3ac5c25698138df1a608cb00 --- /dev/null +++ b/backend/open_webui/migrations/versions/3781e22d8b01_update_message_table.py @@ -0,0 +1,58 @@ +"""Update message & channel tables + +Revision ID: 3781e22d8b01 +Revises: 7826ab40b532 +Create Date: 2024-12-30 03:00:00.000000 + +""" + +from alembic import op +import sqlalchemy as sa + +revision = '3781e22d8b01' +down_revision = '7826ab40b532' +branch_labels = None +depends_on = None + + +def upgrade(): + # Add 'type' column to the 'channel' table + op.add_column( + 'channel', + sa.Column( + 'type', + sa.Text(), + nullable=True, + ), + ) + + # Add 'parent_id' column to the 'message' table for threads + op.add_column( + 'message', + sa.Column('parent_id', sa.Text(), nullable=True), + ) + + op.create_table( + 'message_reaction', + sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True), # Unique reaction ID + sa.Column('user_id', sa.Text(), nullable=False), # User who reacted + sa.Column('message_id', sa.Text(), nullable=False), # Message that was reacted to + sa.Column('name', sa.Text(), nullable=False), # Reaction name (e.g. "thumbs_up") + sa.Column('created_at', sa.BigInteger(), nullable=True), # Timestamp of when the reaction was added + ) + + op.create_table( + 'channel_member', + sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True), # Record ID for the membership row + sa.Column('channel_id', sa.Text(), nullable=False), # Associated channel + sa.Column('user_id', sa.Text(), nullable=False), # Associated user + sa.Column('created_at', sa.BigInteger(), nullable=True), # Timestamp of when the user joined the channel + ) + + +def downgrade(): + # Revert 'type' column addition to the 'channel' table + op.drop_column('channel', 'type') + op.drop_column('message', 'parent_id') + op.drop_table('message_reaction') + op.drop_table('channel_member') diff --git a/backend/open_webui/migrations/versions/37f288994c47_add_group_member_table.py b/backend/open_webui/migrations/versions/37f288994c47_add_group_member_table.py new file mode 100644 index 0000000000000000000000000000000000000000..4bf24d3b46e9e39565686408684fa2d0a06b4143 --- /dev/null +++ b/backend/open_webui/migrations/versions/37f288994c47_add_group_member_table.py @@ -0,0 +1,137 @@ +"""add_group_member_table + +Revision ID: 37f288994c47 +Revises: a5c220713937 +Create Date: 2025-11-17 03:45:25.123939 + +""" + +import uuid +import time +import json +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = '37f288994c47' +down_revision: Union[str, None] = 'a5c220713937' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # 1. Create new table + op.create_table( + 'group_member', + sa.Column('id', sa.Text(), primary_key=True, unique=True, nullable=False), + sa.Column( + 'group_id', + sa.Text(), + sa.ForeignKey('group.id', ondelete='CASCADE'), + nullable=False, + ), + sa.Column( + 'user_id', + sa.Text(), + sa.ForeignKey('user.id', ondelete='CASCADE'), + nullable=False, + ), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + sa.UniqueConstraint('group_id', 'user_id', name='uq_group_member_group_user'), + ) + + connection = op.get_bind() + + # 2. Read existing group with user_ids JSON column + group_table = sa.Table( + 'group', + sa.MetaData(), + sa.Column('id', sa.Text()), + sa.Column('user_ids', sa.JSON()), # JSON stored as text in SQLite + PG + ) + + results = connection.execute(sa.select(group_table.c.id, group_table.c.user_ids)).fetchall() + + print(results) + + # 3. Insert members into group_member table + gm_table = sa.Table( + 'group_member', + sa.MetaData(), + sa.Column('id', sa.Text()), + sa.Column('group_id', sa.Text()), + sa.Column('user_id', sa.Text()), + sa.Column('created_at', sa.BigInteger()), + sa.Column('updated_at', sa.BigInteger()), + ) + + now = int(time.time()) + for group_id, user_ids in results: + if not user_ids: + continue + + if isinstance(user_ids, str): + try: + user_ids = json.loads(user_ids) + except Exception: + continue # skip invalid JSON + + if not isinstance(user_ids, list): + continue + + rows = [ + { + 'id': str(uuid.uuid4()), + 'group_id': group_id, + 'user_id': uid, + 'created_at': now, + 'updated_at': now, + } + for uid in user_ids + ] + + if rows: + connection.execute(gm_table.insert(), rows) + + # 4. Optionally drop the old column + with op.batch_alter_table('group') as batch: + batch.drop_column('user_ids') + + +def downgrade(): + # Reverse: restore user_ids column + with op.batch_alter_table('group') as batch: + batch.add_column(sa.Column('user_ids', sa.JSON())) + + connection = op.get_bind() + gm_table = sa.Table( + 'group_member', + sa.MetaData(), + sa.Column('group_id', sa.Text()), + sa.Column('user_id', sa.Text()), + sa.Column('created_at', sa.BigInteger()), + sa.Column('updated_at', sa.BigInteger()), + ) + + group_table = sa.Table( + 'group', + sa.MetaData(), + sa.Column('id', sa.Text()), + sa.Column('user_ids', sa.JSON()), + ) + + # Build JSON arrays again + results = connection.execute(sa.select(group_table.c.id)).fetchall() + + for (group_id,) in results: + members = connection.execute(sa.select(gm_table.c.user_id).where(gm_table.c.group_id == group_id)).fetchall() + + member_ids = [m[0] for m in members] + + connection.execute(group_table.update().where(group_table.c.id == group_id).values(user_ids=member_ids)) + + # Drop the new table + op.drop_table('group_member') diff --git a/backend/open_webui/migrations/versions/38d63c18f30f_add_oauth_session_table.py b/backend/open_webui/migrations/versions/38d63c18f30f_add_oauth_session_table.py new file mode 100644 index 0000000000000000000000000000000000000000..d415f500f3d13836eb85b8bd688e34880efd8059 --- /dev/null +++ b/backend/open_webui/migrations/versions/38d63c18f30f_add_oauth_session_table.py @@ -0,0 +1,75 @@ +"""Add oauth_session table + +Revision ID: 38d63c18f30f +Revises: 3af16a1c9fb6 +Create Date: 2025-09-08 14:19:59.583921 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = '38d63c18f30f' +down_revision: Union[str, None] = '3af16a1c9fb6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Ensure 'id' column in 'user' table is unique and primary key (ForeignKey constraint) + inspector = sa.inspect(op.get_bind()) + columns = inspector.get_columns('user') + + pk_columns = inspector.get_pk_constraint('user')['constrained_columns'] + id_column = next((col for col in columns if col['name'] == 'id'), None) + + if id_column and not id_column.get('unique', False): + unique_constraints = inspector.get_unique_constraints('user') + unique_columns = {tuple(u['column_names']) for u in unique_constraints} + + with op.batch_alter_table('user') as batch_op: + # If primary key is wrong, drop it + if pk_columns and pk_columns != ['id']: + batch_op.drop_constraint(inspector.get_pk_constraint('user')['name'], type_='primary') + + # Add unique constraint if missing + if ('id',) not in unique_columns: + batch_op.create_unique_constraint('uq_user_id', ['id']) + + # Re-create correct primary key + batch_op.create_primary_key('pk_user_id', ['id']) + + # Create oauth_session table + op.create_table( + 'oauth_session', + sa.Column('id', sa.Text(), primary_key=True, nullable=False, unique=True), + sa.Column( + 'user_id', + sa.Text(), + sa.ForeignKey('user.id', ondelete='CASCADE'), + nullable=False, + ), + sa.Column('provider', sa.Text(), nullable=False), + sa.Column('token', sa.Text(), nullable=False), + sa.Column('expires_at', sa.BigInteger(), nullable=False), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + ) + + # Create indexes for better performance + op.create_index('idx_oauth_session_user_id', 'oauth_session', ['user_id']) + op.create_index('idx_oauth_session_expires_at', 'oauth_session', ['expires_at']) + op.create_index('idx_oauth_session_user_provider', 'oauth_session', ['user_id', 'provider']) + + +def downgrade() -> None: + # Drop indexes first + op.drop_index('idx_oauth_session_user_provider', table_name='oauth_session') + op.drop_index('idx_oauth_session_expires_at', table_name='oauth_session') + op.drop_index('idx_oauth_session_user_id', table_name='oauth_session') + + # Drop the table + op.drop_table('oauth_session') diff --git a/backend/open_webui/migrations/versions/3ab32c4b8f59_update_tags.py b/backend/open_webui/migrations/versions/3ab32c4b8f59_update_tags.py new file mode 100644 index 0000000000000000000000000000000000000000..31bd355ede126e5c06d0a972942b40d8e38f5dd5 --- /dev/null +++ b/backend/open_webui/migrations/versions/3ab32c4b8f59_update_tags.py @@ -0,0 +1,78 @@ +"""Update tags + +Revision ID: 3ab32c4b8f59 +Revises: 1af9b942657b +Create Date: 2024-10-09 21:02:35.241684 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.sql import table, select, update, column +from sqlalchemy.engine.reflection import Inspector + +import json + +revision = '3ab32c4b8f59' +down_revision = '1af9b942657b' +branch_labels = None +depends_on = None + + +def upgrade(): + conn = op.get_bind() + inspector = Inspector.from_engine(conn) + + # Inspecting the 'tag' table constraints and structure + existing_pk = inspector.get_pk_constraint('tag') + unique_constraints = inspector.get_unique_constraints('tag') + existing_indexes = inspector.get_indexes('tag') + + print(f'Primary Key: {existing_pk}') + print(f'Unique Constraints: {unique_constraints}') + print(f'Indexes: {existing_indexes}') + + with op.batch_alter_table('tag', schema=None) as batch_op: + # Drop existing primary key constraint if it exists + if existing_pk and existing_pk.get('constrained_columns'): + pk_name = existing_pk.get('name') + if pk_name: + print(f'Dropping primary key constraint: {pk_name}') + batch_op.drop_constraint(pk_name, type_='primary') + + # Now create the new primary key with the combination of 'id' and 'user_id' + print("Creating new primary key with 'id' and 'user_id'.") + batch_op.create_primary_key('pk_id_user_id', ['id', 'user_id']) + + # Drop unique constraints that could conflict with the new primary key + for constraint in unique_constraints: + if ( + constraint['name'] == 'uq_id_user_id' + ): # Adjust this name according to what is actually returned by the inspector + print(f'Dropping unique constraint: {constraint["name"]}') + batch_op.drop_constraint(constraint['name'], type_='unique') + + for index in existing_indexes: + if index['unique']: + if not any(constraint['name'] == index['name'] for constraint in unique_constraints): + # You are attempting to drop unique indexes + print(f'Dropping unique index: {index["name"]}') + batch_op.drop_index(index['name']) + + +def downgrade(): + conn = op.get_bind() + inspector = Inspector.from_engine(conn) + + current_pk = inspector.get_pk_constraint('tag') + + with op.batch_alter_table('tag', schema=None) as batch_op: + # Drop the current primary key first, if it matches the one we know we added in upgrade + if current_pk and 'pk_id_user_id' == current_pk.get('name'): + batch_op.drop_constraint('pk_id_user_id', type_='primary') + + # Restore the original primary key + batch_op.create_primary_key('pk_id', ['id']) + + # Since primary key on just 'id' is restored, we now add back any unique constraints if necessary + batch_op.create_unique_constraint('uq_id_user_id', ['id', 'user_id']) diff --git a/backend/open_webui/migrations/versions/3af16a1c9fb6_update_user_table.py b/backend/open_webui/migrations/versions/3af16a1c9fb6_update_user_table.py new file mode 100644 index 0000000000000000000000000000000000000000..629c1c8c24288c0c34db8ed5fccfdaa8defaebdc --- /dev/null +++ b/backend/open_webui/migrations/versions/3af16a1c9fb6_update_user_table.py @@ -0,0 +1,32 @@ +"""update user table + +Revision ID: 3af16a1c9fb6 +Revises: 018012973d35 +Create Date: 2025-08-21 02:07:18.078283 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = '3af16a1c9fb6' +down_revision: Union[str, None] = '018012973d35' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column('user', sa.Column('username', sa.String(length=50), nullable=True)) + op.add_column('user', sa.Column('bio', sa.Text(), nullable=True)) + op.add_column('user', sa.Column('gender', sa.Text(), nullable=True)) + op.add_column('user', sa.Column('date_of_birth', sa.Date(), nullable=True)) + + +def downgrade() -> None: + op.drop_column('user', 'username') + op.drop_column('user', 'bio') + op.drop_column('user', 'gender') + op.drop_column('user', 'date_of_birth') diff --git a/backend/open_webui/migrations/versions/3e0e00844bb0_add_knowledge_file_table.py b/backend/open_webui/migrations/versions/3e0e00844bb0_add_knowledge_file_table.py new file mode 100644 index 0000000000000000000000000000000000000000..f772987a448f0c5c4aa373e7022b5083fad7c1b5 --- /dev/null +++ b/backend/open_webui/migrations/versions/3e0e00844bb0_add_knowledge_file_table.py @@ -0,0 +1,161 @@ +"""Add knowledge_file table + +Revision ID: 3e0e00844bb0 +Revises: 90ef40d4714e +Create Date: 2025-12-02 06:54:19.401334 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy import inspect +import open_webui.internal.db + +import time +import json +import uuid + +# revision identifiers, used by Alembic. +revision: str = '3e0e00844bb0' +down_revision: Union[str, None] = '90ef40d4714e' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + 'knowledge_file', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column( + 'knowledge_id', + sa.Text(), + sa.ForeignKey('knowledge.id', ondelete='CASCADE'), + nullable=False, + ), + sa.Column( + 'file_id', + sa.Text(), + sa.ForeignKey('file.id', ondelete='CASCADE'), + nullable=False, + ), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + # indexes + sa.Index('ix_knowledge_file_knowledge_id', 'knowledge_id'), + sa.Index('ix_knowledge_file_file_id', 'file_id'), + sa.Index('ix_knowledge_file_user_id', 'user_id'), + # unique constraints + sa.UniqueConstraint( + 'knowledge_id', 'file_id', name='uq_knowledge_file_knowledge_file' + ), # prevent duplicate entries + ) + + connection = op.get_bind() + + # 2. Read existing group with user_ids JSON column + knowledge_table = sa.Table( + 'knowledge', + sa.MetaData(), + sa.Column('id', sa.Text()), + sa.Column('user_id', sa.Text()), + sa.Column('data', sa.JSON()), # JSON stored as text in SQLite + PG + ) + + results = connection.execute( + sa.select(knowledge_table.c.id, knowledge_table.c.user_id, knowledge_table.c.data) + ).fetchall() + + # 3. Insert members into group_member table + kf_table = sa.Table( + 'knowledge_file', + sa.MetaData(), + sa.Column('id', sa.Text()), + sa.Column('user_id', sa.Text()), + sa.Column('knowledge_id', sa.Text()), + sa.Column('file_id', sa.Text()), + sa.Column('created_at', sa.BigInteger()), + sa.Column('updated_at', sa.BigInteger()), + ) + + file_table = sa.Table( + 'file', + sa.MetaData(), + sa.Column('id', sa.Text()), + ) + + now = int(time.time()) + for knowledge_id, user_id, data in results: + if not data: + continue + + if isinstance(data, str): + try: + data = json.loads(data) + except Exception: + continue # skip invalid JSON + + if not isinstance(data, dict): + continue + + file_ids = data.get('file_ids', []) + + for file_id in file_ids: + file_exists = connection.execute(sa.select(file_table.c.id).where(file_table.c.id == file_id)).fetchone() + + if not file_exists: + continue # skip non-existing files + + row = { + 'id': str(uuid.uuid4()), + 'user_id': user_id, + 'knowledge_id': knowledge_id, + 'file_id': file_id, + 'created_at': now, + 'updated_at': now, + } + connection.execute(kf_table.insert().values(**row)) + + with op.batch_alter_table('knowledge') as batch: + batch.drop_column('data') + + +def downgrade() -> None: + # 1. Add back the old data column + op.add_column('knowledge', sa.Column('data', sa.JSON(), nullable=True)) + + connection = op.get_bind() + + # 2. Read knowledge_file entries and reconstruct data JSON + knowledge_table = sa.Table( + 'knowledge', + sa.MetaData(), + sa.Column('id', sa.Text()), + sa.Column('data', sa.JSON()), + ) + + kf_table = sa.Table( + 'knowledge_file', + sa.MetaData(), + sa.Column('id', sa.Text()), + sa.Column('knowledge_id', sa.Text()), + sa.Column('file_id', sa.Text()), + ) + + results = connection.execute(sa.select(knowledge_table.c.id)).fetchall() + + for (knowledge_id,) in results: + file_ids = connection.execute( + sa.select(kf_table.c.file_id).where(kf_table.c.knowledge_id == knowledge_id) + ).fetchall() + + file_ids_list = [fid for (fid,) in file_ids] + + data_json = {'file_ids': file_ids_list} + + connection.execute(knowledge_table.update().where(knowledge_table.c.id == knowledge_id).values(data=data_json)) + + # 3. Drop the knowledge_file table + op.drop_table('knowledge_file') diff --git a/backend/open_webui/migrations/versions/4ace53fd72c8_update_folder_table_datetime.py b/backend/open_webui/migrations/versions/4ace53fd72c8_update_folder_table_datetime.py new file mode 100644 index 0000000000000000000000000000000000000000..91e0dce0be33ec21a538b9602c9c64e7b734fdb6 --- /dev/null +++ b/backend/open_webui/migrations/versions/4ace53fd72c8_update_folder_table_datetime.py @@ -0,0 +1,67 @@ +"""Update folder table and change DateTime to BigInteger for timestamp fields + +Revision ID: 4ace53fd72c8 +Revises: af906e964978 +Create Date: 2024-10-23 03:00:00.000000 + +""" + +from alembic import op +import sqlalchemy as sa + +revision = '4ace53fd72c8' +down_revision = 'af906e964978' +branch_labels = None +depends_on = None + + +def upgrade(): + # Perform safe alterations using batch operation + with op.batch_alter_table('folder', schema=None) as batch_op: + # Step 1: Remove server defaults for created_at and updated_at + batch_op.alter_column( + 'created_at', + server_default=None, # Removing server default + ) + batch_op.alter_column( + 'updated_at', + server_default=None, # Removing server default + ) + + # Step 2: Change the column types to BigInteger for created_at + batch_op.alter_column( + 'created_at', + type_=sa.BigInteger(), + existing_type=sa.DateTime(), + existing_nullable=False, + postgresql_using='extract(epoch from created_at)::bigint', # Conversion for PostgreSQL + ) + + # Change the column types to BigInteger for updated_at + batch_op.alter_column( + 'updated_at', + type_=sa.BigInteger(), + existing_type=sa.DateTime(), + existing_nullable=False, + postgresql_using='extract(epoch from updated_at)::bigint', # Conversion for PostgreSQL + ) + + +def downgrade(): + # Downgrade: Convert columns back to DateTime and restore defaults + with op.batch_alter_table('folder', schema=None) as batch_op: + batch_op.alter_column( + 'created_at', + type_=sa.DateTime(), + existing_type=sa.BigInteger(), + existing_nullable=False, + server_default=sa.func.now(), # Restoring server default on downgrade + ) + batch_op.alter_column( + 'updated_at', + type_=sa.DateTime(), + existing_type=sa.BigInteger(), + existing_nullable=False, + server_default=sa.func.now(), # Restoring server default on downgrade + onupdate=sa.func.now(), # Restoring onupdate behavior if it was there + ) diff --git a/backend/open_webui/migrations/versions/56359461a091_add_calendar_tables.py b/backend/open_webui/migrations/versions/56359461a091_add_calendar_tables.py new file mode 100644 index 0000000000000000000000000000000000000000..e556440f56f58798c50f5bda753bfa878d1314f1 --- /dev/null +++ b/backend/open_webui/migrations/versions/56359461a091_add_calendar_tables.py @@ -0,0 +1,83 @@ +"""add calendar tables + +Revision ID: 56359461a091 +Revises: c1d2e3f4a5b6 +Create Date: 2026-04-19 16:20:58.162045 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '56359461a091' +down_revision: Union[str, None] = 'c1d2e3f4a5b6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + 'calendar', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('color', sa.Text(), nullable=True), + sa.Column('is_default', sa.Boolean(), nullable=False), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index('ix_calendar_user', 'calendar', ['user_id'], unique=False) + + op.create_table( + 'calendar_event', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('calendar_id', sa.Text(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('title', sa.Text(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('start_at', sa.BigInteger(), nullable=False), + sa.Column('end_at', sa.BigInteger(), nullable=True), + sa.Column('all_day', sa.Boolean(), nullable=False), + sa.Column('rrule', sa.Text(), nullable=True), + sa.Column('color', sa.Text(), nullable=True), + sa.Column('location', sa.Text(), nullable=True), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('is_cancelled', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index('ix_calendar_event_calendar', 'calendar_event', ['calendar_id', 'start_at'], unique=False) + op.create_index('ix_calendar_event_user_date', 'calendar_event', ['user_id', 'start_at'], unique=False) + + op.create_table( + 'calendar_event_attendee', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('event_id', sa.Text(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('status', sa.Text(), nullable=False), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('event_id', 'user_id', name='uq_event_attendee'), + ) + op.create_index('ix_calendar_event_attendee_user', 'calendar_event_attendee', ['user_id', 'status'], unique=False) + + +def downgrade() -> None: + op.drop_index('ix_calendar_event_attendee_user', table_name='calendar_event_attendee') + op.drop_table('calendar_event_attendee') + op.drop_index('ix_calendar_event_user_date', table_name='calendar_event') + op.drop_index('ix_calendar_event_calendar', table_name='calendar_event') + op.drop_table('calendar_event') + op.drop_index('ix_calendar_user', table_name='calendar') + op.drop_table('calendar') diff --git a/backend/open_webui/migrations/versions/57c599a3cb57_add_channel_table.py b/backend/open_webui/migrations/versions/57c599a3cb57_add_channel_table.py new file mode 100644 index 0000000000000000000000000000000000000000..79f0e8827e4a3badf325ca6828869867d5624a0d --- /dev/null +++ b/backend/open_webui/migrations/versions/57c599a3cb57_add_channel_table.py @@ -0,0 +1,48 @@ +"""Add channel table + +Revision ID: 57c599a3cb57 +Revises: 922e7a387820 +Create Date: 2024-12-22 03:00:00.000000 + +""" + +from alembic import op +import sqlalchemy as sa + +revision = '57c599a3cb57' +down_revision = '922e7a387820' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + 'channel', + sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True), + sa.Column('user_id', sa.Text()), + sa.Column('name', sa.Text()), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('access_control', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + ) + + op.create_table( + 'message', + sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True), + sa.Column('user_id', sa.Text()), + sa.Column('channel_id', sa.Text(), nullable=True), + sa.Column('content', sa.Text()), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + ) + + +def downgrade(): + op.drop_table('channel') + + op.drop_table('message') diff --git a/backend/open_webui/migrations/versions/6283dc0e4d8d_add_channel_file_table.py b/backend/open_webui/migrations/versions/6283dc0e4d8d_add_channel_file_table.py new file mode 100644 index 0000000000000000000000000000000000000000..2bd2d9fd60a5efcafead97d86b255a958128d36d --- /dev/null +++ b/backend/open_webui/migrations/versions/6283dc0e4d8d_add_channel_file_table.py @@ -0,0 +1,51 @@ +"""Add channel file table + +Revision ID: 6283dc0e4d8d +Revises: 3e0e00844bb0 +Create Date: 2025-12-10 15:11:39.424601 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import open_webui.internal.db + +# revision identifiers, used by Alembic. +revision: str = '6283dc0e4d8d' +down_revision: Union[str, None] = '3e0e00844bb0' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + 'channel_file', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column( + 'channel_id', + sa.Text(), + sa.ForeignKey('channel.id', ondelete='CASCADE'), + nullable=False, + ), + sa.Column( + 'file_id', + sa.Text(), + sa.ForeignKey('file.id', ondelete='CASCADE'), + nullable=False, + ), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + # indexes + sa.Index('ix_channel_file_channel_id', 'channel_id'), + sa.Index('ix_channel_file_file_id', 'file_id'), + sa.Index('ix_channel_file_user_id', 'user_id'), + # unique constraints + sa.UniqueConstraint('channel_id', 'file_id', name='uq_channel_file_channel_file'), # prevent duplicate entries + ) + + +def downgrade() -> None: + op.drop_table('channel_file') diff --git a/backend/open_webui/migrations/versions/6a39f3d8e55c_add_knowledge_table.py b/backend/open_webui/migrations/versions/6a39f3d8e55c_add_knowledge_table.py new file mode 100644 index 0000000000000000000000000000000000000000..c65ca01415b37b0fbf7d0672ecc9396d9d767806 --- /dev/null +++ b/backend/open_webui/migrations/versions/6a39f3d8e55c_add_knowledge_table.py @@ -0,0 +1,79 @@ +"""Add knowledge table + +Revision ID: 6a39f3d8e55c +Revises: c0fbf31ca0db +Create Date: 2024-10-01 14:02:35.241684 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.sql import table, column, select +import json + +revision = '6a39f3d8e55c' +down_revision = 'c0fbf31ca0db' +branch_labels = None +depends_on = None + + +def upgrade(): + # Creating the 'knowledge' table + print('Creating knowledge table') + knowledge_table = op.create_table( + 'knowledge', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + ) + + print('Migrating data from document table to knowledge table') + # Representation of the existing 'document' table + document_table = table( + 'document', + column('collection_name', sa.String()), + column('user_id', sa.String()), + column('name', sa.String()), + column('title', sa.Text()), + column('content', sa.Text()), + column('timestamp', sa.BigInteger()), + ) + + # Select all from existing document table + documents = op.get_bind().execute( + select( + document_table.c.collection_name, + document_table.c.user_id, + document_table.c.name, + document_table.c.title, + document_table.c.content, + document_table.c.timestamp, + ) + ) + + # Insert data into knowledge table from document table + for doc in documents: + op.get_bind().execute( + knowledge_table.insert().values( + id=doc.collection_name, + user_id=doc.user_id, + description=doc.name, + meta={ + 'legacy': True, + 'document': True, + 'tags': json.loads(doc.content or '{}').get('tags', []), + }, + name=doc.title, + created_at=doc.timestamp, + updated_at=doc.timestamp, # using created_at for both created_at and updated_at in project + ) + ) + + +def downgrade(): + op.drop_table('knowledge') diff --git a/backend/open_webui/migrations/versions/7826ab40b532_update_file_table.py b/backend/open_webui/migrations/versions/7826ab40b532_update_file_table.py new file mode 100644 index 0000000000000000000000000000000000000000..4211c6642e14c38983dbfc21525e0540fd93ba07 --- /dev/null +++ b/backend/open_webui/migrations/versions/7826ab40b532_update_file_table.py @@ -0,0 +1,26 @@ +"""Update file table + +Revision ID: 7826ab40b532 +Revises: 57c599a3cb57 +Create Date: 2024-12-23 03:00:00.000000 + +""" + +from alembic import op +import sqlalchemy as sa + +revision = '7826ab40b532' +down_revision = '57c599a3cb57' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + 'file', + sa.Column('access_control', sa.JSON(), nullable=True), + ) + + +def downgrade(): + op.drop_column('file', 'access_control') diff --git a/backend/open_webui/migrations/versions/7e5b5dc7342b_init.py b/backend/open_webui/migrations/versions/7e5b5dc7342b_init.py new file mode 100644 index 0000000000000000000000000000000000000000..39f488d72eab19eeb046ecc5445a3e55a624f915 --- /dev/null +++ b/backend/open_webui/migrations/versions/7e5b5dc7342b_init.py @@ -0,0 +1,204 @@ +"""init + +Revision ID: 7e5b5dc7342b +Revises: +Create Date: 2024-06-24 13:15:33.808998 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +import open_webui.internal.db +from open_webui.internal.db import JSONField +from open_webui.migrations.util import get_existing_tables + +# revision identifiers, used by Alembic. +revision: str = '7e5b5dc7342b' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + existing_tables = set(get_existing_tables()) + + # ### commands auto generated by Alembic - please adjust! ### + if 'auth' not in existing_tables: + op.create_table( + 'auth', + sa.Column('id', sa.String(), nullable=False), + sa.Column('email', sa.String(), nullable=True), + sa.Column('password', sa.Text(), nullable=True), + sa.Column('active', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id'), + ) + + if 'chat' not in existing_tables: + op.create_table( + 'chat', + sa.Column('id', sa.String(), nullable=False), + sa.Column('user_id', sa.String(), nullable=True), + sa.Column('title', sa.Text(), nullable=True), + sa.Column('chat', sa.Text(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + sa.Column('share_id', sa.Text(), nullable=True), + sa.Column('archived', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('share_id'), + ) + + if 'chatidtag' not in existing_tables: + op.create_table( + 'chatidtag', + sa.Column('id', sa.String(), nullable=False), + sa.Column('tag_name', sa.String(), nullable=True), + sa.Column('chat_id', sa.String(), nullable=True), + sa.Column('user_id', sa.String(), nullable=True), + sa.Column('timestamp', sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint('id'), + ) + + if 'document' not in existing_tables: + op.create_table( + 'document', + sa.Column('collection_name', sa.String(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('title', sa.Text(), nullable=True), + sa.Column('filename', sa.Text(), nullable=True), + sa.Column('content', sa.Text(), nullable=True), + sa.Column('user_id', sa.String(), nullable=True), + sa.Column('timestamp', sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint('collection_name'), + sa.UniqueConstraint('name'), + ) + + if 'file' not in existing_tables: + op.create_table( + 'file', + sa.Column('id', sa.String(), nullable=False), + sa.Column('user_id', sa.String(), nullable=True), + sa.Column('filename', sa.Text(), nullable=True), + sa.Column('meta', JSONField(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint('id'), + ) + + if 'function' not in existing_tables: + op.create_table( + 'function', + sa.Column('id', sa.String(), nullable=False), + sa.Column('user_id', sa.String(), nullable=True), + sa.Column('name', sa.Text(), nullable=True), + sa.Column('type', sa.Text(), nullable=True), + sa.Column('content', sa.Text(), nullable=True), + sa.Column('meta', JSONField(), nullable=True), + sa.Column('valves', JSONField(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('is_global', sa.Boolean(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint('id'), + ) + + if 'memory' not in existing_tables: + op.create_table( + 'memory', + sa.Column('id', sa.String(), nullable=False), + sa.Column('user_id', sa.String(), nullable=True), + sa.Column('content', sa.Text(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint('id'), + ) + + if 'model' not in existing_tables: + op.create_table( + 'model', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=True), + sa.Column('base_model_id', sa.Text(), nullable=True), + sa.Column('name', sa.Text(), nullable=True), + sa.Column('params', JSONField(), nullable=True), + sa.Column('meta', JSONField(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint('id'), + ) + + if 'prompt' not in existing_tables: + op.create_table( + 'prompt', + sa.Column('command', sa.String(), nullable=False), + sa.Column('user_id', sa.String(), nullable=True), + sa.Column('title', sa.Text(), nullable=True), + sa.Column('content', sa.Text(), nullable=True), + sa.Column('timestamp', sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint('command'), + ) + + if 'tag' not in existing_tables: + op.create_table( + 'tag', + sa.Column('id', sa.String(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('user_id', sa.String(), nullable=True), + sa.Column('data', sa.Text(), nullable=True), + sa.PrimaryKeyConstraint('id'), + ) + + if 'tool' not in existing_tables: + op.create_table( + 'tool', + sa.Column('id', sa.String(), nullable=False), + sa.Column('user_id', sa.String(), nullable=True), + sa.Column('name', sa.Text(), nullable=True), + sa.Column('content', sa.Text(), nullable=True), + sa.Column('specs', JSONField(), nullable=True), + sa.Column('meta', JSONField(), nullable=True), + sa.Column('valves', JSONField(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint('id'), + ) + + if 'user' not in existing_tables: + op.create_table( + 'user', + sa.Column('id', sa.String(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('email', sa.String(), nullable=True), + sa.Column('role', sa.String(), nullable=True), + sa.Column('profile_image_url', sa.Text(), nullable=True), + sa.Column('last_active_at', sa.BigInteger(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.Column('api_key', sa.String(), nullable=True), + sa.Column('settings', JSONField(), nullable=True), + sa.Column('info', JSONField(), nullable=True), + sa.Column('oauth_sub', sa.Text(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('api_key'), + sa.UniqueConstraint('oauth_sub'), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('user') + op.drop_table('tool') + op.drop_table('tag') + op.drop_table('prompt') + op.drop_table('model') + op.drop_table('memory') + op.drop_table('function') + op.drop_table('file') + op.drop_table('document') + op.drop_table('chatidtag') + op.drop_table('chat') + op.drop_table('auth') + # ### end Alembic commands ### diff --git a/backend/open_webui/migrations/versions/81cc2ce44d79_update_channel_file_and_knowledge_table.py b/backend/open_webui/migrations/versions/81cc2ce44d79_update_channel_file_and_knowledge_table.py new file mode 100644 index 0000000000000000000000000000000000000000..e45a2443dfd00d79369b97a53c87d36a616f6f01 --- /dev/null +++ b/backend/open_webui/migrations/versions/81cc2ce44d79_update_channel_file_and_knowledge_table.py @@ -0,0 +1,46 @@ +"""Update channel file and knowledge table + +Revision ID: 81cc2ce44d79 +Revises: 6283dc0e4d8d +Create Date: 2025-12-10 16:07:58.001282 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import open_webui.internal.db + +# revision identifiers, used by Alembic. +revision: str = '81cc2ce44d79' +down_revision: Union[str, None] = '6283dc0e4d8d' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add message_id column to channel_file table + with op.batch_alter_table('channel_file', schema=None) as batch_op: + batch_op.add_column( + sa.Column( + 'message_id', + sa.Text(), + sa.ForeignKey('message.id', ondelete='CASCADE', name='fk_channel_file_message_id'), + nullable=True, + ) + ) + + # Add data column to knowledge table + with op.batch_alter_table('knowledge', schema=None) as batch_op: + batch_op.add_column(sa.Column('data', sa.JSON(), nullable=True)) + + +def downgrade() -> None: + # Remove message_id column from channel_file table + with op.batch_alter_table('channel_file', schema=None) as batch_op: + batch_op.drop_column('message_id') + + # Remove data column from knowledge table + with op.batch_alter_table('knowledge', schema=None) as batch_op: + batch_op.drop_column('data') diff --git a/backend/open_webui/migrations/versions/8452d01d26d7_add_chat_message_table.py b/backend/open_webui/migrations/versions/8452d01d26d7_add_chat_message_table.py new file mode 100644 index 0000000000000000000000000000000000000000..3254b57858163f7f8b03ac0e93bc008af0739c9b --- /dev/null +++ b/backend/open_webui/migrations/versions/8452d01d26d7_add_chat_message_table.py @@ -0,0 +1,221 @@ +"""Add chat_message table + +Revision ID: 8452d01d26d7 +Revises: 374d2f66af06 +Create Date: 2026-02-01 04:00:00.000000 + +""" + +import time +import json +import logging +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +log = logging.getLogger(__name__) + +revision: str = '8452d01d26d7' +down_revision: Union[str, None] = '374d2f66af06' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +BATCH_SIZE = 5000 + + +def _flush_batch(conn, table, batch): + """ + Insert a batch of messages, falling back to row-by-row on error. + + Tries a single bulk insert first (fast path). If that fails (e.g. due to + a duplicate key), falls back to individual inserts wrapped in savepoints + so the rest of the batch can still succeed. + """ + savepoint = conn.begin_nested() + try: + conn.execute(sa.insert(table), batch) + savepoint.commit() + return len(batch), 0 + except Exception: + savepoint.rollback() + # Batch failed - insert one-by-one to isolate the bad row(s) + inserted = 0 + failed = 0 + for msg in batch: + sp = conn.begin_nested() + try: + conn.execute(sa.insert(table).values(**msg)) + sp.commit() + inserted += 1 + except Exception as e: + sp.rollback() + failed += 1 + log.warning(f'Failed to insert message {msg["id"]}: {e}') + return inserted, failed + + +def upgrade() -> None: + # Step 1: Create table + op.create_table( + 'chat_message', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('chat_id', sa.Text(), nullable=False, index=True), + sa.Column('user_id', sa.Text(), index=True), + sa.Column('role', sa.Text(), nullable=False), + sa.Column('parent_id', sa.Text(), nullable=True), + sa.Column('content', sa.JSON(), nullable=True), + sa.Column('output', sa.JSON(), nullable=True), + sa.Column('model_id', sa.Text(), nullable=True, index=True), + sa.Column('files', sa.JSON(), nullable=True), + sa.Column('sources', sa.JSON(), nullable=True), + sa.Column('embeds', sa.JSON(), nullable=True), + sa.Column('done', sa.Boolean(), default=True), + sa.Column('status_history', sa.JSON(), nullable=True), + sa.Column('error', sa.JSON(), nullable=True), + sa.Column('usage', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), index=True), + sa.Column('updated_at', sa.BigInteger()), + sa.ForeignKeyConstraint(['chat_id'], ['chat.id'], ondelete='CASCADE'), + ) + + # Create composite indexes + op.create_index('chat_message_chat_parent_idx', 'chat_message', ['chat_id', 'parent_id']) + op.create_index('chat_message_model_created_idx', 'chat_message', ['model_id', 'created_at']) + op.create_index('chat_message_user_created_idx', 'chat_message', ['user_id', 'created_at']) + + # Step 2: Backfill from existing chats + conn = op.get_bind() + + chat_table = sa.table( + 'chat', + sa.column('id', sa.Text()), + sa.column('user_id', sa.Text()), + sa.column('chat', sa.JSON()), + ) + + chat_message_table = sa.table( + 'chat_message', + sa.column('id', sa.Text()), + sa.column('chat_id', sa.Text()), + sa.column('user_id', sa.Text()), + sa.column('role', sa.Text()), + sa.column('parent_id', sa.Text()), + sa.column('content', sa.JSON()), + sa.column('output', sa.JSON()), + sa.column('model_id', sa.Text()), + sa.column('files', sa.JSON()), + sa.column('sources', sa.JSON()), + sa.column('embeds', sa.JSON()), + sa.column('done', sa.Boolean()), + sa.column('status_history', sa.JSON()), + sa.column('error', sa.JSON()), + sa.column('usage', sa.JSON()), + sa.column('created_at', sa.BigInteger()), + sa.column('updated_at', sa.BigInteger()), + ) + + # Stream rows instead of loading all into memory: + # - yield_per: fetches rows in chunks via cursor.fetchmany() (all backends) + # - stream_results: enables server-side cursors on PostgreSQL (no-op on SQLite) + result = conn.execute( + sa.select(chat_table.c.id, chat_table.c.user_id, chat_table.c.chat) + .where(~chat_table.c.user_id.like('shared-%')) + .execution_options(yield_per=1000, stream_results=True) + ) + + now = int(time.time()) + messages_batch = [] + total_inserted = 0 + total_failed = 0 + + for chat_row in result: + chat_id = chat_row[0] + user_id = chat_row[1] + chat_data = chat_row[2] + + if not chat_data: + continue + + # Handle both string and dict chat data + if isinstance(chat_data, str): + try: + chat_data = json.loads(chat_data) + except Exception: + continue + + history = chat_data.get('history', {}) + if not isinstance(history, dict): + continue + + messages = history.get('messages', {}) + if not isinstance(messages, dict): + continue + + for message_id, message in messages.items(): + if not isinstance(message, dict): + continue + + role = message.get('role') + if not role: + continue + + timestamp = message.get('timestamp', now) + + try: + timestamp = int(float(timestamp)) + except Exception as e: + timestamp = now + + # Normalize timestamp: convert ms to seconds, validate range + if timestamp > 10_000_000_000: + timestamp = timestamp // 1000 + # Must be after 2020 and not too far in the future + if timestamp < 1577836800 or timestamp > now + 86400: + timestamp = now + + messages_batch.append( + { + 'id': f'{chat_id}-{message_id}', + 'chat_id': chat_id, + 'user_id': user_id, + 'role': role, + 'parent_id': message.get('parentId'), + 'content': message.get('content'), + 'output': message.get('output'), + 'model_id': message.get('model'), + 'files': message.get('files'), + 'sources': message.get('sources'), + 'embeds': message.get('embeds'), + 'done': message.get('done', True), + 'status_history': message.get('statusHistory'), + 'error': message.get('error'), + 'usage': message.get('usage'), + 'created_at': timestamp, + 'updated_at': timestamp, + } + ) + + # Flush batch when full + if len(messages_batch) >= BATCH_SIZE: + inserted, failed = _flush_batch(conn, chat_message_table, messages_batch) + total_inserted += inserted + total_failed += failed + if total_inserted % 50000 < BATCH_SIZE: + log.info(f'Migration progress: {total_inserted} messages inserted...') + messages_batch.clear() + + # Flush remaining messages + if messages_batch: + inserted, failed = _flush_batch(conn, chat_message_table, messages_batch) + total_inserted += inserted + total_failed += failed + + log.info(f'Backfilled {total_inserted} messages into chat_message table ({total_failed} failed)') + + +def downgrade() -> None: + op.drop_index('chat_message_user_created_idx', table_name='chat_message') + op.drop_index('chat_message_model_created_idx', table_name='chat_message') + op.drop_index('chat_message_chat_parent_idx', table_name='chat_message') + op.drop_table('chat_message') diff --git a/backend/open_webui/migrations/versions/90ef40d4714e_update_channel_and_channel_members_table.py b/backend/open_webui/migrations/versions/90ef40d4714e_update_channel_and_channel_members_table.py new file mode 100644 index 0000000000000000000000000000000000000000..9d115b1e5c4dc8bd2f06512ef0cea23691a613e4 --- /dev/null +++ b/backend/open_webui/migrations/versions/90ef40d4714e_update_channel_and_channel_members_table.py @@ -0,0 +1,78 @@ +"""Update channel and channel members table + +Revision ID: 90ef40d4714e +Revises: b10670c03dd5 +Create Date: 2025-11-30 06:33:38.790341 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import open_webui.internal.db + +# revision identifiers, used by Alembic. +revision: str = '90ef40d4714e' +down_revision: Union[str, None] = 'b10670c03dd5' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Update 'channel' table + op.add_column('channel', sa.Column('is_private', sa.Boolean(), nullable=True)) + + op.add_column('channel', sa.Column('archived_at', sa.BigInteger(), nullable=True)) + op.add_column('channel', sa.Column('archived_by', sa.Text(), nullable=True)) + + op.add_column('channel', sa.Column('deleted_at', sa.BigInteger(), nullable=True)) + op.add_column('channel', sa.Column('deleted_by', sa.Text(), nullable=True)) + + op.add_column('channel', sa.Column('updated_by', sa.Text(), nullable=True)) + + # Update 'channel_member' table + op.add_column('channel_member', sa.Column('role', sa.Text(), nullable=True)) + op.add_column('channel_member', sa.Column('invited_by', sa.Text(), nullable=True)) + op.add_column('channel_member', sa.Column('invited_at', sa.BigInteger(), nullable=True)) + + # Create 'channel_webhook' table + op.create_table( + 'channel_webhook', + sa.Column('id', sa.Text(), primary_key=True, unique=True, nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column( + 'channel_id', + sa.Text(), + sa.ForeignKey('channel.id', ondelete='CASCADE'), + nullable=False, + ), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('profile_image_url', sa.Text(), nullable=True), + sa.Column('token', sa.Text(), nullable=False), + sa.Column('last_used_at', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + ) + + pass + + +def downgrade() -> None: + # Downgrade 'channel' table + op.drop_column('channel', 'is_private') + op.drop_column('channel', 'archived_at') + op.drop_column('channel', 'archived_by') + op.drop_column('channel', 'deleted_at') + op.drop_column('channel', 'deleted_by') + op.drop_column('channel', 'updated_by') + + # Downgrade 'channel_member' table + op.drop_column('channel_member', 'role') + op.drop_column('channel_member', 'invited_by') + op.drop_column('channel_member', 'invited_at') + + # Drop 'channel_webhook' table + op.drop_table('channel_webhook') + + pass diff --git a/backend/open_webui/migrations/versions/922e7a387820_add_group_table.py b/backend/open_webui/migrations/versions/922e7a387820_add_group_table.py new file mode 100644 index 0000000000000000000000000000000000000000..5e617be1e6a24347490a70024dd3008c054b4f09 --- /dev/null +++ b/backend/open_webui/migrations/versions/922e7a387820_add_group_table.py @@ -0,0 +1,85 @@ +"""Add group table + +Revision ID: 922e7a387820 +Revises: 4ace53fd72c8 +Create Date: 2024-11-14 03:00:00.000000 + +""" + +from alembic import op +import sqlalchemy as sa + +revision = '922e7a387820' +down_revision = '4ace53fd72c8' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + 'group', + sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True), + sa.Column('user_id', sa.Text(), nullable=True), + sa.Column('name', sa.Text(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('permissions', sa.JSON(), nullable=True), + sa.Column('user_ids', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + ) + + # Add 'access_control' column to 'model' table + op.add_column( + 'model', + sa.Column('access_control', sa.JSON(), nullable=True), + ) + + # Add 'is_active' column to 'model' table + op.add_column( + 'model', + sa.Column( + 'is_active', + sa.Boolean(), + nullable=False, + server_default=sa.sql.expression.true(), + ), + ) + + # Add 'access_control' column to 'knowledge' table + op.add_column( + 'knowledge', + sa.Column('access_control', sa.JSON(), nullable=True), + ) + + # Add 'access_control' column to 'prompt' table + op.add_column( + 'prompt', + sa.Column('access_control', sa.JSON(), nullable=True), + ) + + # Add 'access_control' column to 'tools' table + op.add_column( + 'tool', + sa.Column('access_control', sa.JSON(), nullable=True), + ) + + +def downgrade(): + op.drop_table('group') + + # Drop 'access_control' column from 'model' table + op.drop_column('model', 'access_control') + + # Drop 'is_active' column from 'model' table + op.drop_column('model', 'is_active') + + # Drop 'access_control' column from 'knowledge' table + op.drop_column('knowledge', 'access_control') + + # Drop 'access_control' column from 'prompt' table + op.drop_column('prompt', 'access_control') + + # Drop 'access_control' column from 'tools' table + op.drop_column('tool', 'access_control') diff --git a/backend/open_webui/migrations/versions/9f0c9cd09105_add_note_table.py b/backend/open_webui/migrations/versions/9f0c9cd09105_add_note_table.py new file mode 100644 index 0000000000000000000000000000000000000000..c75db04ca5f02ad9e40cc0dd54daf8cea8ddd2ee --- /dev/null +++ b/backend/open_webui/migrations/versions/9f0c9cd09105_add_note_table.py @@ -0,0 +1,33 @@ +"""Add note table + +Revision ID: 9f0c9cd09105 +Revises: 3781e22d8b01 +Create Date: 2025-05-03 03:00:00.000000 + +""" + +from alembic import op +import sqlalchemy as sa + +revision = '9f0c9cd09105' +down_revision = '3781e22d8b01' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + 'note', + sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True), + sa.Column('user_id', sa.Text(), nullable=True), + sa.Column('title', sa.Text(), nullable=True), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('access_control', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + ) + + +def downgrade(): + op.drop_table('note') diff --git a/backend/open_webui/migrations/versions/a1b2c3d4e5f6_add_skill_table.py b/backend/open_webui/migrations/versions/a1b2c3d4e5f6_add_skill_table.py new file mode 100644 index 0000000000000000000000000000000000000000..f11f7d8d1b34496b7dca943ba4169ec084f1f503 --- /dev/null +++ b/backend/open_webui/migrations/versions/a1b2c3d4e5f6_add_skill_table.py @@ -0,0 +1,45 @@ +"""Add skill table + +Revision ID: a1b2c3d4e5f6 +Revises: f1e2d3c4b5a6 +Create Date: 2026-02-11 09:30:00.000000 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +from open_webui.migrations.util import get_existing_tables + +revision: str = 'a1b2c3d4e5f6' +down_revision: Union[str, None] = 'f1e2d3c4b5a6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + existing_tables = set(get_existing_tables()) + + if 'skill' not in existing_tables: + op.create_table( + 'skill', + sa.Column('id', sa.String(), nullable=False, primary_key=True), + sa.Column('user_id', sa.String(), nullable=False), + sa.Column('name', sa.Text(), nullable=False, unique=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + sa.Column('created_at', sa.BigInteger(), nullable=False), + ) + op.create_index('idx_skill_user_id', 'skill', ['user_id']) + op.create_index('idx_skill_updated_at', 'skill', ['updated_at']) + + +def downgrade() -> None: + op.drop_index('idx_skill_updated_at', table_name='skill') + op.drop_index('idx_skill_user_id', table_name='skill') + op.drop_table('skill') diff --git a/backend/open_webui/migrations/versions/a3dd5bedd151_add_tasks_and_summary_to_chat.py b/backend/open_webui/migrations/versions/a3dd5bedd151_add_tasks_and_summary_to_chat.py new file mode 100644 index 0000000000000000000000000000000000000000..20a3152cfe56154ec208a145013dc7d7e7ad1ee1 --- /dev/null +++ b/backend/open_webui/migrations/versions/a3dd5bedd151_add_tasks_and_summary_to_chat.py @@ -0,0 +1,28 @@ +"""Add tasks and summary columns to chat table + +Revision ID: a3dd5bedd151 +Revises: b2c3d4e5f6a7 +Create Date: 2026-03-29 22:15:00.000000 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = 'a3dd5bedd151' +down_revision: Union[str, None] = 'b2c3d4e5f6a7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column('chat', sa.Column('tasks', sa.JSON(), nullable=True)) + op.add_column('chat', sa.Column('summary', sa.Text(), nullable=True)) + + +def downgrade() -> None: + op.drop_column('chat', 'summary') + op.drop_column('chat', 'tasks') diff --git a/backend/open_webui/migrations/versions/a5c220713937_add_reply_to_id_column_to_message.py b/backend/open_webui/migrations/versions/a5c220713937_add_reply_to_id_column_to_message.py new file mode 100644 index 0000000000000000000000000000000000000000..29157baa0747d6a5e57d9477be69f1df5b08b44a --- /dev/null +++ b/backend/open_webui/migrations/versions/a5c220713937_add_reply_to_id_column_to_message.py @@ -0,0 +1,34 @@ +"""Add reply_to_id column to message + +Revision ID: a5c220713937 +Revises: 38d63c18f30f +Create Date: 2025-09-27 02:24:18.058455 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = 'a5c220713937' +down_revision: Union[str, None] = '38d63c18f30f' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add 'reply_to_id' column to the 'message' table for replying to messages + op.add_column( + 'message', + sa.Column('reply_to_id', sa.Text(), nullable=True), + ) + pass + + +def downgrade() -> None: + # Remove 'reply_to_id' column from the 'message' table + op.drop_column('message', 'reply_to_id') + + pass diff --git a/backend/open_webui/migrations/versions/af906e964978_add_feedback_table.py b/backend/open_webui/migrations/versions/af906e964978_add_feedback_table.py new file mode 100644 index 0000000000000000000000000000000000000000..4d8fd63e802b7745b6a7609ccb8f6dde2cfb3441 --- /dev/null +++ b/backend/open_webui/migrations/versions/af906e964978_add_feedback_table.py @@ -0,0 +1,41 @@ +"""Add feedback table + +Revision ID: af906e964978 +Revises: c29facfe716b +Create Date: 2024-10-20 17:02:35.241684 + +""" + +from alembic import op +import sqlalchemy as sa + +# Revision identifiers, used by Alembic. +revision = 'af906e964978' +down_revision = 'c29facfe716b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### Create feedback table ### + op.create_table( + 'feedback', + sa.Column('id', sa.Text(), primary_key=True), # Unique identifier for each feedback (TEXT type) + sa.Column('user_id', sa.Text(), nullable=True), # ID of the user providing the feedback (TEXT type) + sa.Column('version', sa.BigInteger(), default=0), # Version of feedback (BIGINT type) + sa.Column('type', sa.Text(), nullable=True), # Type of feedback (TEXT type) + sa.Column('data', sa.JSON(), nullable=True), # Feedback data (JSON type) + sa.Column('meta', sa.JSON(), nullable=True), # Metadata for feedback (JSON type) + sa.Column('snapshot', sa.JSON(), nullable=True), # snapshot data for feedback (JSON type) + sa.Column( + 'created_at', sa.BigInteger(), nullable=False + ), # Feedback creation timestamp (BIGINT representing epoch) + sa.Column( + 'updated_at', sa.BigInteger(), nullable=False + ), # Feedback update timestamp (BIGINT representing epoch) + ) + + +def downgrade(): + # ### Drop feedback table ### + op.drop_table('feedback') diff --git a/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py b/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py new file mode 100644 index 0000000000000000000000000000000000000000..623289d885068e3fbe250720e5adbcefde9e36c2 --- /dev/null +++ b/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py @@ -0,0 +1,237 @@ +"""Update user table + +Revision ID: b10670c03dd5 +Revises: 2f1211949ecc +Create Date: 2025-11-28 04:55:31.737538 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +import open_webui.internal.db +import json +import time + +# revision identifiers, used by Alembic. +revision: str = 'b10670c03dd5' +down_revision: Union[str, None] = '2f1211949ecc' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def _drop_sqlite_indexes_for_column(table_name, column_name, conn): + """ + SQLite requires manual removal of any indexes referencing a column + before ALTER TABLE ... DROP COLUMN can succeed. + """ + indexes = conn.execute(sa.text(f"PRAGMA index_list('{table_name}')")).fetchall() + + for idx in indexes: + index_name = idx[1] # index name + # Get indexed columns + idx_info = conn.execute(sa.text(f"PRAGMA index_info('{index_name}')")).fetchall() + + indexed_cols = [row[2] for row in idx_info] # col names + if column_name in indexed_cols: + conn.execute(sa.text(f'DROP INDEX IF EXISTS {index_name}')) + + +def _convert_column_to_json(table: str, column: str): + conn = op.get_bind() + dialect = conn.dialect.name + + # SQLite cannot ALTER COLUMN → must recreate column + if dialect == 'sqlite': + # 1. Add temporary column + op.add_column(table, sa.Column(f'{column}_json', sa.JSON(), nullable=True)) + + # 2. Load old data + rows = conn.execute(sa.text(f'SELECT id, {column} FROM "{table}"')).fetchall() + + for row in rows: + uid, raw = row + if raw is None: + parsed = None + else: + try: + parsed = json.loads(raw) + except Exception: + parsed = None # fallback safe behavior + + conn.execute( + sa.text(f'UPDATE "{table}" SET {column}_json = :val WHERE id = :id'), + {'val': json.dumps(parsed) if parsed else None, 'id': uid}, + ) + + # 3. Drop old TEXT column + op.drop_column(table, column) + + # 4. Rename new JSON column → original name + op.alter_column(table, f'{column}_json', new_column_name=column) + + else: + # PostgreSQL supports direct CAST + op.alter_column( + table, + column, + type_=sa.JSON(), + postgresql_using=f'{column}::json', + ) + + +def _convert_column_to_text(table: str, column: str): + conn = op.get_bind() + dialect = conn.dialect.name + + if dialect == 'sqlite': + op.add_column(table, sa.Column(f'{column}_text', sa.Text(), nullable=True)) + + rows = conn.execute(sa.text(f'SELECT id, {column} FROM "{table}"')).fetchall() + + for uid, raw in rows: + conn.execute( + sa.text(f'UPDATE "{table}" SET {column}_text = :val WHERE id = :id'), + {'val': json.dumps(raw) if raw else None, 'id': uid}, + ) + + op.drop_column(table, column) + op.alter_column(table, f'{column}_text', new_column_name=column) + + else: + op.alter_column( + table, + column, + type_=sa.Text(), + postgresql_using=f'to_json({column})::text', + ) + + +def upgrade() -> None: + op.add_column('user', sa.Column('profile_banner_image_url', sa.Text(), nullable=True)) + op.add_column('user', sa.Column('timezone', sa.String(), nullable=True)) + + op.add_column('user', sa.Column('presence_state', sa.String(), nullable=True)) + op.add_column('user', sa.Column('status_emoji', sa.String(), nullable=True)) + op.add_column('user', sa.Column('status_message', sa.Text(), nullable=True)) + op.add_column('user', sa.Column('status_expires_at', sa.BigInteger(), nullable=True)) + + op.add_column('user', sa.Column('oauth', sa.JSON(), nullable=True)) + + # Convert info (TEXT/JSONField) → JSON + _convert_column_to_json('user', 'info') + # Convert settings (TEXT/JSONField) → JSON + _convert_column_to_json('user', 'settings') + + op.create_table( + 'api_key', + sa.Column('id', sa.Text(), primary_key=True, unique=True), + sa.Column('user_id', sa.Text(), sa.ForeignKey('user.id', ondelete='CASCADE')), + sa.Column('key', sa.Text(), unique=True, nullable=False), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('expires_at', sa.BigInteger(), nullable=True), + sa.Column('last_used_at', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + ) + + conn = op.get_bind() + users = conn.execute(sa.text('SELECT id, oauth_sub FROM "user" WHERE oauth_sub IS NOT NULL')).fetchall() + + for uid, oauth_sub in users: + if oauth_sub: + # Example formats supported: + # provider@sub + # plain sub (stored as {"oidc": {"sub": sub}}) + if '@' in oauth_sub: + provider, sub = oauth_sub.split('@', 1) + else: + provider, sub = 'oidc', oauth_sub + + oauth_json = json.dumps({provider: {'sub': sub}}) + conn.execute( + sa.text('UPDATE "user" SET oauth = :oauth WHERE id = :id'), + {'oauth': oauth_json, 'id': uid}, + ) + + users_with_keys = conn.execute(sa.text('SELECT id, api_key FROM "user" WHERE api_key IS NOT NULL')).fetchall() + now = int(time.time()) + + for uid, api_key in users_with_keys: + if api_key: + conn.execute( + sa.text(""" + INSERT INTO api_key (id, user_id, key, created_at, updated_at) + VALUES (:id, :user_id, :key, :created_at, :updated_at) + """), + { + 'id': f'key_{uid}', + 'user_id': uid, + 'key': api_key, + 'created_at': now, + 'updated_at': now, + }, + ) + + if conn.dialect.name == 'sqlite': + _drop_sqlite_indexes_for_column('user', 'api_key', conn) + _drop_sqlite_indexes_for_column('user', 'oauth_sub', conn) + + with op.batch_alter_table('user') as batch_op: + batch_op.drop_column('api_key') + batch_op.drop_column('oauth_sub') + + +def downgrade() -> None: + # --- 1. Restore old oauth_sub column --- + op.add_column('user', sa.Column('oauth_sub', sa.Text(), nullable=True)) + + conn = op.get_bind() + users = conn.execute(sa.text('SELECT id, oauth FROM "user" WHERE oauth IS NOT NULL')).fetchall() + + for uid, oauth in users: + try: + data = json.loads(oauth) + provider = list(data.keys())[0] + sub = data[provider].get('sub') + oauth_sub = f'{provider}@{sub}' + except Exception: + oauth_sub = None + + conn.execute( + sa.text('UPDATE "user" SET oauth_sub = :oauth_sub WHERE id = :id'), + {'oauth_sub': oauth_sub, 'id': uid}, + ) + + op.drop_column('user', 'oauth') + + # --- 2. Restore api_key field --- + op.add_column('user', sa.Column('api_key', sa.String(), nullable=True)) + + # Restore values from api_key + keys = conn.execute(sa.text('SELECT user_id, key FROM api_key')).fetchall() + for uid, key in keys: + conn.execute( + sa.text('UPDATE "user" SET api_key = :key WHERE id = :id'), + {'key': key, 'id': uid}, + ) + + # Drop new table + op.drop_table('api_key') + + with op.batch_alter_table('user') as batch_op: + batch_op.drop_column('profile_banner_image_url') + batch_op.drop_column('timezone') + + batch_op.drop_column('presence_state') + batch_op.drop_column('status_emoji') + batch_op.drop_column('status_message') + batch_op.drop_column('status_expires_at') + + # Convert info (JSON) → TEXT + _convert_column_to_text('user', 'info') + # Convert settings (JSON) → TEXT + _convert_column_to_text('user', 'settings') diff --git a/backend/open_webui/migrations/versions/b2c3d4e5f6a7_add_scim_column_to_user_table.py b/backend/open_webui/migrations/versions/b2c3d4e5f6a7_add_scim_column_to_user_table.py new file mode 100644 index 0000000000000000000000000000000000000000..e3668d3b6ed983ebf071bce8875345fd0ad229f3 --- /dev/null +++ b/backend/open_webui/migrations/versions/b2c3d4e5f6a7_add_scim_column_to_user_table.py @@ -0,0 +1,26 @@ +"""add scim column to user table + +Revision ID: b2c3d4e5f6a7 +Revises: a1b2c3d4e5f6 +Create Date: 2026-02-13 14:19:00.000000 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = 'b2c3d4e5f6a7' +down_revision: Union[str, None] = 'a1b2c3d4e5f6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column('user', sa.Column('scim', sa.JSON(), nullable=True)) + + +def downgrade() -> None: + op.drop_column('user', 'scim') diff --git a/backend/open_webui/migrations/versions/b7c8d9e0f1a2_add_last_read_at_to_chat.py b/backend/open_webui/migrations/versions/b7c8d9e0f1a2_add_last_read_at_to_chat.py new file mode 100644 index 0000000000000000000000000000000000000000..fb254432f69fde97dfe082f36d56ba0694debb55 --- /dev/null +++ b/backend/open_webui/migrations/versions/b7c8d9e0f1a2_add_last_read_at_to_chat.py @@ -0,0 +1,27 @@ +"""add last_read_at to chat + +Revision ID: b7c8d9e0f1a2 +Revises: d4e5f6a7b8c9 +Create Date: 2026-04-01 04:00:00.000000 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'b7c8d9e0f1a2' +down_revision = 'd4e5f6a7b8c9' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('chat', sa.Column('last_read_at', sa.BigInteger(), nullable=True)) + # Set existing chats to be marked as read + op.execute('UPDATE chat SET last_read_at = updated_at') + + +def downgrade(): + op.drop_column('chat', 'last_read_at') diff --git a/backend/open_webui/migrations/versions/c0fbf31ca0db_update_file_table.py b/backend/open_webui/migrations/versions/c0fbf31ca0db_update_file_table.py new file mode 100644 index 0000000000000000000000000000000000000000..709b644150b88e005f2adf934cd775c41d0a6455 --- /dev/null +++ b/backend/open_webui/migrations/versions/c0fbf31ca0db_update_file_table.py @@ -0,0 +1,32 @@ +"""Update file table + +Revision ID: c0fbf31ca0db +Revises: ca81bd47c050 +Create Date: 2024-09-20 15:26:35.241684 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = 'c0fbf31ca0db' +down_revision: Union[str, None] = 'ca81bd47c050' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('file', sa.Column('hash', sa.Text(), nullable=True)) + op.add_column('file', sa.Column('data', sa.JSON(), nullable=True)) + op.add_column('file', sa.Column('updated_at', sa.BigInteger(), nullable=True)) + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('file', 'updated_at') + op.drop_column('file', 'data') + op.drop_column('file', 'hash') diff --git a/backend/open_webui/migrations/versions/c1d2e3f4a5b6_add_shared_chat_table.py b/backend/open_webui/migrations/versions/c1d2e3f4a5b6_add_shared_chat_table.py new file mode 100644 index 0000000000000000000000000000000000000000..2451f50ae2f39db190a7e590c96960b6d61597b1 --- /dev/null +++ b/backend/open_webui/migrations/versions/c1d2e3f4a5b6_add_shared_chat_table.py @@ -0,0 +1,164 @@ +"""Add shared_chat table and migrate existing shares + +Revision ID: c1d2e3f4a5b6 +Revises: e1f2a3b4c5d6 +Create Date: 2026-04-16 23:00:00.000000 + +""" + +import time +import uuid + +from alembic import op +import sqlalchemy as sa + +revision = 'c1d2e3f4a5b6' +down_revision = 'e1f2a3b4c5d6' +branch_labels = None +depends_on = None + +# Lightweight table references for data migration (no ORM models needed) +chat_t = sa.table( + 'chat', + sa.column('id', sa.Text), + sa.column('user_id', sa.Text), + sa.column('title', sa.Text), + sa.column('chat', sa.JSON), + sa.column('share_id', sa.Text), + sa.column('created_at', sa.BigInteger), + sa.column('updated_at', sa.BigInteger), + sa.column('archived', sa.Boolean), + sa.column('meta', sa.JSON), +) + +shared_chat_t = sa.table( + 'shared_chat', + sa.column('id', sa.Text), + sa.column('chat_id', sa.Text), + sa.column('user_id', sa.Text), + sa.column('title', sa.Text), + sa.column('chat', sa.JSON), + sa.column('created_at', sa.BigInteger), + sa.column('updated_at', sa.BigInteger), +) + +chat_message_t = sa.table( + 'chat_message', + sa.column('chat_id', sa.Text), +) + +access_grant_t = sa.table( + 'access_grant', + sa.column('id', sa.Text), + sa.column('resource_type', sa.Text), + sa.column('resource_id', sa.Text), + sa.column('principal_type', sa.Text), + sa.column('principal_id', sa.Text), + sa.column('permission', sa.Text), + sa.column('created_at', sa.BigInteger), +) + + +def upgrade(): + conn = op.get_bind() + + # 1. Create shared_chat table + op.create_table( + 'shared_chat', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('chat_id', sa.Text(), sa.ForeignKey('chat.id', ondelete='CASCADE'), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('title', sa.Text(), nullable=True), + sa.Column('chat', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + ) + + # 2. Migrate existing shared-* rows + shared_rows = conn.execute( + sa.select( + chat_t.c.id, + chat_t.c.user_id, + chat_t.c.title, + chat_t.c.chat, + chat_t.c.created_at, + chat_t.c.updated_at, + ).where(chat_t.c.user_id.like('shared-%')) + ).fetchall() + + for row in shared_rows: + share_token = row.id + original_chat_id = row.user_id.replace('shared-', '', 1) + + # Verify original chat still exists + original = conn.execute(sa.select(chat_t.c.user_id).where(chat_t.c.id == original_chat_id)).fetchone() + + if not original: + continue + + # Insert snapshot into shared_chat + conn.execute( + shared_chat_t.insert().values( + id=share_token, + chat_id=original_chat_id, + user_id=original.user_id, + title=row.title, + chat=row.chat, + created_at=row.created_at, + updated_at=row.updated_at, + ) + ) + + # Create user:*:read grant for backward compat + conn.execute( + access_grant_t.insert().values( + id=str(uuid.uuid4()), + resource_type='shared_chat', + resource_id=original_chat_id, + principal_type='user', + principal_id='*', + permission='read', + created_at=row.created_at or int(time.time()), + ) + ) + + # 3. Clean up old phantom rows + conn.execute( + chat_message_t.delete().where( + chat_message_t.c.chat_id.in_(sa.select(chat_t.c.id).where(chat_t.c.user_id.like('shared-%'))) + ) + ) + conn.execute(chat_t.delete().where(chat_t.c.user_id.like('shared-%'))) + + +def downgrade(): + conn = op.get_bind() + + shared_rows = conn.execute( + sa.select( + shared_chat_t.c.id, + shared_chat_t.c.chat_id, + shared_chat_t.c.user_id, + shared_chat_t.c.title, + shared_chat_t.c.chat, + shared_chat_t.c.created_at, + shared_chat_t.c.updated_at, + ) + ).fetchall() + + for row in shared_rows: + conn.execute( + chat_t.insert().values( + id=row.id, + user_id=f'shared-{row.chat_id}', + title=row.title, + chat=row.chat, + created_at=row.created_at, + updated_at=row.updated_at, + archived=False, + meta={}, + ) + ) + + conn.execute(access_grant_t.delete().where(access_grant_t.c.resource_type == 'shared_chat')) + op.drop_table('shared_chat') diff --git a/backend/open_webui/migrations/versions/c29facfe716b_update_file_table_path.py b/backend/open_webui/migrations/versions/c29facfe716b_update_file_table_path.py new file mode 100644 index 0000000000000000000000000000000000000000..37fe63ef15e7f27210ee96eab59e840e13168565 --- /dev/null +++ b/backend/open_webui/migrations/versions/c29facfe716b_update_file_table_path.py @@ -0,0 +1,70 @@ +"""Update file table path + +Revision ID: c29facfe716b +Revises: c69f45358db4 +Create Date: 2024-10-20 17:02:35.241684 + +""" + +from alembic import op +import sqlalchemy as sa +import json +from sqlalchemy.sql import table, column +from sqlalchemy import String, Text, JSON, and_ + +revision = 'c29facfe716b' +down_revision = 'c69f45358db4' +branch_labels = None +depends_on = None + + +def upgrade(): + # 1. Add the `path` column to the "file" table. + op.add_column('file', sa.Column('path', sa.Text(), nullable=True)) + + # 2. Convert the `meta` column from Text/JSONField to `JSON()` + # Use Alembic's default batch_op for dialect compatibility. + with op.batch_alter_table('file', schema=None) as batch_op: + batch_op.alter_column( + 'meta', + type_=sa.JSON(), + existing_type=sa.Text(), + existing_nullable=True, + nullable=True, + postgresql_using='meta::json', + ) + + # 3. Migrate legacy data from `meta` JSONField + # Fetch and process `meta` data from the table, add values to the new `path` column as necessary. + # We will use SQLAlchemy core bindings to ensure safety across different databases. + + file_table = table('file', column('id', String), column('meta', JSON), column('path', Text)) + + # Create connection to the database + connection = op.get_bind() + + # Get the rows where `meta` has a path and `path` column is null (new column) + # Loop through each row in the result set to update the path + results = connection.execute( + sa.select(file_table.c.id, file_table.c.meta).where( + and_(file_table.c.path.is_(None), file_table.c.meta.isnot(None)) + ) + ).fetchall() + + # Iterate over each row to extract and update the `path` from `meta` column + for row in results: + if 'path' in row.meta: + # Extract the `path` field from the `meta` JSON + path = row.meta.get('path') + + # Update the `file` table with the new `path` value + connection.execute(file_table.update().where(file_table.c.id == row.id).values({'path': path})) + + +def downgrade(): + # 1. Remove the `path` column + op.drop_column('file', 'path') + + # 2. Revert the `meta` column back to Text/JSONField + with op.batch_alter_table('file', schema=None) as batch_op: + batch_op.alter_column('meta', type_=sa.Text(), existing_type=sa.JSON(), existing_nullable=True) diff --git a/backend/open_webui/migrations/versions/c440947495f3_add_chat_file_table.py b/backend/open_webui/migrations/versions/c440947495f3_add_chat_file_table.py new file mode 100644 index 0000000000000000000000000000000000000000..0eae928b91a7e80accd5cc0bf7a77cd18427ec68 --- /dev/null +++ b/backend/open_webui/migrations/versions/c440947495f3_add_chat_file_table.py @@ -0,0 +1,54 @@ +"""Add chat_file table + +Revision ID: c440947495f3 +Revises: 81cc2ce44d79 +Create Date: 2025-12-21 20:27:41.694897 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = 'c440947495f3' +down_revision: Union[str, None] = '81cc2ce44d79' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + 'chat_file', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column( + 'chat_id', + sa.Text(), + sa.ForeignKey('chat.id', ondelete='CASCADE'), + nullable=False, + ), + sa.Column( + 'file_id', + sa.Text(), + sa.ForeignKey('file.id', ondelete='CASCADE'), + nullable=False, + ), + sa.Column('message_id', sa.Text(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + # indexes + sa.Index('ix_chat_file_chat_id', 'chat_id'), + sa.Index('ix_chat_file_file_id', 'file_id'), + sa.Index('ix_chat_file_message_id', 'message_id'), + sa.Index('ix_chat_file_user_id', 'user_id'), + # unique constraints + sa.UniqueConstraint('chat_id', 'file_id', name='uq_chat_file_chat_file'), # prevent duplicate entries + ) + pass + + +def downgrade() -> None: + op.drop_table('chat_file') + pass diff --git a/backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py b/backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py new file mode 100644 index 0000000000000000000000000000000000000000..c9572fe7a31a0fb6fc38772f12bf7ac61a6615d1 --- /dev/null +++ b/backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py @@ -0,0 +1,48 @@ +"""Add folder table + +Revision ID: c69f45358db4 +Revises: 3ab32c4b8f59 +Create Date: 2024-10-16 02:02:35.241684 + +""" + +from alembic import op +import sqlalchemy as sa + +revision = 'c69f45358db4' +down_revision = '3ab32c4b8f59' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + 'folder', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('parent_id', sa.Text(), nullable=True), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('items', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('is_expanded', sa.Boolean(), default=False, nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.Column( + 'updated_at', + sa.DateTime(), + nullable=False, + server_default=sa.func.now(), + onupdate=sa.func.now(), + ), + sa.PrimaryKeyConstraint('id', 'user_id'), + ) + + op.add_column( + 'chat', + sa.Column('folder_id', sa.Text(), nullable=True), + ) + + +def downgrade(): + op.drop_column('chat', 'folder_id') + + op.drop_table('folder') diff --git a/backend/open_webui/migrations/versions/ca81bd47c050_add_config_table.py b/backend/open_webui/migrations/versions/ca81bd47c050_add_config_table.py new file mode 100644 index 0000000000000000000000000000000000000000..5fdf933dd6fe66f8661f1d6530fa38f7b75f1b7b --- /dev/null +++ b/backend/open_webui/migrations/versions/ca81bd47c050_add_config_table.py @@ -0,0 +1,39 @@ +"""Add config table + +Revision ID: ca81bd47c050 +Revises: 7e5b5dc7342b +Create Date: 2024-08-25 15:26:35.241684 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = 'ca81bd47c050' +down_revision: Union[str, None] = '7e5b5dc7342b' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade(): + op.create_table( + 'config', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('data', sa.JSON(), nullable=False), + sa.Column('version', sa.Integer, nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.now()), + sa.Column( + 'updated_at', + sa.DateTime(), + nullable=True, + server_default=sa.func.now(), + onupdate=sa.func.now(), + ), + ) + + +def downgrade(): + op.drop_table('config') diff --git a/backend/open_webui/migrations/versions/d31026856c01_update_folder_table_data.py b/backend/open_webui/migrations/versions/d31026856c01_update_folder_table_data.py new file mode 100644 index 0000000000000000000000000000000000000000..444e131db7c7d09898347ba8576f2773ef068c83 --- /dev/null +++ b/backend/open_webui/migrations/versions/d31026856c01_update_folder_table_data.py @@ -0,0 +1,23 @@ +"""Update folder table data + +Revision ID: d31026856c01 +Revises: 9f0c9cd09105 +Create Date: 2025-07-13 03:00:00.000000 + +""" + +from alembic import op +import sqlalchemy as sa + +revision = 'd31026856c01' +down_revision = '9f0c9cd09105' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('folder', sa.Column('data', sa.JSON(), nullable=True)) + + +def downgrade(): + op.drop_column('folder', 'data') diff --git a/backend/open_webui/migrations/versions/d4e5f6a7b8c9_add_automation_tables.py b/backend/open_webui/migrations/versions/d4e5f6a7b8c9_add_automation_tables.py new file mode 100644 index 0000000000000000000000000000000000000000..fc90dc417f2d1a0171e6a7fa73e66d38368f2b31 --- /dev/null +++ b/backend/open_webui/migrations/versions/d4e5f6a7b8c9_add_automation_tables.py @@ -0,0 +1,55 @@ +"""add automation tables + +Revision ID: d4e5f6a7b8c9 +Revises: f1e2d3c4b5a6 +Create Date: 2026-03-30 +""" + +from typing import Union + +from alembic import op +import sqlalchemy as sa + +revision: str = 'd4e5f6a7b8c9' +down_revision: Union[str, None] = 'a3dd5bedd151' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + 'automation', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('data', sa.JSON(), nullable=False), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False, default=True), + sa.Column('last_run_at', sa.BigInteger(), nullable=True), + sa.Column('next_run_at', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + ) + op.create_index('ix_automation_next_run', 'automation', ['next_run_at']) + + op.create_table( + 'automation_run', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('automation_id', sa.Text(), nullable=False), + sa.Column('chat_id', sa.Text(), nullable=True), + sa.Column('status', sa.Text(), nullable=False), + sa.Column('error', sa.Text(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + ) + op.create_index( + 'ix_automation_run_automation_id', + 'automation_run', + ['automation_id'], + ) + + +def downgrade(): + op.drop_index('ix_automation_run_automation_id') + op.drop_table('automation_run') + op.drop_index('ix_automation_next_run') + op.drop_table('automation') diff --git a/backend/open_webui/migrations/versions/e1f2a3b4c5d6_add_is_pinned_to_note.py b/backend/open_webui/migrations/versions/e1f2a3b4c5d6_add_is_pinned_to_note.py new file mode 100644 index 0000000000000000000000000000000000000000..0d80558746e73b144f9773dbb47172ae8836d2bd --- /dev/null +++ b/backend/open_webui/migrations/versions/e1f2a3b4c5d6_add_is_pinned_to_note.py @@ -0,0 +1,23 @@ +"""Add is_pinned to note table + +Revision ID: e1f2a3b4c5d6 +Revises: b7c8d9e0f1a2 +Create Date: 2026-04-14 22:00:00.000000 + +""" + +from alembic import op +import sqlalchemy as sa + +revision = 'e1f2a3b4c5d6' +down_revision = 'b7c8d9e0f1a2' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('note', sa.Column('is_pinned', sa.Boolean(), nullable=True)) + + +def downgrade(): + op.drop_column('note', 'is_pinned') diff --git a/backend/open_webui/migrations/versions/f1e2d3c4b5a6_add_access_grant_table.py b/backend/open_webui/migrations/versions/f1e2d3c4b5a6_add_access_grant_table.py new file mode 100644 index 0000000000000000000000000000000000000000..5ed572cf7a747685a68a7378beaa9bfa46fa3527 --- /dev/null +++ b/backend/open_webui/migrations/versions/f1e2d3c4b5a6_add_access_grant_table.py @@ -0,0 +1,344 @@ +"""Add access_grant table + +Revision ID: f1e2d3c4b5a6 +Revises: 8452d01d26d7 +Create Date: 2026-02-05 10:00:00.000000 + +Migrates from JSON access_control columns to normalized access_grant table. +Access control semantics: +- NULL: Public access (all users can read) -> insert user:* for read +- {}: Private/owner-only (no grants) -> insert nothing +- {read: {...}, write: {...}}: Custom permissions -> insert specific grants +""" + +from typing import Sequence, Union +import time +import uuid + +from alembic import op +import sqlalchemy as sa + +from open_webui.migrations.util import get_existing_tables + +revision: str = 'f1e2d3c4b5a6' +down_revision: Union[str, None] = '8452d01d26d7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + existing_tables = set(get_existing_tables()) + + # Create access_grant table + if 'access_grant' not in existing_tables: + op.create_table( + 'access_grant', + sa.Column('id', sa.Text(), nullable=False, primary_key=True), + sa.Column('resource_type', sa.Text(), nullable=False), + sa.Column('resource_id', sa.Text(), nullable=False), + sa.Column('principal_type', sa.Text(), nullable=False), + sa.Column('principal_id', sa.Text(), nullable=False), + sa.Column('permission', sa.Text(), nullable=False), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.UniqueConstraint( + 'resource_type', + 'resource_id', + 'principal_type', + 'principal_id', + 'permission', + name='uq_access_grant_grant', + ), + ) + op.create_index( + 'idx_access_grant_resource', + 'access_grant', + ['resource_type', 'resource_id'], + ) + op.create_index( + 'idx_access_grant_principal', + 'access_grant', + ['principal_type', 'principal_id'], + ) + + # Backfill existing access_control JSON data + conn = op.get_bind() + + # Tables with access_control JSON columns: (table_name, resource_type) + resource_tables = [ + ('knowledge', 'knowledge'), + ('prompt', 'prompt'), + ('tool', 'tool'), + ('model', 'model'), + ('note', 'note'), + ('channel', 'channel'), + ('file', 'file'), + ] + + now = int(time.time()) + inserted = set() + + for table_name, resource_type in resource_tables: + if table_name not in existing_tables: + continue + + # Query all rows + try: + result = conn.execute(sa.text(f'SELECT id, access_control FROM "{table_name}"')) + rows = result.fetchall() + except Exception: + continue + + for row in rows: + resource_id = row[0] + access_control_json = row[1] + + # Handle NULL or JSON "null" = public access (user:* for read) + # Could be Python None (SQL NULL) or string "null" (JSON null) + # EXCEPTION: files with NULL are PRIVATE (owner-only), not public + is_null = ( + access_control_json is None + or access_control_json == 'null' + or (isinstance(access_control_json, str) and access_control_json.strip().lower() == 'null') + ) + if is_null: + # Files: NULL = private (no entry needed, owner has implicit access) + # Other resources: NULL = public (insert user:* for read) + if resource_type == 'file': + continue # Private - no entry needed + + key = (resource_type, resource_id, 'user', '*', 'read') + if key not in inserted: + try: + conn.execute( + sa.text(""" + INSERT INTO access_grant (id, resource_type, resource_id, principal_type, principal_id, permission, created_at) + VALUES (:id, :resource_type, :resource_id, :principal_type, :principal_id, :permission, :created_at) + """), + { + 'id': str(uuid.uuid4()), + 'resource_type': resource_type, + 'resource_id': resource_id, + 'principal_type': 'user', + 'principal_id': '*', + 'permission': 'read', + 'created_at': now, + }, + ) + inserted.add(key) + except Exception: + pass + continue + + # Handle JSON parsing + if isinstance(access_control_json, str): + import json + + try: + access_control_json = json.loads(access_control_json) + except Exception: + continue + + # Handle {} = private/owner-only - NO entries needed + # Owner access is implicit, no grants to store + if not access_control_json or not isinstance(access_control_json, dict): + continue + + # Check if it's effectively empty (no read/write keys with content) + read_data = access_control_json.get('read', {}) + write_data = access_control_json.get('write', {}) + + has_read_grants = read_data.get('group_ids', []) or read_data.get('user_ids', []) + has_write_grants = write_data.get('group_ids', []) or write_data.get('user_ids', []) + + if not has_read_grants and not has_write_grants: + # Empty permissions = private, no grants needed + continue + + # Extract permissions and insert into access_grant table + for permission in ['read', 'write']: + perm_data = access_control_json.get(permission, {}) + if not perm_data: + continue + + for group_id in perm_data.get('group_ids', []): + key = (resource_type, resource_id, 'group', group_id, permission) + if key in inserted: + continue + try: + conn.execute( + sa.text(""" + INSERT INTO access_grant (id, resource_type, resource_id, principal_type, principal_id, permission, created_at) + VALUES (:id, :resource_type, :resource_id, :principal_type, :principal_id, :permission, :created_at) + """), + { + 'id': str(uuid.uuid4()), + 'resource_type': resource_type, + 'resource_id': resource_id, + 'principal_type': 'group', + 'principal_id': group_id, + 'permission': permission, + 'created_at': now, + }, + ) + inserted.add(key) + except Exception: + pass + + for user_id in perm_data.get('user_ids', []): + key = (resource_type, resource_id, 'user', user_id, permission) + if key in inserted: + continue + try: + conn.execute( + sa.text(""" + INSERT INTO access_grant (id, resource_type, resource_id, principal_type, principal_id, permission, created_at) + VALUES (:id, :resource_type, :resource_id, :principal_type, :principal_id, :permission, :created_at) + """), + { + 'id': str(uuid.uuid4()), + 'resource_type': resource_type, + 'resource_id': resource_id, + 'principal_type': 'user', + 'principal_id': user_id, + 'permission': permission, + 'created_at': now, + }, + ) + inserted.add(key) + except Exception: + pass + + # Drop access_control columns from resource tables + for table_name, _ in resource_tables: + if table_name not in existing_tables: + continue + try: + with op.batch_alter_table(table_name) as batch: + batch.drop_column('access_control') + except Exception: + pass + + +def downgrade() -> None: + import json + + conn = op.get_bind() + + # Resource tables mapping: (table_name, resource_type) + resource_tables = [ + ('knowledge', 'knowledge'), + ('prompt', 'prompt'), + ('tool', 'tool'), + ('model', 'model'), + ('note', 'note'), + ('channel', 'channel'), + ('file', 'file'), + ] + + # Step 1: Re-add access_control columns to resource tables + for table_name, _ in resource_tables: + try: + with op.batch_alter_table(table_name) as batch: + batch.add_column(sa.Column('access_control', sa.JSON(), nullable=True)) + except Exception: + pass + + # Step 2: Query access_grant table and reconstruct JSON for each resource + for table_name, resource_type in resource_tables: + try: + # Get all grants for this resource type + result = conn.execute( + sa.text(""" + SELECT resource_id, principal_type, principal_id, permission + FROM access_grant + WHERE resource_type = :resource_type + """), + {'resource_type': resource_type}, + ) + rows = result.fetchall() + except Exception: + continue + + # Group by resource_id and reconstruct JSON structure + resource_grants = {} + for row in rows: + resource_id = row[0] + principal_type = row[1] + principal_id = row[2] + permission = row[3] + + if resource_id not in resource_grants: + resource_grants[resource_id] = { + 'is_public': False, + 'read': {'group_ids': [], 'user_ids': []}, + 'write': {'group_ids': [], 'user_ids': []}, + } + + # Handle public access (user:* for read) + if principal_type == 'user' and principal_id == '*' and permission == 'read': + resource_grants[resource_id]['is_public'] = True + continue + + # Add to appropriate list + if permission in ['read', 'write']: + if principal_type == 'group': + if principal_id not in resource_grants[resource_id][permission]['group_ids']: + resource_grants[resource_id][permission]['group_ids'].append(principal_id) + elif principal_type == 'user': + if principal_id not in resource_grants[resource_id][permission]['user_ids']: + resource_grants[resource_id][permission]['user_ids'].append(principal_id) + + # Step 3: Update each resource with reconstructed JSON + for resource_id, grants in resource_grants.items(): + if grants['is_public']: + # Public = NULL + access_control_value = None + elif ( + not grants['read']['group_ids'] + and not grants['read']['user_ids'] + and not grants['write']['group_ids'] + and not grants['write']['user_ids'] + ): + # No grants = should not happen (would mean no entries), default to {} + access_control_value = json.dumps({}) + else: + # Custom permissions + access_control_value = json.dumps( + { + 'read': grants['read'], + 'write': grants['write'], + } + ) + + try: + conn.execute( + sa.text(f'UPDATE "{table_name}" SET access_control = :access_control WHERE id = :id'), + {'access_control': access_control_value, 'id': resource_id}, + ) + except Exception: + pass + + # Step 4: Set all resources WITHOUT entries to private + # For files: NULL means private (owner-only), so leave as NULL + # For other resources: {} means private, so update to {} + if resource_type != 'file': + try: + conn.execute( + sa.text(f""" + UPDATE "{table_name}" + SET access_control = :private_value + WHERE id NOT IN ( + SELECT DISTINCT resource_id FROM access_grant WHERE resource_type = :resource_type + ) + AND access_control IS NULL + """), + {'private_value': json.dumps({}), 'resource_type': resource_type}, + ) + except Exception: + pass + # For files, NULL stays NULL - no action needed + + # Step 5: Drop the access_grant table + op.drop_index('idx_access_grant_principal', table_name='access_grant') + op.drop_index('idx_access_grant_resource', table_name='access_grant') + op.drop_table('access_grant') diff --git a/backend/open_webui/models/access_grants.py b/backend/open_webui/models/access_grants.py new file mode 100644 index 0000000000000000000000000000000000000000..f031495912dae75b597493ff3d508aa75d7519e5 --- /dev/null +++ b/backend/open_webui/models/access_grants.py @@ -0,0 +1,882 @@ +import logging +import time +import uuid +from typing import Optional + +from sqlalchemy import select, delete +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, get_async_db_context + +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, Text, UniqueConstraint, or_, and_ +from sqlalchemy.dialects.postgresql import JSONB + +log = logging.getLogger(__name__) + + +#################### +# AccessGrant DB Schema +#################### + + +class AccessGrant(Base): + __tablename__ = 'access_grant' + + id = Column(Text, primary_key=True) + resource_type = Column(Text, nullable=False) # "knowledge", "model", "prompt", "tool", "note", "channel", "file" + resource_id = Column(Text, nullable=False) + principal_type = Column(Text, nullable=False) # "user" or "group" + principal_id = Column(Text, nullable=False) # user_id, group_id, or "*" (wildcard for public) + permission = Column(Text, nullable=False) # "read" or "write" + created_at = Column(BigInteger, nullable=False) + + __table_args__ = ( + UniqueConstraint( + 'resource_type', + 'resource_id', + 'principal_type', + 'principal_id', + 'permission', + name='uq_access_grant_grant', + ), + ) + + +class AccessGrantModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + resource_type: str + resource_id: str + principal_type: str + principal_id: str + permission: str + created_at: int + + +class AccessGrantResponse(BaseModel): + """Slim grant model for API responses — resource context is implicit from the parent.""" + + id: str + principal_type: str + principal_id: str + permission: str + + @classmethod + def from_grant(cls, grant: 'AccessGrantModel') -> 'AccessGrantResponse': + return cls( + id=grant.id, + principal_type=grant.principal_type, + principal_id=grant.principal_id, + permission=grant.permission, + ) + + +#################### +# Conversion utilities +#################### + + +def access_control_to_grants( + resource_type: str, + resource_id: str, + access_control: Optional[dict], +) -> list[dict]: + """ + Convert an old-style access_control JSON dict to a flat list of grant dicts. + + Semantics: + - None → public read (user:* read) — except files which are private + - {} → private/owner-only (no grants) + - {read: {group_ids, user_ids}, write: {group_ids, user_ids}} → specific grants + + Returns a list of dicts with keys: resource_type, resource_id, principal_type, principal_id, permission + """ + grants = [] + + if access_control is None: + # NULL → public read (user:* for read) + # Exception: files with NULL are private (owner-only), no grants needed + if resource_type != 'file': + grants.append( + { + 'resource_type': resource_type, + 'resource_id': resource_id, + 'principal_type': 'user', + 'principal_id': '*', + 'permission': 'read', + } + ) + return grants + + # {} → private/owner-only, no grants + if not access_control: + return grants + + # Parse structured permissions + for permission in ['read', 'write']: + perm_data = access_control.get(permission, {}) + if not perm_data: + continue + + for group_id in perm_data.get('group_ids', []): + grants.append( + { + 'resource_type': resource_type, + 'resource_id': resource_id, + 'principal_type': 'group', + 'principal_id': group_id, + 'permission': permission, + } + ) + + for user_id in perm_data.get('user_ids', []): + grants.append( + { + 'resource_type': resource_type, + 'resource_id': resource_id, + 'principal_type': 'user', + 'principal_id': user_id, + 'permission': permission, + } + ) + + return grants + + +def normalize_access_grants(access_grants: Optional[list]) -> list[dict]: + """ + Normalize direct access_grants payloads from API forms. + + Keeps only valid grants and removes duplicates by + (principal_type, principal_id, permission). + """ + if not access_grants: + return [] + + deduped = {} + for grant in access_grants: + if isinstance(grant, BaseModel): + grant = grant.model_dump() + if not isinstance(grant, dict): + continue + + principal_type = grant.get('principal_type') + principal_id = grant.get('principal_id') + permission = grant.get('permission') + + if principal_type not in ('user', 'group'): + continue + if permission not in ('read', 'write'): + continue + if not isinstance(principal_id, str) or not principal_id: + continue + + key = (principal_type, principal_id, permission) + deduped[key] = { + 'id': (grant.get('id') if isinstance(grant.get('id'), str) and grant.get('id') else str(uuid.uuid4())), + 'principal_type': principal_type, + 'principal_id': principal_id, + 'permission': permission, + } + + return list(deduped.values()) + + +def has_public_read_access_grant(access_grants: Optional[list]) -> bool: + """ + Returns True when a direct grant list includes wildcard public-read. + """ + for grant in normalize_access_grants(access_grants): + if grant['principal_type'] == 'user' and grant['principal_id'] == '*' and grant['permission'] == 'read': + return True + return False + + +def has_public_write_access_grant(access_grants: Optional[list]) -> bool: + """ + Returns True when a direct grant list includes wildcard public-write. + """ + for grant in normalize_access_grants(access_grants): + if grant['principal_type'] == 'user' and grant['principal_id'] == '*' and grant['permission'] == 'write': + return True + return False + + +def has_user_access_grant(access_grants: Optional[list]) -> bool: + """ + Returns True when a direct grant list includes any non-wildcard user grant. + """ + for grant in normalize_access_grants(access_grants): + if grant['principal_type'] == 'user' and grant['principal_id'] != '*': + return True + return False + + +def strip_user_access_grants(access_grants: Optional[list]) -> list: + """ + Remove all non-wildcard user grants from the list. + Keeps group grants and the public wildcard (user:*) intact. + """ + if not access_grants: + return [] + return [ + grant + for grant in access_grants + if not ( + (grant.get('principal_type') if isinstance(grant, dict) else getattr(grant, 'principal_type', None)) + == 'user' + and (grant.get('principal_id') if isinstance(grant, dict) else getattr(grant, 'principal_id', None)) != '*' + ) + ] + + +def grants_to_access_control(grants: list) -> Optional[dict]: + """ + Convert a list of grant objects (AccessGrantModel or AccessGrantResponse) + back to the old-style access_control JSON dict for backward compatibility. + + Semantics: + - [] (empty) → {} (private/owner-only) + - Contains user:*:read → None (public), but write grants are preserved + - Otherwise → {read: {group_ids, user_ids}, write: {group_ids, user_ids}} + + Note: "public" (user:*:read) still allows additional write permissions + to coexist. When the wildcard read is present the function returns None + for the legacy dict, so callers that need write info should inspect the + grants list directly. + """ + if not grants: + return {} # No grants = private/owner-only + + result = { + 'read': {'group_ids': [], 'user_ids': []}, + 'write': {'group_ids': [], 'user_ids': []}, + } + + is_public = False + for grant in grants: + if grant.principal_type == 'user' and grant.principal_id == '*' and grant.permission == 'read': + is_public = True + continue # Don't add wildcard to user_ids list + + if grant.permission not in ('read', 'write'): + continue + + if grant.principal_type == 'group': + if grant.principal_id not in result[grant.permission]['group_ids']: + result[grant.permission]['group_ids'].append(grant.principal_id) + elif grant.principal_type == 'user': + if grant.principal_id not in result[grant.permission]['user_ids']: + result[grant.permission]['user_ids'].append(grant.principal_id) + + if is_public: + return None # Public read access + + return result + + +#################### +# Table Operations +#################### + + +class AccessGrantsTable: + async def grant_access( + self, + resource_type: str, + resource_id: str, + principal_type: str, + principal_id: str, + permission: str, + db: Optional[AsyncSession] = None, + ) -> Optional[AccessGrantModel]: + """Add a single access grant. Idempotent (ignores duplicates).""" + async with get_async_db_context(db) as db: + # Check for existing grant + result = await db.execute( + select(AccessGrant).filter_by( + resource_type=resource_type, + resource_id=resource_id, + principal_type=principal_type, + principal_id=principal_id, + permission=permission, + ) + ) + existing = result.scalars().first() + if existing: + return AccessGrantModel.model_validate(existing) + + grant = AccessGrant( + id=str(uuid.uuid4()), + resource_type=resource_type, + resource_id=resource_id, + principal_type=principal_type, + principal_id=principal_id, + permission=permission, + created_at=int(time.time()), + ) + db.add(grant) + await db.commit() + await db.refresh(grant) + return AccessGrantModel.model_validate(grant) + + async def revoke_access( + self, + resource_type: str, + resource_id: str, + principal_type: str, + principal_id: str, + permission: str, + db: Optional[AsyncSession] = None, + ) -> bool: + """Remove a single access grant.""" + async with get_async_db_context(db) as db: + result = await db.execute( + delete(AccessGrant).filter_by( + resource_type=resource_type, + resource_id=resource_id, + principal_type=principal_type, + principal_id=principal_id, + permission=permission, + ) + ) + await db.commit() + return result.rowcount > 0 + + async def revoke_all_access( + self, + resource_type: str, + resource_id: str, + db: Optional[AsyncSession] = None, + ) -> int: + """Remove all access grants for a resource.""" + async with get_async_db_context(db) as db: + result = await db.execute( + delete(AccessGrant).filter_by( + resource_type=resource_type, + resource_id=resource_id, + ) + ) + await db.commit() + return result.rowcount + + async def set_access_control( + self, + resource_type: str, + resource_id: str, + access_control: Optional[dict], + db: Optional[AsyncSession] = None, + ) -> list[AccessGrantModel]: + """ + Replace all grants for a resource from an access_control JSON dict. + This is the primary bridge for backward compat with the frontend. + """ + async with get_async_db_context(db) as db: + # Delete all existing grants for this resource + await db.execute( + delete(AccessGrant).filter_by( + resource_type=resource_type, + resource_id=resource_id, + ) + ) + + # Convert JSON to grant dicts + grant_dicts = access_control_to_grants(resource_type, resource_id, access_control) + + # Insert new grants + results = [] + for grant_dict in grant_dicts: + grant = AccessGrant( + id=str(uuid.uuid4()), + **grant_dict, + created_at=int(time.time()), + ) + db.add(grant) + results.append(grant) + + await db.commit() + + return [AccessGrantModel.model_validate(g) for g in results] + + async def set_access_grants( + self, + resource_type: str, + resource_id: str, + access_grants: Optional[list], + db: Optional[AsyncSession] = None, + ) -> list[AccessGrantModel]: + """ + Replace all grants for a resource from a direct access_grants list. + """ + async with get_async_db_context(db) as db: + await db.execute( + delete(AccessGrant).filter_by( + resource_type=resource_type, + resource_id=resource_id, + ) + ) + + normalized_grants = normalize_access_grants(access_grants) + + results = [] + for grant_dict in normalized_grants: + grant = AccessGrant( + id=str(uuid.uuid4()), + resource_type=resource_type, + resource_id=resource_id, + principal_type=grant_dict['principal_type'], + principal_id=grant_dict['principal_id'], + permission=grant_dict['permission'], + created_at=int(time.time()), + ) + db.add(grant) + results.append(grant) + + await db.commit() + return [AccessGrantModel.model_validate(g) for g in results] + + async def get_access_control( + self, + resource_type: str, + resource_id: str, + db: Optional[AsyncSession] = None, + ) -> Optional[dict]: + """ + Reconstruct the old-style access_control JSON dict from grants. + For backward compat with the frontend. + """ + async with get_async_db_context(db) as db: + result = await db.execute( + select(AccessGrant).filter_by( + resource_type=resource_type, + resource_id=resource_id, + ) + ) + grants = result.scalars().all() + grant_models = [AccessGrantModel.model_validate(g) for g in grants] + return grants_to_access_control(grant_models) + + async def get_grants_by_resource( + self, + resource_type: str, + resource_id: str, + db: Optional[AsyncSession] = None, + ) -> list[AccessGrantModel]: + """Get all grants for a specific resource.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(AccessGrant).filter_by( + resource_type=resource_type, + resource_id=resource_id, + ) + ) + grants = result.scalars().all() + return [AccessGrantModel.model_validate(g) for g in grants] + + async def get_grants_by_resources( + self, + resource_type: str, + resource_ids: list[str], + db: Optional[AsyncSession] = None, + ) -> dict[str, list[AccessGrantModel]]: + """Batch-fetch grants for multiple resources. Returns {resource_id: [grants]}.""" + if not resource_ids: + return {} + async with get_async_db_context(db) as db: + result = await db.execute( + select(AccessGrant).filter( + AccessGrant.resource_type == resource_type, + AccessGrant.resource_id.in_(resource_ids), + ) + ) + grants = result.scalars().all() + result_dict: dict[str, list[AccessGrantModel]] = {rid: [] for rid in resource_ids} + for g in grants: + result_dict[g.resource_id].append(AccessGrantModel.model_validate(g)) + return result_dict + + async def has_access( + self, + user_id: str, + resource_type: str, + resource_id: str, + permission: str = 'read', + user_group_ids: Optional[set[str]] = None, + db: Optional[AsyncSession] = None, + ) -> bool: + """ + Check if a user has the specified permission on a resource. + + Access is granted if any of the following is true: + - There's a grant for user:* (public) with the requested permission + - There's a grant for the specific user with the requested permission + - There's a grant for any of the user's groups with the requested permission + """ + async with get_async_db_context(db) as db: + # Build conditions for matching grants + conditions = [ + # Public access + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == '*', + ), + # Direct user access + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == user_id, + ), + ] + + # Group access + if user_group_ids is None: + from open_webui.models.groups import Groups + + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} + + if user_group_ids: + conditions.append( + and_( + AccessGrant.principal_type == 'group', + AccessGrant.principal_id.in_(user_group_ids), + ) + ) + + result = await db.execute( + select(AccessGrant) + .filter( + AccessGrant.resource_type == resource_type, + AccessGrant.resource_id == resource_id, + AccessGrant.permission == permission, + or_(*conditions), + ) + .limit(1) + ) + grant = result.scalars().first() + return grant is not None + + async def get_accessible_resource_ids( + self, + user_id: str, + resource_type: str, + resource_ids: list[str], + permission: str = 'read', + user_group_ids: Optional[set[str]] = None, + db: Optional[AsyncSession] = None, + ) -> set[str]: + """ + Batch check: return the subset of resource_ids that the user can access. + + This replaces calling has_access() in a loop (N+1) with a single query. + """ + if not resource_ids: + return set() + + async with get_async_db_context(db) as db: + conditions = [ + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == '*', + ), + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == user_id, + ), + ] + + if user_group_ids is None: + from open_webui.models.groups import Groups + + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} + + if user_group_ids: + conditions.append( + and_( + AccessGrant.principal_type == 'group', + AccessGrant.principal_id.in_(user_group_ids), + ) + ) + + result = await db.execute( + select(AccessGrant.resource_id) + .filter( + AccessGrant.resource_type == resource_type, + AccessGrant.resource_id.in_(resource_ids), + AccessGrant.permission == permission, + or_(*conditions), + ) + .distinct() + ) + rows = result.all() + return {row[0] for row in rows} + + async def get_users_with_access( + self, + resource_type: str, + resource_id: str, + permission: str = 'read', + db: Optional[AsyncSession] = None, + ) -> list: + """ + Get all users who have the specified permission on a resource. + Returns a list of UserModel instances. + """ + from open_webui.models.users import Users, UserModel + from open_webui.models.groups import Groups + + async with get_async_db_context(db) as db: + result = await db.execute( + select(AccessGrant).filter_by( + resource_type=resource_type, + resource_id=resource_id, + permission=permission, + ) + ) + grants = result.scalars().all() + + # Check for public access + for grant in grants: + if grant.principal_type == 'user' and grant.principal_id == '*': + result = await Users.get_users(filter={'roles': ['!pending']}, db=db) + return result.get('users', []) + + user_ids_with_access = set() + + for grant in grants: + if grant.principal_type == 'user': + user_ids_with_access.add(grant.principal_id) + elif grant.principal_type == 'group': + group_user_ids = await Groups.get_group_user_ids_by_id(grant.principal_id, db=db) + if group_user_ids: + user_ids_with_access.update(group_user_ids) + + if not user_ids_with_access: + return [] + + return await Users.get_users_by_user_ids(list(user_ids_with_access), db=db) + + def has_permission_filter( + self, + db, + query, + DocumentModel, + filter: dict, + resource_type: str, + permission: str = 'read', + ): + """ + Apply access control filtering to a SQLAlchemy query by JOINing with access_grant. + + This replaces the old JSON-column-based filtering with a proper relational JOIN. + + Note: This method builds SQLAlchemy expressions and does NOT perform I/O itself, + so it remains synchronous. The caller is responsible for executing the query + asynchronously with `await db.execute(...)`. + """ + group_ids = filter.get('group_ids', []) + user_id = filter.get('user_id') + + if permission == 'read_only': + return self._has_read_only_permission_filter(db, query, DocumentModel, filter, resource_type) + + # Build principal conditions + principal_conditions = [] + + if group_ids or user_id: + # Public access: user:* read + principal_conditions.append( + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == '*', + ) + ) + + if user_id: + # Owner always has access + principal_conditions.append(DocumentModel.user_id == user_id) + + # Direct user grant + principal_conditions.append( + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == user_id, + ) + ) + + if group_ids: + # Group grants + principal_conditions.append( + and_( + AccessGrant.principal_type == 'group', + AccessGrant.principal_id.in_(group_ids), + ) + ) + + if not principal_conditions: + return query + + # LEFT JOIN access_grant and filter + # We use a subquery approach to avoid duplicates from multiple matching grants + from sqlalchemy import exists as sa_exists + + grant_exists = ( + select(AccessGrant.id) + .where( + AccessGrant.resource_type == resource_type, + AccessGrant.resource_id == DocumentModel.id, + AccessGrant.permission == permission, + or_( + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == '*', + ), + *( + [ + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == user_id, + ) + ] + if user_id + else [] + ), + *( + [ + and_( + AccessGrant.principal_type == 'group', + AccessGrant.principal_id.in_(group_ids), + ) + ] + if group_ids + else [] + ), + ), + ) + .correlate(DocumentModel) + .exists() + ) + + # Owner OR has a matching grant + owner_or_grant = [grant_exists] + if user_id: + owner_or_grant.append(DocumentModel.user_id == user_id) + + query = query.filter(or_(*owner_or_grant)) + return query + + def _has_read_only_permission_filter( + self, + db, + query, + DocumentModel, + filter: dict, + resource_type: str, + ): + """ + Filter for items where user has read BUT NOT write access. + Public items are NOT considered read_only. + + Note: This method builds SQLAlchemy expressions and does NOT perform I/O itself, + so it remains synchronous. The caller is responsible for executing the query + asynchronously with `await db.execute(...)`. + """ + group_ids = filter.get('group_ids', []) + user_id = filter.get('user_id') + + from sqlalchemy import exists as sa_exists + + # Has read grant (not public) + read_grant_exists = ( + select(AccessGrant.id) + .where( + AccessGrant.resource_type == resource_type, + AccessGrant.resource_id == DocumentModel.id, + AccessGrant.permission == 'read', + or_( + *( + [ + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == user_id, + ) + ] + if user_id + else [] + ), + *( + [ + and_( + AccessGrant.principal_type == 'group', + AccessGrant.principal_id.in_(group_ids), + ) + ] + if group_ids + else [] + ), + ), + ) + .correlate(DocumentModel) + .exists() + ) + + # Does NOT have write grant + write_grant_exists = ( + select(AccessGrant.id) + .where( + AccessGrant.resource_type == resource_type, + AccessGrant.resource_id == DocumentModel.id, + AccessGrant.permission == 'write', + or_( + *( + [ + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == user_id, + ) + ] + if user_id + else [] + ), + *( + [ + and_( + AccessGrant.principal_type == 'group', + AccessGrant.principal_id.in_(group_ids), + ) + ] + if group_ids + else [] + ), + ), + ) + .correlate(DocumentModel) + .exists() + ) + + # Is NOT public + public_grant_exists = ( + select(AccessGrant.id) + .where( + AccessGrant.resource_type == resource_type, + AccessGrant.resource_id == DocumentModel.id, + AccessGrant.permission == 'read', + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == '*', + ) + .correlate(DocumentModel) + .exists() + ) + + conditions = [read_grant_exists, ~write_grant_exists, ~public_grant_exists] + + # Not owner + if user_id: + conditions.append(DocumentModel.user_id != user_id) + + query = query.filter(and_(*conditions)) + return query + + +AccessGrants = AccessGrantsTable() diff --git a/backend/open_webui/models/auths.py b/backend/open_webui/models/auths.py new file mode 100644 index 0000000000000000000000000000000000000000..2c8c6ba99fe5d8e8fe4553c8b89066249307c7e2 --- /dev/null +++ b/backend/open_webui/models/auths.py @@ -0,0 +1,212 @@ +import logging +import uuid +from typing import Optional + +from sqlalchemy import select, delete, update +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.models.users import User, UserModel, UserProfileImageResponse, Users +from open_webui.utils.validate import validate_profile_image_url +from pydantic import BaseModel, field_validator +from sqlalchemy import Boolean, Column, String, Text + +log = logging.getLogger(__name__) + +#################### +# DB MODEL +#################### + + +class Auth(Base): + __tablename__ = 'auth' + + id = Column(String, primary_key=True, unique=True) + email = Column(String) + password = Column(Text) + active = Column(Boolean) + + +class AuthModel(BaseModel): + id: str + email: str + password: str + active: bool = True + + +#################### +# Forms +#################### + + +class Token(BaseModel): + token: str + token_type: str + + +class ApiKey(BaseModel): + api_key: Optional[str] = None + + +class SigninResponse(Token, UserProfileImageResponse): + pass + + +class SigninForm(BaseModel): + email: str + password: str + + +class LdapForm(BaseModel): + user: str + password: str + + +class ProfileImageUrlForm(BaseModel): + profile_image_url: str + + +class UpdatePasswordForm(BaseModel): + password: str + new_password: str + + +class SignupForm(BaseModel): + name: str + email: str + password: str + profile_image_url: Optional[str] = '/user.png' + + @field_validator('profile_image_url') + @classmethod + def check_profile_image_url(cls, v: Optional[str]) -> Optional[str]: + if v is not None: + return validate_profile_image_url(v) + return v + + +class AddUserForm(SignupForm): + role: Optional[str] = 'pending' + + +class AuthsTable: + async def insert_new_auth( + self, + email: str, + password: str, + name: str, + profile_image_url: str = '/user.png', + role: str = 'pending', + oauth: Optional[dict] = None, + db: Optional[AsyncSession] = None, + ) -> Optional[UserModel]: + async with get_async_db_context(db) as db: + log.info('insert_new_auth') + + id = str(uuid.uuid4()) + + auth = AuthModel(**{'id': id, 'email': email, 'password': password, 'active': True}) + result = Auth(**auth.model_dump()) + db.add(result) + + user = await Users.insert_new_user(id, name, email, profile_image_url, role, oauth=oauth, db=db) + + await db.commit() + await db.refresh(result) + + if result and user: + return user + else: + return None + + async def authenticate_user( + self, email: str, verify_password: callable, db: Optional[AsyncSession] = None + ) -> Optional[UserModel]: + log.info(f'authenticate_user: {email}') + + user = await Users.get_user_by_email(email, db=db) + if not user: + return None + + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Auth).filter_by(id=user.id, active=True)) + auth = result.scalars().first() + if auth: + if verify_password(auth.password): + return user + else: + return None + else: + return None + except Exception: + return None + + async def authenticate_user_by_api_key( + self, api_key: str, db: Optional[AsyncSession] = None + ) -> Optional[UserModel]: + log.info(f'authenticate_user_by_api_key') + # if no api_key, return None + if not api_key: + return None + + try: + user = await Users.get_user_by_api_key(api_key, db=db) + return user if user else None + except Exception: + return False + + async def authenticate_user_by_email(self, email: str, db: Optional[AsyncSession] = None) -> Optional[UserModel]: + log.info(f'authenticate_user_by_email: {email}') + try: + async with get_async_db_context(db) as db: + # Single JOIN query instead of two separate queries + result = await db.execute( + select(Auth, User).join(User, Auth.id == User.id).filter(Auth.email == email, Auth.active == True) + ) + row = result.first() + if row: + _, user = row + return UserModel.model_validate(user) + return None + except Exception: + return None + + async def update_user_password_by_id(self, id: str, new_password: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + result = await db.execute(update(Auth).filter_by(id=id).values(password=new_password)) + await db.commit() + return True if result.rowcount == 1 else False + except Exception: + return False + + async def update_email_by_id(self, id: str, email: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + result = await db.execute(update(Auth).filter_by(id=id).values(email=email)) + await db.commit() + if result.rowcount == 1: + await Users.update_user_by_id(id, {'email': email}, db=db) + return True + return False + except Exception: + return False + + async def delete_auth_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + # Delete User + result = await Users.delete_user_by_id(id, db=db) + + if result: + await db.execute(delete(Auth).filter_by(id=id)) + await db.commit() + + return True + else: + return False + except Exception: + return False + + +Auths = AuthsTable() diff --git a/backend/open_webui/models/automations.py b/backend/open_webui/models/automations.py new file mode 100644 index 0000000000000000000000000000000000000000..05f449ad13c7562388f605be9dfccad9b886f05a --- /dev/null +++ b/backend/open_webui/models/automations.py @@ -0,0 +1,421 @@ +import time +import logging +from typing import Optional +from uuid import uuid4 + +from pydantic import BaseModel, ConfigDict +from sqlalchemy import Column, Text, JSON, Boolean, BigInteger, Index, select, or_, func, cast, String, delete, update +from sqlalchemy.ext.asyncio import AsyncSession + +from open_webui.internal.db import Base, get_async_db_context + +log = logging.getLogger(__name__) + + +#################### +# Automation DB Schema +#################### + + +class Automation(Base): + __tablename__ = 'automation' + + id = Column(Text, primary_key=True) + user_id = Column(Text, nullable=False) + name = Column(Text, nullable=False) + data = Column(JSON, nullable=False) # {prompt, model_id, rrule} + meta = Column(JSON, nullable=True) + is_active = Column(Boolean, nullable=False, default=True) + last_run_at = Column(BigInteger, nullable=True) + next_run_at = Column(BigInteger, nullable=True) + + created_at = Column(BigInteger, nullable=False) + updated_at = Column(BigInteger, nullable=False) + + __table_args__ = (Index('ix_automation_next_run', 'next_run_at'),) + + +class AutomationRun(Base): + __tablename__ = 'automation_run' + + id = Column(Text, primary_key=True) + automation_id = Column(Text, nullable=False) + chat_id = Column(Text, nullable=True) + status = Column(Text, nullable=False) # success | error + error = Column(Text, nullable=True) + created_at = Column(BigInteger, nullable=False) + + __table_args__ = ( + Index('ix_automation_run_automation_id', 'automation_id'), + Index('ix_automation_run_aid_created', 'automation_id', 'created_at'), + ) + + +#################### +# Pydantic Models +#################### + + +class AutomationTerminalConfig(BaseModel): + server_id: str + cwd: Optional[str] = None + + +class AutomationData(BaseModel): + prompt: str + model_id: str + rrule: str + terminal: Optional[AutomationTerminalConfig] = None + + +class AutomationModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + user_id: str + name: str + data: dict + meta: Optional[dict] = None + is_active: bool + last_run_at: Optional[int] = None + next_run_at: Optional[int] = None + + created_at: int + updated_at: int + + +class AutomationRunModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + automation_id: str + chat_id: Optional[str] = None + status: str + error: Optional[str] = None + created_at: int + + +class AutomationForm(BaseModel): + name: str + data: AutomationData + meta: Optional[dict] = None + is_active: Optional[bool] = True + + +class AutomationResponse(AutomationModel): + last_run: Optional[AutomationRunModel] = None + next_runs: Optional[list[int]] = None + + +class AutomationListResponse(BaseModel): + items: list[AutomationModel] + total: int + + +#################### +# AutomationTable +#################### + + +class AutomationTable: + async def insert( + self, + user_id: str, + form: AutomationForm, + next_run_at: int, + db: Optional[AsyncSession] = None, + ) -> AutomationModel: + async with get_async_db_context(db) as db: + now = int(time.time_ns()) + row = Automation( + id=str(uuid4()), + user_id=user_id, + name=form.name, + data=form.data.model_dump(), + meta=form.meta, + is_active=form.is_active, + next_run_at=next_run_at, + created_at=now, + updated_at=now, + ) + db.add(row) + await db.commit() + await db.refresh(row) + return AutomationModel.model_validate(row) + + async def count_by_user(self, user_id: str, db: Optional[AsyncSession] = None) -> int: + async with get_async_db_context(db) as db: + result = await db.execute(select(func.count()).select_from(Automation).filter_by(user_id=user_id)) + return result.scalar() + + async def get_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[AutomationModel]: + async with get_async_db_context(db) as db: + row = await db.get(Automation, id) + return AutomationModel.model_validate(row) if row else None + + async def get_active_by_user(self, user_id: str, db: Optional[AsyncSession] = None) -> list[AutomationModel]: + """Get active automations for a user (for calendar RRULE expansion).""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(Automation).filter_by(user_id=user_id, is_active=True).order_by(Automation.created_at.desc()) + ) + return [AutomationModel.model_validate(r) for r in result.scalars().all()] + + async def search_automations( + self, + user_id: str, + query: Optional[str] = None, + status: Optional[str] = None, + skip: int = 0, + limit: int = 30, + db: Optional[AsyncSession] = None, + ) -> 'AutomationListResponse': + async with get_async_db_context(db) as db: + stmt = select(Automation).filter_by(user_id=user_id) + + if query: + search = f'%{query}%' + # Search in name and prompt inside JSON data + stmt = stmt.filter( + or_( + Automation.name.ilike(search), + cast(Automation.data, String).ilike(search), + ) + ) + + if status == 'active': + stmt = stmt.filter(Automation.is_active == True) + elif status == 'paused': + stmt = stmt.filter(Automation.is_active == False) + + stmt = stmt.order_by(Automation.created_at.desc()) + + # Get total count + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + rows = result.scalars().all() + return AutomationListResponse( + items=[AutomationModel.model_validate(r) for r in rows], + total=total, + ) + + async def update_by_id( + self, + id: str, + form: AutomationForm, + next_run_at: int, + db: Optional[AsyncSession] = None, + ) -> Optional[AutomationModel]: + async with get_async_db_context(db) as db: + row = await db.get(Automation, id) + if not row: + return None + row.name = form.name + row.data = form.data.model_dump() + row.meta = form.meta + if form.is_active is not None: + row.is_active = form.is_active + row.next_run_at = next_run_at + row.updated_at = int(time.time_ns()) + await db.commit() + await db.refresh(row) + return AutomationModel.model_validate(row) + + async def toggle( + self, + id: str, + next_run_at: Optional[int], + db: Optional[AsyncSession] = None, + ) -> Optional[AutomationModel]: + async with get_async_db_context(db) as db: + row = await db.get(Automation, id) + if not row: + return None + row.is_active = not row.is_active + row.next_run_at = next_run_at if row.is_active else None + row.updated_at = int(time.time_ns()) + await db.commit() + await db.refresh(row) + return AutomationModel.model_validate(row) + + async def delete(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + row = await db.get(Automation, id) + if not row: + return False + await db.delete(row) + await db.commit() + return True + + async def claim_due(self, now_ns: int, limit: int = 10, db: Optional[AsyncSession] = None) -> list[AutomationModel]: + """ + Atomically claim due automations for execution. + + Advances next_run_at immediately so the row can never be + double-claimed. On PostgreSQL, uses FOR UPDATE SKIP LOCKED + for zero-contention distributed work claiming. + """ + async with get_async_db_context(db) as db: + stmt = ( + select(Automation) + .where( + Automation.is_active == True, + Automation.next_run_at <= now_ns, + ) + .order_by(Automation.next_run_at) + .limit(limit) + ) + + if db.bind.dialect.name == 'postgresql': + stmt = stmt.with_for_update(skip_locked=True) + + result = await db.execute(stmt) + rows = result.scalars().all() + + from open_webui.utils.automations import next_run_ns + + # Batch-fetch user timezones so rescheduling respects each + # user's local timezone instead of falling back to server time. + user_ids = list({row.user_id for row in rows}) + timezone_by_user_id: dict[str, Optional[str]] = {} + if user_ids: + from open_webui.models.users import User + + tz_result = await db.execute(select(User.id, User.timezone).where(User.id.in_(user_ids))) + timezone_by_user_id = {uid: tz for uid, tz in tz_result.all()} + + for row in rows: + row.last_run_at = now_ns + row.next_run_at = next_run_ns(row.data.get('rrule', ''), tz=timezone_by_user_id.get(row.user_id)) + + await db.commit() + + return [AutomationModel.model_validate(r) for r in rows] + + +#################### +# AutomationRunTable +#################### + + +class AutomationRunTable: + async def insert( + self, + automation_id: str, + status: str, + chat_id: Optional[str] = None, + error: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> AutomationRunModel: + async with get_async_db_context(db) as db: + row = AutomationRun( + id=str(uuid4()), + automation_id=automation_id, + chat_id=chat_id, + status=status, + error=error, + created_at=int(time.time_ns()), + ) + db.add(row) + await db.commit() + await db.refresh(row) + return AutomationRunModel.model_validate(row) + + async def get_latest(self, automation_id: str, db: Optional[AsyncSession] = None) -> Optional[AutomationRunModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(AutomationRun) + .filter_by(automation_id=automation_id) + .order_by(AutomationRun.created_at.desc()) + .limit(1) + ) + row = result.scalars().first() + return AutomationRunModel.model_validate(row) if row else None + + async def get_latest_batch( + self, automation_ids: list[str], db: Optional[AsyncSession] = None + ) -> dict[str, AutomationRunModel]: + """Fetch the latest run for each automation in a single query.""" + if not automation_ids: + return {} + async with get_async_db_context(db) as db: + # Subquery: max created_at per automation_id + subq = ( + select( + AutomationRun.automation_id, + func.max(AutomationRun.created_at).label('max_created'), + ) + .filter(AutomationRun.automation_id.in_(automation_ids)) + .group_by(AutomationRun.automation_id) + .subquery() + ) + result = await db.execute( + select(AutomationRun).join( + subq, + (AutomationRun.automation_id == subq.c.automation_id) + & (AutomationRun.created_at == subq.c.max_created), + ) + ) + rows = result.scalars().all() + return {row.automation_id: AutomationRunModel.model_validate(row) for row in rows} + + async def get_by_automation( + self, + automation_id: str, + skip: int = 0, + limit: int = 50, + db: Optional[AsyncSession] = None, + ) -> list[AutomationRunModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(AutomationRun) + .filter_by(automation_id=automation_id) + .order_by(AutomationRun.created_at.desc()) + .offset(skip) + .limit(limit) + ) + rows = result.scalars().all() + return [AutomationRunModel.model_validate(r) for r in rows] + + async def delete_by_automation(self, automation_id: str, db: Optional[AsyncSession] = None) -> int: + async with get_async_db_context(db) as db: + result = await db.execute(delete(AutomationRun).filter_by(automation_id=automation_id)) + await db.commit() + return result.rowcount + + async def get_runs_by_user_range( + self, + user_id: str, + start_ns: int, + end_ns: int, + limit: int = 500, + db: Optional[AsyncSession] = None, + ) -> list[tuple['AutomationRunModel', 'AutomationModel']]: + """Get runs within a date range for a user, joined with parent automation.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(AutomationRun, Automation) + .join(Automation, Automation.id == AutomationRun.automation_id) + .filter( + Automation.user_id == user_id, + AutomationRun.created_at >= start_ns, + AutomationRun.created_at < end_ns, + ) + .order_by(AutomationRun.created_at.desc()) + .limit(limit) + ) + return [ + (AutomationRunModel.model_validate(run), AutomationModel.model_validate(auto)) + for run, auto in result.all() + ] + + +Automations = AutomationTable() +AutomationRuns = AutomationRunTable() diff --git a/backend/open_webui/models/calendar.py b/backend/open_webui/models/calendar.py new file mode 100644 index 0000000000000000000000000000000000000000..47f0a6f722405dff853950214c0f849aa5ed3e68 --- /dev/null +++ b/backend/open_webui/models/calendar.py @@ -0,0 +1,822 @@ +import time +import logging +from typing import Optional +from uuid import uuid4 + +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy import ( + Column, + Text, + JSON, + Boolean, + BigInteger, + Index, + UniqueConstraint, + select, + or_, + exists, + func, + delete, + update, +) +from sqlalchemy.ext.asyncio import AsyncSession + +from open_webui.internal.db import Base, get_async_db_context +from open_webui.models.access_grants import AccessGrantModel, AccessGrants +from open_webui.models.groups import Groups +from open_webui.models.users import User, UserModel, UserResponse + +log = logging.getLogger(__name__) + + +#################### +# Calendar DB Schema +#################### + + +class Calendar(Base): + __tablename__ = 'calendar' + + id = Column(Text, primary_key=True) + user_id = Column(Text, nullable=False) + name = Column(Text, nullable=False) + color = Column(Text, nullable=True) + is_default = Column(Boolean, nullable=False, default=False) + data = Column(JSON, nullable=True) + meta = Column(JSON, nullable=True) + + created_at = Column(BigInteger, nullable=False) + updated_at = Column(BigInteger, nullable=False) + + __table_args__ = (Index('ix_calendar_user', 'user_id'),) + + +class CalendarEvent(Base): + __tablename__ = 'calendar_event' + + id = Column(Text, primary_key=True) + calendar_id = Column(Text, nullable=False) + user_id = Column(Text, nullable=False) + title = Column(Text, nullable=False) + description = Column(Text, nullable=True) + start_at = Column(BigInteger, nullable=False) + end_at = Column(BigInteger, nullable=True) + all_day = Column(Boolean, nullable=False, default=False) + rrule = Column(Text, nullable=True) + color = Column(Text, nullable=True) + location = Column(Text, nullable=True) + data = Column(JSON, nullable=True) + meta = Column(JSON, nullable=True) + is_cancelled = Column(Boolean, nullable=False, default=False) + + created_at = Column(BigInteger, nullable=False) + updated_at = Column(BigInteger, nullable=False) + + __table_args__ = ( + Index('ix_calendar_event_calendar', 'calendar_id', 'start_at'), + Index('ix_calendar_event_user_date', 'user_id', 'start_at'), + ) + + +class CalendarEventAttendee(Base): + __tablename__ = 'calendar_event_attendee' + + id = Column(Text, primary_key=True) + event_id = Column(Text, nullable=False) + user_id = Column(Text, nullable=False) + status = Column(Text, nullable=False, default='pending') + meta = Column(JSON, nullable=True) + + created_at = Column(BigInteger, nullable=False) + updated_at = Column(BigInteger, nullable=False) + + __table_args__ = ( + UniqueConstraint('event_id', 'user_id', name='uq_event_attendee'), + Index('ix_calendar_event_attendee_user', 'user_id', 'status'), + ) + + +#################### +# Pydantic Models +#################### + + +class CalendarModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + user_id: str + name: str + color: Optional[str] = None + is_default: bool = False + is_system: bool = False + + data: Optional[dict] = None + meta: Optional[dict] = None + + access_grants: list[AccessGrantModel] = Field(default_factory=list) + + created_at: int + updated_at: int + + +class CalendarEventModel(BaseModel): + model_config = ConfigDict(from_attributes=True, extra='allow') + + id: str + calendar_id: str + user_id: str + title: str + description: Optional[str] = None + start_at: int + end_at: Optional[int] = None + all_day: bool = False + rrule: Optional[str] = None + color: Optional[str] = None + location: Optional[str] = None + data: Optional[dict] = None + meta: Optional[dict] = None + is_cancelled: bool = False + + attendees: list['CalendarEventAttendeeModel'] = Field(default_factory=list) + + created_at: int + updated_at: int + + +class CalendarEventAttendeeModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + event_id: str + user_id: str + status: str = 'pending' + meta: Optional[dict] = None + + created_at: int + updated_at: int + + +#################### +# Forms +#################### + + +class CalendarForm(BaseModel): + name: str + color: Optional[str] = None + data: Optional[dict] = None + meta: Optional[dict] = None + access_grants: Optional[list[dict]] = None + + +class CalendarUpdateForm(BaseModel): + name: Optional[str] = None + color: Optional[str] = None + data: Optional[dict] = None + meta: Optional[dict] = None + access_grants: Optional[list[dict]] = None + + +class CalendarEventForm(BaseModel): + calendar_id: str + title: str + description: Optional[str] = None + start_at: int + end_at: Optional[int] = None + all_day: bool = False + rrule: Optional[str] = None + color: Optional[str] = None + location: Optional[str] = None + data: Optional[dict] = None + meta: Optional[dict] = None + attendees: Optional[list[dict]] = None + + +class CalendarEventUpdateForm(BaseModel): + calendar_id: Optional[str] = None + title: Optional[str] = None + description: Optional[str] = None + start_at: Optional[int] = None + end_at: Optional[int] = None + all_day: Optional[bool] = None + rrule: Optional[str] = None + color: Optional[str] = None + location: Optional[str] = None + data: Optional[dict] = None + meta: Optional[dict] = None + is_cancelled: Optional[bool] = None + attendees: Optional[list[dict]] = None + + +class RSVPForm(BaseModel): + status: str # 'accepted' | 'declined' | 'tentative' | 'pending' + + +#################### +# Response Models +#################### + + +class CalendarEventUserResponse(CalendarEventModel): + user: Optional[UserResponse] = None + + +class CalendarEventListResponse(BaseModel): + items: list[CalendarEventUserResponse] + total: int + + +#################### +# Table Operations +#################### + + +class CalendarTable: + async def _get_access_grants(self, calendar_id: str, db: Optional[AsyncSession] = None) -> list[AccessGrantModel]: + return await AccessGrants.get_grants_by_resource('calendar', calendar_id, db=db) + + async def _to_calendar_model( + self, + cal: Calendar, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[AsyncSession] = None, + ) -> CalendarModel: + cal_data = CalendarModel.model_validate(cal).model_dump(exclude={'access_grants'}) + cal_data['access_grants'] = ( + access_grants if access_grants is not None else await self._get_access_grants(cal_data['id'], db=db) + ) + return CalendarModel.model_validate(cal_data) + + async def get_or_create_defaults(self, user_id: str, db: Optional[AsyncSession] = None) -> list[CalendarModel]: + """Return user's calendars, creating 'Personal' default if none exist.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(Calendar).filter(Calendar.user_id == user_id).order_by(Calendar.created_at.asc()) + ) + calendars = result.scalars().all() + + if calendars: + return [CalendarModel.model_validate(c) for c in calendars] + + now = int(time.time_ns()) + cal = Calendar( + id=str(uuid4()), + user_id=user_id, + name='Personal', + color='#3b82f6', + is_default=True, + created_at=now, + updated_at=now, + ) + db.add(cal) + await db.commit() + return [CalendarModel.model_validate(cal)] + + async def get_calendars_by_user(self, user_id: str, db: Optional[AsyncSession] = None) -> list[CalendarModel]: + """Owned + shared calendars.""" + async with get_async_db_context(db) as db: + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = [g.id for g in user_groups] + + stmt = select(Calendar) + stmt = AccessGrants.has_permission_filter( + db=db, + query=stmt, + DocumentModel=Calendar, + filter={'user_id': user_id, 'group_ids': user_group_ids}, + resource_type='calendar', + permission='read', + ) + stmt = stmt.order_by(Calendar.created_at.asc()) + + result = await db.execute(stmt) + calendars = result.scalars().all() + + if not calendars: + return await self.get_or_create_defaults(user_id, db=db) + + cal_ids = [c.id for c in calendars] + grants_map = await AccessGrants.get_grants_by_resources('calendar', cal_ids, db=db) + + return [await self._to_calendar_model(c, access_grants=grants_map.get(c.id, []), db=db) for c in calendars] + + async def get_calendar_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[CalendarModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Calendar).filter(Calendar.id == id)) + cal = result.scalars().first() + return await self._to_calendar_model(cal, db=db) if cal else None + + async def insert_new_calendar( + self, user_id: str, form_data: CalendarForm, db: Optional[AsyncSession] = None + ) -> Optional[CalendarModel]: + async with get_async_db_context(db) as db: + now = int(time.time_ns()) + cal = Calendar( + id=str(uuid4()), + user_id=user_id, + name=form_data.name, + color=form_data.color, + is_default=False, + data=form_data.data, + meta=form_data.meta, + created_at=now, + updated_at=now, + ) + db.add(cal) + await db.commit() + if form_data.access_grants is not None: + await AccessGrants.set_access_grants('calendar', cal.id, form_data.access_grants, db=db) + return await self._to_calendar_model(cal, db=db) + + async def update_calendar_by_id( + self, id: str, form_data: CalendarUpdateForm, db: Optional[AsyncSession] = None + ) -> Optional[CalendarModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Calendar).filter(Calendar.id == id)) + cal = result.scalars().first() + if not cal: + return None + + update_data = form_data.model_dump(exclude_unset=True) + if 'name' in update_data: + cal.name = update_data['name'] + if 'color' in update_data: + cal.color = update_data['color'] + if 'data' in update_data: + cal.data = {**(cal.data or {}), **update_data['data']} + if 'meta' in update_data: + cal.meta = {**(cal.meta or {}), **update_data['meta']} + if 'access_grants' in update_data: + await AccessGrants.set_access_grants('calendar', id, update_data['access_grants'], db=db) + + cal.updated_at = int(time.time_ns()) + await db.commit() + return await self._to_calendar_model(cal, db=db) + + async def set_default_calendar( + self, user_id: str, calendar_id: str, db: Optional[AsyncSession] = None + ) -> Optional[CalendarModel]: + """Set a calendar as the user's default, clearing all others.""" + async with get_async_db_context(db) as db: + # Clear all defaults for this user + await db.execute( + update(Calendar) + .where(Calendar.user_id == user_id, Calendar.is_default == True) + .values(is_default=False) + ) + # Set the new default + result = await db.execute(select(Calendar).filter(Calendar.id == calendar_id, Calendar.user_id == user_id)) + cal = result.scalars().first() + if not cal: + return None + cal.is_default = True + cal.updated_at = int(time.time_ns()) + await db.commit() + return await self._to_calendar_model(cal, db=db) + + async def delete_calendar_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + """Delete a non-default calendar. Cascades to events, attendees, and grants.""" + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Calendar).filter(Calendar.id == id)) + cal = result.scalars().first() + if not cal or cal.is_default: + return False + + # Delete attendees for all events in this calendar + event_ids_result = await db.execute(select(CalendarEvent.id).filter(CalendarEvent.calendar_id == id)) + event_ids = [r[0] for r in event_ids_result.all()] + if event_ids: + await db.execute( + delete(CalendarEventAttendee).filter(CalendarEventAttendee.event_id.in_(event_ids)) + ) + + # Delete events + await db.execute(delete(CalendarEvent).filter(CalendarEvent.calendar_id == id)) + + # Delete access grants + await AccessGrants.revoke_all_access('calendar', id, db=db) + + # Delete calendar + await db.execute(delete(Calendar).filter(Calendar.id == id)) + await db.commit() + return True + except Exception: + return False + + +class CalendarEventTable: + async def _get_attendees( + self, event_id: str, db: Optional[AsyncSession] = None + ) -> list[CalendarEventAttendeeModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id)) + rows = result.scalars().all() + return [CalendarEventAttendeeModel.model_validate(r) for r in rows] + + async def _to_event_model( + self, + event: CalendarEvent, + attendees: Optional[list[CalendarEventAttendeeModel]] = None, + db: Optional[AsyncSession] = None, + ) -> CalendarEventModel: + event_data = CalendarEventModel.model_validate(event).model_dump(exclude={'attendees'}) + event_data['attendees'] = ( + attendees if attendees is not None else await self._get_attendees(event_data['id'], db=db) + ) + return CalendarEventModel.model_validate(event_data) + + async def insert_new_event( + self, user_id: str, form_data: CalendarEventForm, db: Optional[AsyncSession] = None + ) -> Optional[CalendarEventModel]: + async with get_async_db_context(db) as db: + now = int(time.time_ns()) + event = CalendarEvent( + id=str(uuid4()), + calendar_id=form_data.calendar_id, + user_id=user_id, + title=form_data.title, + description=form_data.description, + start_at=form_data.start_at, + end_at=form_data.end_at, + all_day=form_data.all_day, + rrule=form_data.rrule, + color=form_data.color, + location=form_data.location, + data=form_data.data, + meta=form_data.meta, + is_cancelled=False, + created_at=now, + updated_at=now, + ) + db.add(event) + await db.commit() + + # Add attendees + if form_data.attendees: + await CalendarEventAttendees.set_attendees(event.id, form_data.attendees, db=db) + + return await self._to_event_model(event, db=db) + + async def get_event_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[CalendarEventModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(CalendarEvent).filter(CalendarEvent.id == id)) + event = result.scalars().first() + return await self._to_event_model(event, db=db) if event else None + + async def get_events_by_range( + self, + user_id: str, + start: int, + end: int, + calendar_ids: Optional[list[str]] = None, + db: Optional[AsyncSession] = None, + ) -> list[CalendarEventUserResponse]: + """Fetch events visible to user within a date range. + + Visible events = events in owned/shared calendars + events user attends. + Recurring events are fetched if they have any rrule (expansion in Python). + """ + async with get_async_db_context(db) as db: + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = [g.id for g in user_groups] + + # Get calendar IDs accessible to user + cal_stmt = select(Calendar.id) + cal_stmt = AccessGrants.has_permission_filter( + db=db, + query=cal_stmt, + DocumentModel=Calendar, + filter={'user_id': user_id, 'group_ids': user_group_ids}, + resource_type='calendar', + permission='read', + ) + cal_result = await db.execute(cal_stmt) + accessible_cal_ids = [r[0] for r in cal_result.all()] + + if calendar_ids: + # Filter to requested calendars only + accessible_cal_ids = [c for c in accessible_cal_ids if c in calendar_ids] + + # Also get event IDs where user is an attendee + attendee_event_ids_result = await db.execute( + select(CalendarEventAttendee.event_id).filter(CalendarEventAttendee.user_id == user_id) + ) + attendee_event_ids = [r[0] for r in attendee_event_ids_result.all()] + + # Build conditions for accessible events + conditions = [] + if accessible_cal_ids: + conditions.append(CalendarEvent.calendar_id.in_(accessible_cal_ids)) + if attendee_event_ids: + conditions.append(CalendarEvent.id.in_(attendee_event_ids)) + + if not conditions: + return [] + + # Build event query + stmt = ( + select(CalendarEvent, User) + .outerjoin(User, User.id == CalendarEvent.user_id) + .filter( + CalendarEvent.is_cancelled == False, + or_(*conditions), + or_( + # Non-recurring: overlaps the range + ( + CalendarEvent.rrule.is_(None) + & (CalendarEvent.start_at < end) + & or_( + CalendarEvent.end_at.is_(None) & (CalendarEvent.start_at >= start), + CalendarEvent.end_at.isnot(None) & (CalendarEvent.end_at > start), + ) + ), + # Recurring: fetch all (expansion in Python) + CalendarEvent.rrule.isnot(None), + ), + ) + .order_by(CalendarEvent.start_at.asc()) + ) + + result = await db.execute(stmt) + items = result.all() + + if not items: + return [] + + # Batch-load attendees for all events in one query (avoid N+1) + event_ids = [event.id for event, _user in items] + att_result = await db.execute( + select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id.in_(event_ids)) + ) + att_rows = att_result.scalars().all() + att_map: dict[str, list[CalendarEventAttendeeModel]] = {} + for a in att_rows: + att_map.setdefault(a.event_id, []).append(CalendarEventAttendeeModel.model_validate(a)) + + events = [] + for event, user in items: + event_data = CalendarEventModel.model_validate(event).model_dump(exclude={'attendees'}) + event_data['attendees'] = att_map.get(event.id, []) + events.append( + CalendarEventUserResponse( + **event_data, + user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None), + ) + ) + + return events + + async def search_events( + self, + user_id: str, + query: Optional[str] = None, + skip: int = 0, + limit: int = 30, + db: Optional[AsyncSession] = None, + ) -> CalendarEventListResponse: + async with get_async_db_context(db) as db: + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = [g.id for g in user_groups] + + # Get accessible calendar IDs + cal_stmt = select(Calendar.id) + cal_stmt = AccessGrants.has_permission_filter( + db=db, + query=cal_stmt, + DocumentModel=Calendar, + filter={'user_id': user_id, 'group_ids': user_group_ids}, + resource_type='calendar', + permission='read', + ) + cal_result = await db.execute(cal_stmt) + accessible_cal_ids = [r[0] for r in cal_result.all()] + if not accessible_cal_ids: + return CalendarEventListResponse(items=[], total=0) + + stmt = ( + select(CalendarEvent, User) + .outerjoin(User, User.id == CalendarEvent.user_id) + .filter( + CalendarEvent.is_cancelled == False, + CalendarEvent.calendar_id.in_(accessible_cal_ids), + ) + ) + + if query: + search = f'%{query}%' + stmt = stmt.filter( + or_( + CalendarEvent.title.ilike(search), + CalendarEvent.description.ilike(search), + CalendarEvent.location.ilike(search), + ) + ) + + stmt = stmt.order_by(CalendarEvent.start_at.desc()) + + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + items = result.all() + + if not items: + return CalendarEventListResponse(items=[], total=total) + + # Batch-load attendees + event_ids = [event.id for event, _user in items] + att_result = await db.execute( + select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id.in_(event_ids)) + ) + att_rows = att_result.scalars().all() + att_map: dict[str, list[CalendarEventAttendeeModel]] = {} + for a in att_rows: + att_map.setdefault(a.event_id, []).append(CalendarEventAttendeeModel.model_validate(a)) + + events = [] + for event, user in items: + event_data = CalendarEventModel.model_validate(event).model_dump(exclude={'attendees'}) + event_data['attendees'] = att_map.get(event.id, []) + events.append( + CalendarEventUserResponse( + **event_data, + user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None), + ) + ) + + return CalendarEventListResponse(items=events, total=total) + + async def update_event_by_id( + self, id: str, form_data: CalendarEventUpdateForm, db: Optional[AsyncSession] = None + ) -> Optional[CalendarEventModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(CalendarEvent).filter(CalendarEvent.id == id)) + event = result.scalars().first() + if not event: + return None + + update_data = form_data.model_dump(exclude_unset=True) + for field in [ + 'calendar_id', + 'title', + 'description', + 'start_at', + 'end_at', + 'all_day', + 'rrule', + 'color', + 'location', + 'is_cancelled', + ]: + if field in update_data: + setattr(event, field, update_data[field]) + + if 'data' in update_data and update_data['data'] is not None: + event.data = {**(event.data or {}), **update_data['data']} + if 'meta' in update_data and update_data['meta'] is not None: + event.meta = {**(event.meta or {}), **update_data['meta']} + + if 'attendees' in update_data and update_data['attendees'] is not None: + await CalendarEventAttendees.set_attendees(id, update_data['attendees'], db=db) + + event.updated_at = int(time.time_ns()) + await db.commit() + return await self._to_event_model(event, db=db) + + async def get_upcoming_events( + self, + now_ns: int, + default_lookahead_ns: int, + db: Optional[AsyncSession] = None, + ) -> list[tuple[CalendarEventModel, Optional[str]]]: + """Events starting between now and now + lookahead, for alert processing. + + Per-event lookahead is read from meta.alert_minutes (falls back to + default_lookahead_ns). Returns (event, user_timezone) pairs. + """ + from open_webui.models.users import User as UserRow + + # Use the maximum possible lookahead (60 min) to cast a wide net; + # per-event filtering happens in Python after fetching. + max_lookahead_ns = max(default_lookahead_ns, 60 * 60 * 1_000_000_000) + upper = now_ns + max_lookahead_ns + + async with get_async_db_context(db) as db: + result = await db.execute( + select(CalendarEvent, UserRow.timezone) + .outerjoin(UserRow, UserRow.id == CalendarEvent.user_id) + .filter( + CalendarEvent.is_cancelled == False, + CalendarEvent.start_at >= now_ns, + CalendarEvent.start_at <= upper, + ) + ) + rows = result.all() + + events = [] + for event, tz in rows: + model = CalendarEventModel.model_validate(event) + # Determine per-event alert window + alert_minutes = None + if model.meta and 'alert_minutes' in model.meta: + alert_minutes = model.meta['alert_minutes'] + + if alert_minutes is not None: + if alert_minutes < 0: + # alert_minutes < 0 means "no alert" + continue + event_lookahead_ns = alert_minutes * 60 * 1_000_000_000 + else: + event_lookahead_ns = default_lookahead_ns + + if model.start_at <= now_ns + event_lookahead_ns: + events.append((model, tz)) + + return events + + async def delete_event_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + await db.execute(delete(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == id)) + await db.execute(delete(CalendarEvent).filter(CalendarEvent.id == id)) + await db.commit() + return True + except Exception: + return False + + +class CalendarEventAttendeeTable: + async def set_attendees( + self, event_id: str, attendees: list[dict], db: Optional[AsyncSession] = None + ) -> list[CalendarEventAttendeeModel]: + """Replace all attendees for an event. + + Each dict in attendees: {user_id: str, status?: str, meta?: dict} + """ + async with get_async_db_context(db) as db: + # Remove existing + await db.execute(delete(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id)) + + now = int(time.time_ns()) + models = [] + for att in attendees: + row = CalendarEventAttendee( + id=str(uuid4()), + event_id=event_id, + user_id=att['user_id'], + status=att.get('status', 'pending'), + meta=att.get('meta'), + created_at=now, + updated_at=now, + ) + db.add(row) + models.append(CalendarEventAttendeeModel.model_validate(row)) + + await db.commit() + return models + + async def update_rsvp( + self, event_id: str, user_id: str, status: str, db: Optional[AsyncSession] = None + ) -> Optional[CalendarEventAttendeeModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(CalendarEventAttendee).filter( + CalendarEventAttendee.event_id == event_id, + CalendarEventAttendee.user_id == user_id, + ) + ) + att = result.scalars().first() + if not att: + return None + + att.status = status + att.updated_at = int(time.time_ns()) + await db.commit() + return CalendarEventAttendeeModel.model_validate(att) + + async def get_attendees_by_event( + self, event_id: str, db: Optional[AsyncSession] = None + ) -> list[CalendarEventAttendeeModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id)) + return [CalendarEventAttendeeModel.model_validate(r) for r in result.scalars().all()] + + async def get_events_by_attendee(self, user_id: str, db: Optional[AsyncSession] = None) -> list[str]: + """Return event IDs where user is an attendee.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(CalendarEventAttendee.event_id).filter(CalendarEventAttendee.user_id == user_id) + ) + return [r[0] for r in result.all()] + + +Calendars = CalendarTable() +CalendarEvents = CalendarEventTable() +CalendarEventAttendees = CalendarEventAttendeeTable() diff --git a/backend/open_webui/models/channels.py b/backend/open_webui/models/channels.py new file mode 100644 index 0000000000000000000000000000000000000000..942c06d6b3cd4ee44d1bff547bdd1308aaafa02d --- /dev/null +++ b/backend/open_webui/models/channels.py @@ -0,0 +1,999 @@ +import json +import secrets +import time +import uuid +from typing import Optional + +from sqlalchemy import select, delete, update, func, case, or_, and_ +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.models.groups import Groups +from open_webui.models.access_grants import ( + AccessGrantModel, + AccessGrants, +) + +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy.dialects.postgresql import JSONB + + +from sqlalchemy import ( + BigInteger, + Boolean, + Column, + ForeignKey, + String, + Text, + JSON, + UniqueConstraint, +) + +#################### +# Channel DB Schema +#################### + + +class Channel(Base): + __tablename__ = 'channel' + + id = Column(Text, primary_key=True, unique=True) + user_id = Column(Text) + type = Column(Text, nullable=True) + + name = Column(Text) + description = Column(Text, nullable=True) + + # Used to indicate if the channel is private (for 'group' type channels) + is_private = Column(Boolean, nullable=True) + + data = Column(JSON, nullable=True) + meta = Column(JSON, nullable=True) + + created_at = Column(BigInteger) + + updated_at = Column(BigInteger) + updated_by = Column(Text, nullable=True) + + archived_at = Column(BigInteger, nullable=True) + archived_by = Column(Text, nullable=True) + + deleted_at = Column(BigInteger, nullable=True) + deleted_by = Column(Text, nullable=True) + + +class ChannelModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + user_id: str + + type: Optional[str] = None + + name: str + description: Optional[str] = None + + is_private: Optional[bool] = None + + data: Optional[dict] = None + meta: Optional[dict] = None + access_grants: list[AccessGrantModel] = Field(default_factory=list) + + created_at: int # timestamp in epoch (time_ns) + + updated_at: int # timestamp in epoch (time_ns) + updated_by: Optional[str] = None + + archived_at: Optional[int] = None # timestamp in epoch (time_ns) + archived_by: Optional[str] = None + + deleted_at: Optional[int] = None # timestamp in epoch (time_ns) + deleted_by: Optional[str] = None + + +class ChannelMember(Base): + __tablename__ = 'channel_member' + + id = Column(Text, primary_key=True, unique=True) + channel_id = Column(Text, nullable=False) + user_id = Column(Text, nullable=False) + + role = Column(Text, nullable=True) + status = Column(Text, nullable=True) + + is_active = Column(Boolean, nullable=False, default=True) + + is_channel_muted = Column(Boolean, nullable=False, default=False) + is_channel_pinned = Column(Boolean, nullable=False, default=False) + + data = Column(JSON, nullable=True) + meta = Column(JSON, nullable=True) + + invited_at = Column(BigInteger, nullable=True) + invited_by = Column(Text, nullable=True) + + joined_at = Column(BigInteger) + left_at = Column(BigInteger, nullable=True) + + last_read_at = Column(BigInteger, nullable=True) + + created_at = Column(BigInteger) + updated_at = Column(BigInteger) + + +class ChannelMemberModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + channel_id: str + user_id: str + + role: Optional[str] = None + status: Optional[str] = None + + is_active: bool = True + + is_channel_muted: bool = False + is_channel_pinned: bool = False + + data: Optional[dict] = None + meta: Optional[dict] = None + + invited_at: Optional[int] = None # timestamp in epoch (time_ns) + invited_by: Optional[str] = None + + joined_at: Optional[int] = None # timestamp in epoch (time_ns) + left_at: Optional[int] = None # timestamp in epoch (time_ns) + + last_read_at: Optional[int] = None # timestamp in epoch (time_ns) + + created_at: Optional[int] = None # timestamp in epoch (time_ns) + updated_at: Optional[int] = None # timestamp in epoch (time_ns) + + +class ChannelFile(Base): + __tablename__ = 'channel_file' + + id = Column(Text, unique=True, primary_key=True) + user_id = Column(Text, nullable=False) + + channel_id = Column(Text, ForeignKey('channel.id', ondelete='CASCADE'), nullable=False) + message_id = Column(Text, ForeignKey('message.id', ondelete='CASCADE'), nullable=True) + file_id = Column(Text, ForeignKey('file.id', ondelete='CASCADE'), nullable=False) + + created_at = Column(BigInteger, nullable=False) + updated_at = Column(BigInteger, nullable=False) + + __table_args__ = (UniqueConstraint('channel_id', 'file_id', name='uq_channel_file_channel_file'),) + + +class ChannelFileModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + + channel_id: str + file_id: str + user_id: str + + created_at: int # timestamp in epoch (time_ns) + updated_at: int # timestamp in epoch (time_ns) + + +class ChannelWebhook(Base): + __tablename__ = 'channel_webhook' + + id = Column(Text, primary_key=True, unique=True) + channel_id = Column(Text, nullable=False) + user_id = Column(Text, nullable=False) + + name = Column(Text, nullable=False) + profile_image_url = Column(Text, nullable=True) + + token = Column(Text, nullable=False) + last_used_at = Column(BigInteger, nullable=True) + + created_at = Column(BigInteger, nullable=False) + updated_at = Column(BigInteger, nullable=False) + + +class ChannelWebhookModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + channel_id: str + user_id: str + + name: str + profile_image_url: Optional[str] = None + + token: str + last_used_at: Optional[int] = None # timestamp in epoch (time_ns) + + created_at: int # timestamp in epoch (time_ns) + updated_at: int # timestamp in epoch (time_ns) + + +#################### +# Forms +#################### + + +class ChannelResponse(ChannelModel): + is_manager: bool = False + write_access: bool = False + + user_count: Optional[int] = None + + +class ChannelForm(BaseModel): + name: str = '' + description: Optional[str] = None + is_private: Optional[bool] = None + data: Optional[dict] = None + meta: Optional[dict] = None + access_grants: Optional[list[dict]] = None + group_ids: Optional[list[str]] = None + user_ids: Optional[list[str]] = None + + +class CreateChannelForm(ChannelForm): + type: Optional[str] = None + + +class ChannelWebhookForm(BaseModel): + name: str + profile_image_url: Optional[str] = None + + +class ChannelTable: + async def _get_access_grants(self, channel_id: str, db: Optional[AsyncSession] = None) -> list[AccessGrantModel]: + return await AccessGrants.get_grants_by_resource('channel', channel_id, db=db) + + async def _to_channel_model( + self, + channel: Channel, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[AsyncSession] = None, + ) -> ChannelModel: + channel_data = ChannelModel.model_validate(channel).model_dump(exclude={'access_grants'}) + channel_data['access_grants'] = ( + access_grants if access_grants is not None else await self._get_access_grants(channel_data['id'], db=db) + ) + return ChannelModel.model_validate(channel_data) + + async def _collect_unique_user_ids( + self, + invited_by: str, + user_ids: Optional[list[str]] = None, + group_ids: Optional[list[str]] = None, + ) -> set[str]: + """ + Collect unique user ids from: + - invited_by + - user_ids + - each group in group_ids + Returns a set for efficient SQL diffing. + """ + users = set(user_ids or []) + users.add(invited_by) + + for group_id in group_ids or []: + group_user_ids = await Groups.get_group_user_ids_by_id(group_id) + users.update(group_user_ids) + + return users + + def _create_membership_models( + self, + channel_id: str, + invited_by: str, + user_ids: set[str], + ) -> list[ChannelMember]: + """ + Takes a set of NEW user IDs (already filtered to exclude existing members). + Returns ORM ChannelMember objects to be added. + """ + now = int(time.time_ns()) + memberships = [] + + for uid in user_ids: + model = ChannelMemberModel( + **{ + 'id': str(uuid.uuid4()), + 'channel_id': channel_id, + 'user_id': uid, + 'status': 'joined', + 'is_active': True, + 'is_channel_muted': False, + 'is_channel_pinned': False, + 'invited_at': now, + 'invited_by': invited_by, + 'joined_at': now, + 'left_at': None, + 'last_read_at': now, + 'created_at': now, + 'updated_at': now, + } + ) + memberships.append(ChannelMember(**model.model_dump())) + + return memberships + + def _has_permission(self, db, query, filter: dict, permission: str = 'read'): + return AccessGrants.has_permission_filter( + db=db, + query=query, + DocumentModel=Channel, + filter=filter, + resource_type='channel', + permission=permission, + ) + + async def insert_new_channel( + self, form_data: CreateChannelForm, user_id: str, db: Optional[AsyncSession] = None + ) -> Optional[ChannelModel]: + async with get_async_db_context(db) as db: + channel = ChannelModel( + **{ + **form_data.model_dump(exclude={'access_grants'}), + 'type': form_data.type if form_data.type else None, + 'name': form_data.name.lower(), + 'id': str(uuid.uuid4()), + 'user_id': user_id, + 'created_at': int(time.time_ns()), + 'updated_at': int(time.time_ns()), + 'access_grants': [], + } + ) + new_channel = Channel(**channel.model_dump(exclude={'access_grants'})) + + if form_data.type in ['group', 'dm']: + users = await self._collect_unique_user_ids( + invited_by=user_id, + user_ids=form_data.user_ids, + group_ids=form_data.group_ids, + ) + memberships = self._create_membership_models( + channel_id=new_channel.id, + invited_by=user_id, + user_ids=users, + ) + + db.add_all(memberships) + db.add(new_channel) + await db.commit() + await AccessGrants.set_access_grants('channel', new_channel.id, form_data.access_grants, db=db) + return await self._to_channel_model(new_channel, db=db) + + async def get_channels(self, db: Optional[AsyncSession] = None) -> list[ChannelModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Channel)) + channels = result.scalars().all() + channel_ids = [channel.id for channel in channels] + grants_map = await AccessGrants.get_grants_by_resources('channel', channel_ids, db=db) + return [ + await self._to_channel_model( + channel, + access_grants=grants_map.get(channel.id, []), + db=db, + ) + for channel in channels + ] + + async def get_channels_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> list[ChannelModel]: + async with get_async_db_context(db) as db: + user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id, db=db)] + + result = await db.execute( + select(Channel) + .join(ChannelMember, Channel.id == ChannelMember.channel_id) + .filter( + Channel.deleted_at.is_(None), + Channel.archived_at.is_(None), + Channel.type.in_(['group', 'dm']), + ChannelMember.user_id == user_id, + ChannelMember.is_active.is_(True), + ) + ) + membership_channels = result.scalars().all() + + stmt = select(Channel).filter( + Channel.deleted_at.is_(None), + Channel.archived_at.is_(None), + or_( + Channel.type.is_(None), # True NULL/None + Channel.type == '', # Empty string + and_(Channel.type != 'group', Channel.type != 'dm'), + ), + ) + stmt = self._has_permission(db, stmt, {'user_id': user_id, 'group_ids': user_group_ids}) + + result = await db.execute(stmt) + standard_channels = result.scalars().all() + + all_channels = list(membership_channels) + list(standard_channels) + channel_ids = [c.id for c in all_channels] + grants_map = await AccessGrants.get_grants_by_resources('channel', channel_ids, db=db) + return [ + await self._to_channel_model(c, access_grants=grants_map.get(c.id, []), db=db) for c in all_channels + ] + + async def get_dm_channel_by_user_ids( + self, user_ids: list[str], db: Optional[AsyncSession] = None + ) -> Optional[ChannelModel]: + async with get_async_db_context(db) as db: + # Ensure uniqueness in case a list with duplicates is passed + unique_user_ids = list(set(user_ids)) + + match_count = func.sum( + case( + (ChannelMember.user_id.in_(unique_user_ids), 1), + else_=0, + ) + ) + + subquery = ( + select(ChannelMember.channel_id) + .group_by(ChannelMember.channel_id) + # 1. Channel must have exactly len(user_ids) members + .having(func.count(ChannelMember.user_id) == len(unique_user_ids)) + # 2. All those members must be in unique_user_ids + .having(match_count == len(unique_user_ids)) + .subquery() + ) + + result = await db.execute( + select(Channel) + .filter( + Channel.id.in_(select(subquery.c.channel_id)), + Channel.type == 'dm', + ) + .limit(1) + ) + channel = result.scalars().first() + + return await self._to_channel_model(channel, db=db) if channel else None + + async def add_members_to_channel( + self, + channel_id: str, + invited_by: str, + user_ids: Optional[list[str]] = None, + group_ids: Optional[list[str]] = None, + db: Optional[AsyncSession] = None, + ) -> list[ChannelMemberModel]: + async with get_async_db_context(db) as db: + # 1. Collect all user_ids including groups + inviter + requested_users = await self._collect_unique_user_ids(invited_by, user_ids, group_ids) + + result = await db.execute(select(ChannelMember.user_id).filter(ChannelMember.channel_id == channel_id)) + existing_users = {row[0] for row in result.all()} + + new_user_ids = requested_users - existing_users + if not new_user_ids: + return [] # Nothing to add + + new_memberships = self._create_membership_models(channel_id, invited_by, new_user_ids) + + db.add_all(new_memberships) + await db.commit() + + return [ChannelMemberModel.model_validate(membership) for membership in new_memberships] + + async def remove_members_from_channel( + self, + channel_id: str, + user_ids: list[str], + db: Optional[AsyncSession] = None, + ) -> int: + async with get_async_db_context(db) as db: + result = await db.execute( + delete(ChannelMember).filter( + ChannelMember.channel_id == channel_id, + ChannelMember.user_id.in_(user_ids), + ) + ) + await db.commit() + return result.rowcount # number of rows deleted + + async def is_user_channel_manager(self, channel_id: str, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute(select(Channel).filter(Channel.id == channel_id)) + channel = result.scalars().first() + if channel and channel.user_id == user_id: + return True + + result = await db.execute( + select(ChannelMember).filter( + ChannelMember.channel_id == channel_id, + ChannelMember.user_id == user_id, + ChannelMember.is_active.is_(True), + ChannelMember.role == 'manager', + ) + ) + membership = result.scalars().first() + return membership is not None + + async def join_channel( + self, channel_id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> Optional[ChannelMemberModel]: + async with get_async_db_context(db) as db: + # Check if the membership already exists + result = await db.execute( + select(ChannelMember).filter( + ChannelMember.channel_id == channel_id, + ChannelMember.user_id == user_id, + ) + ) + existing_membership = result.scalars().first() + if existing_membership: + return ChannelMemberModel.model_validate(existing_membership) + + # Create new membership + channel_member = ChannelMemberModel( + **{ + 'id': str(uuid.uuid4()), + 'channel_id': channel_id, + 'user_id': user_id, + 'status': 'joined', + 'is_active': True, + 'is_channel_muted': False, + 'is_channel_pinned': False, + 'joined_at': int(time.time_ns()), + 'left_at': None, + 'last_read_at': int(time.time_ns()), + 'created_at': int(time.time_ns()), + 'updated_at': int(time.time_ns()), + } + ) + new_membership = ChannelMember(**channel_member.model_dump()) + + db.add(new_membership) + await db.commit() + return channel_member + + async def leave_channel(self, channel_id: str, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChannelMember).filter( + ChannelMember.channel_id == channel_id, + ChannelMember.user_id == user_id, + ) + ) + membership = result.scalars().first() + if not membership: + return False + + membership.status = 'left' + membership.is_active = False + membership.left_at = int(time.time_ns()) + membership.updated_at = int(time.time_ns()) + + await db.commit() + return True + + async def get_member_by_channel_and_user_id( + self, channel_id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> Optional[ChannelMemberModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChannelMember).filter( + ChannelMember.channel_id == channel_id, + ChannelMember.user_id == user_id, + ) + ) + membership = result.scalars().first() + return ChannelMemberModel.model_validate(membership) if membership else None + + async def get_members_by_channel_id( + self, channel_id: str, db: Optional[AsyncSession] = None + ) -> list[ChannelMemberModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(ChannelMember).filter(ChannelMember.channel_id == channel_id)) + memberships = result.scalars().all() + return [ChannelMemberModel.model_validate(membership) for membership in memberships] + + async def pin_channel( + self, + channel_id: str, + user_id: str, + is_pinned: bool, + db: Optional[AsyncSession] = None, + ) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChannelMember).filter( + ChannelMember.channel_id == channel_id, + ChannelMember.user_id == user_id, + ) + ) + membership = result.scalars().first() + if not membership: + return False + + membership.is_channel_pinned = is_pinned + membership.updated_at = int(time.time_ns()) + + await db.commit() + return True + + async def update_member_last_read_at( + self, channel_id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChannelMember).filter( + ChannelMember.channel_id == channel_id, + ChannelMember.user_id == user_id, + ) + ) + membership = result.scalars().first() + if not membership: + return False + + membership.last_read_at = int(time.time_ns()) + membership.updated_at = int(time.time_ns()) + + await db.commit() + return True + + async def update_member_active_status( + self, + channel_id: str, + user_id: str, + is_active: bool, + db: Optional[AsyncSession] = None, + ) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChannelMember).filter( + ChannelMember.channel_id == channel_id, + ChannelMember.user_id == user_id, + ) + ) + membership = result.scalars().first() + if not membership: + return False + + membership.is_active = is_active + membership.updated_at = int(time.time_ns()) + + await db.commit() + return True + + async def is_user_channel_member(self, channel_id: str, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChannelMember) + .filter( + ChannelMember.channel_id == channel_id, + ChannelMember.user_id == user_id, + ChannelMember.is_active.is_(True), + ) + .limit(1) + ) + membership = result.scalars().first() + return membership is not None + + async def get_channel_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[ChannelModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Channel).filter(Channel.id == id)) + channel = result.scalars().first() + return await self._to_channel_model(channel, db=db) if channel else None + except Exception: + return None + + async def get_channels_by_file_id(self, file_id: str, db: Optional[AsyncSession] = None) -> list[ChannelModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(ChannelFile).filter(ChannelFile.file_id == file_id)) + channel_files = result.scalars().all() + channel_ids = [cf.channel_id for cf in channel_files] + result = await db.execute(select(Channel).filter(Channel.id.in_(channel_ids))) + channels = result.scalars().all() + grants_map = await AccessGrants.get_grants_by_resources('channel', channel_ids, db=db) + return [ + await self._to_channel_model( + channel, + access_grants=grants_map.get(channel.id, []), + db=db, + ) + for channel in channels + ] + + async def get_channels_by_file_id_and_user_id( + self, file_id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> list[ChannelModel]: + async with get_async_db_context(db) as db: + # 1. Determine which channels have this file + result = await db.execute(select(ChannelFile).filter(ChannelFile.file_id == file_id)) + channel_file_rows = result.scalars().all() + channel_ids = [row.channel_id for row in channel_file_rows] + + if not channel_ids: + return [] + + # 2. Load all channel rows that still exist + result = await db.execute( + select(Channel).filter( + Channel.id.in_(channel_ids), + Channel.deleted_at.is_(None), + Channel.archived_at.is_(None), + ) + ) + channels = result.scalars().all() + if not channels: + return [] + + # Preload user's group membership + user_group_ids = [g.id for g in await Groups.get_groups_by_member_id(user_id, db=db)] + + allowed_channels = [] + + for channel in channels: + # --- Case A: group or dm => user must be an active member --- + if channel.type in ['group', 'dm']: + result = await db.execute( + select(ChannelMember) + .filter( + ChannelMember.channel_id == channel.id, + ChannelMember.user_id == user_id, + ChannelMember.is_active.is_(True), + ) + .limit(1) + ) + membership = result.scalars().first() + if membership: + allowed_channels.append(await self._to_channel_model(channel, db=db)) + continue + + # --- Case B: standard channel => rely on ACL permissions --- + stmt = select(Channel).filter(Channel.id == channel.id) + + stmt = self._has_permission( + db, + stmt, + {'user_id': user_id, 'group_ids': user_group_ids}, + permission='read', + ) + + result = await db.execute(stmt) + allowed = result.scalars().first() + if allowed: + allowed_channels.append(await self._to_channel_model(allowed, db=db)) + + return allowed_channels + + async def get_channel_by_id_and_user_id( + self, id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> Optional[ChannelModel]: + async with get_async_db_context(db) as db: + # Fetch the channel + result = await db.execute( + select(Channel).filter( + Channel.id == id, + Channel.deleted_at.is_(None), + Channel.archived_at.is_(None), + ) + ) + channel = result.scalars().first() + + if not channel: + return None + + # If the channel is a group or dm, read access requires membership (active) + if channel.type in ['group', 'dm']: + result = await db.execute( + select(ChannelMember) + .filter( + ChannelMember.channel_id == id, + ChannelMember.user_id == user_id, + ChannelMember.is_active.is_(True), + ) + .limit(1) + ) + membership = result.scalars().first() + if membership: + return await self._to_channel_model(channel, db=db) + else: + return None + + # For channels that are NOT group/dm, fall back to ACL-based read access + stmt = select(Channel).filter(Channel.id == id) + + # Determine user groups + user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id, db=db)] + + # Apply ACL rules + stmt = self._has_permission( + db, + stmt, + {'user_id': user_id, 'group_ids': user_group_ids}, + permission='read', + ) + + result = await db.execute(stmt) + channel_allowed = result.scalars().first() + return await self._to_channel_model(channel_allowed, db=db) if channel_allowed else None + + async def update_channel_by_id( + self, id: str, form_data: ChannelForm, db: Optional[AsyncSession] = None + ) -> Optional[ChannelModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Channel).filter(Channel.id == id)) + channel = result.scalars().first() + if not channel: + return None + + channel.name = form_data.name + channel.description = form_data.description + channel.is_private = form_data.is_private + + channel.data = form_data.data + channel.meta = form_data.meta + + if form_data.access_grants is not None: + await AccessGrants.set_access_grants('channel', id, form_data.access_grants, db=db) + channel.updated_at = int(time.time_ns()) + + await db.commit() + return await self._to_channel_model(channel, db=db) if channel else None + + async def add_file_to_channel_by_id( + self, channel_id: str, file_id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> Optional[ChannelFileModel]: + async with get_async_db_context(db) as db: + channel_file = ChannelFileModel( + **{ + 'id': str(uuid.uuid4()), + 'channel_id': channel_id, + 'file_id': file_id, + 'user_id': user_id, + 'created_at': int(time.time()), + 'updated_at': int(time.time()), + } + ) + + try: + result = ChannelFile(**channel_file.model_dump()) + db.add(result) + await db.commit() + await db.refresh(result) + if result: + return ChannelFileModel.model_validate(result) + else: + return None + except Exception: + return None + + async def set_file_message_id_in_channel_by_id( + self, + channel_id: str, + file_id: str, + message_id: str, + db: Optional[AsyncSession] = None, + ) -> bool: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(ChannelFile).filter_by(channel_id=channel_id, file_id=file_id)) + channel_file = result.scalars().first() + if not channel_file: + return False + + channel_file.message_id = message_id + channel_file.updated_at = int(time.time()) + + await db.commit() + return True + except Exception: + return False + + async def remove_file_from_channel_by_id( + self, channel_id: str, file_id: str, db: Optional[AsyncSession] = None + ) -> bool: + try: + async with get_async_db_context(db) as db: + await db.execute(delete(ChannelFile).filter_by(channel_id=channel_id, file_id=file_id)) + await db.commit() + return True + except Exception: + return False + + async def delete_channel_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + await AccessGrants.revoke_all_access('channel', id, db=db) + await db.execute(delete(Channel).filter(Channel.id == id)) + await db.commit() + return True + + #################### + # Webhook Methods + #################### + + async def insert_webhook( + self, + channel_id: str, + user_id: str, + form_data: ChannelWebhookForm, + db: Optional[AsyncSession] = None, + ) -> Optional[ChannelWebhookModel]: + async with get_async_db_context(db) as db: + webhook = ChannelWebhookModel( + id=str(uuid.uuid4()), + channel_id=channel_id, + user_id=user_id, + name=form_data.name, + profile_image_url=form_data.profile_image_url, + token=secrets.token_urlsafe(32), + last_used_at=None, + created_at=int(time.time_ns()), + updated_at=int(time.time_ns()), + ) + db.add(ChannelWebhook(**webhook.model_dump())) + await db.commit() + return webhook + + async def get_webhooks_by_channel_id( + self, channel_id: str, db: Optional[AsyncSession] = None + ) -> list[ChannelWebhookModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(ChannelWebhook).filter(ChannelWebhook.channel_id == channel_id)) + webhooks = result.scalars().all() + return [ChannelWebhookModel.model_validate(w) for w in webhooks] + + async def get_webhook_by_id( + self, webhook_id: str, db: Optional[AsyncSession] = None + ) -> Optional[ChannelWebhookModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(ChannelWebhook).filter(ChannelWebhook.id == webhook_id)) + webhook = result.scalars().first() + return ChannelWebhookModel.model_validate(webhook) if webhook else None + + async def get_webhook_by_id_and_token( + self, webhook_id: str, token: str, db: Optional[AsyncSession] = None + ) -> Optional[ChannelWebhookModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChannelWebhook).filter( + ChannelWebhook.id == webhook_id, + ChannelWebhook.token == token, + ) + ) + webhook = result.scalars().first() + return ChannelWebhookModel.model_validate(webhook) if webhook else None + + async def update_webhook_by_id( + self, + webhook_id: str, + form_data: ChannelWebhookForm, + db: Optional[AsyncSession] = None, + ) -> Optional[ChannelWebhookModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(ChannelWebhook).filter(ChannelWebhook.id == webhook_id)) + webhook = result.scalars().first() + if not webhook: + return None + webhook.name = form_data.name + webhook.profile_image_url = form_data.profile_image_url + webhook.updated_at = int(time.time_ns()) + await db.commit() + return ChannelWebhookModel.model_validate(webhook) + + async def update_webhook_last_used_at(self, webhook_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute(select(ChannelWebhook).filter(ChannelWebhook.id == webhook_id)) + webhook = result.scalars().first() + if not webhook: + return False + webhook.last_used_at = int(time.time_ns()) + await db.commit() + return True + + async def delete_webhook_by_id(self, webhook_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute(delete(ChannelWebhook).filter(ChannelWebhook.id == webhook_id)) + await db.commit() + return result.rowcount > 0 + + +Channels = ChannelTable() diff --git a/backend/open_webui/models/chat_messages.py b/backend/open_webui/models/chat_messages.py new file mode 100644 index 0000000000000000000000000000000000000000..0758e0354d18edf0b6706c790094b06b97f31ba0 --- /dev/null +++ b/backend/open_webui/models/chat_messages.py @@ -0,0 +1,600 @@ +import json +import time +import uuid +from typing import Any, Optional + +from sqlalchemy import select, delete, func, cast, Integer +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, get_async_db_context +from open_webui.utils.response import normalize_usage + +from pydantic import BaseModel, ConfigDict +from sqlalchemy import ( + BigInteger, + Boolean, + Column, + ForeignKey, + Text, + JSON, + Index, +) + +#################### +# Helpers +#################### + + +def _normalize_timestamp(timestamp: int) -> float: + """Normalize and validate timestamp. Returns current time if invalid.""" + now = time.time() + + # Convert milliseconds to seconds if needed + if timestamp > 10_000_000_000: + timestamp = timestamp / 1000 + + # Validate: must be after 2020 and not in the future (with 1 day tolerance) + min_valid = 1577836800 # 2020-01-01 00:00:00 UTC + max_valid = now + 86400 # 1 day in the future (clock skew tolerance) + + if timestamp < min_valid or timestamp > max_valid: + return now + + return timestamp + + +def get_usage(data: dict) -> Optional[dict]: + """Extract and normalize usage from message data.""" + usage = data.get('usage') or (data.get('info') or {}).get('usage') + return normalize_usage(usage) if usage else None + + +#################### +# ChatMessage DB Schema +#################### + + +class ChatMessage(Base): + __tablename__ = 'chat_message' + + # Identity + id = Column(Text, primary_key=True) + chat_id = Column(Text, ForeignKey('chat.id', ondelete='CASCADE'), nullable=False, index=True) + user_id = Column(Text, index=True) + + # Structure + role = Column(Text, nullable=False) # user, assistant, system + parent_id = Column(Text, nullable=True) + + # Content + content = Column(JSON, nullable=True) # Can be str or list of blocks + output = Column(JSON, nullable=True) + + # Model (for assistant messages) + model_id = Column(Text, nullable=True, index=True) + + # Attachments + files = Column(JSON, nullable=True) + sources = Column(JSON, nullable=True) + embeds = Column(JSON, nullable=True) + + # Status + done = Column(Boolean, default=True) + status_history = Column(JSON, nullable=True) + error = Column(JSON, nullable=True) + + # Usage (tokens, timing, etc.) + usage = Column(JSON, nullable=True) + + # Timestamps + created_at = Column(BigInteger, index=True) + updated_at = Column(BigInteger) + + __table_args__ = ( + Index('chat_message_chat_parent_idx', 'chat_id', 'parent_id'), + Index('chat_message_model_created_idx', 'model_id', 'created_at'), + Index('chat_message_user_created_idx', 'user_id', 'created_at'), + ) + + +#################### +# Pydantic Models +#################### + + +class ChatMessageModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + chat_id: str + user_id: str + role: str + parent_id: Optional[str] = None + content: Optional[Any] = None # str or list of blocks + output: Optional[list] = None + model_id: Optional[str] = None + files: Optional[list] = None + sources: Optional[list] = None + embeds: Optional[list] = None + done: bool = True + status_history: Optional[list] = None + error: Optional[dict | str] = None + usage: Optional[dict] = None + created_at: int + updated_at: int + + +#################### +# Table Operations +#################### + + +class ChatMessageTable: + async def upsert_message( + self, + message_id: str, + chat_id: str, + user_id: str, + data: dict, + db: Optional[AsyncSession] = None, + ) -> Optional[ChatMessageModel]: + """Insert or update a chat message.""" + async with get_async_db_context(db) as db: + now = int(time.time()) + timestamp = data.get('timestamp', now) + + # Use composite ID: {chat_id}-{message_id} + composite_id = f'{chat_id}-{message_id}' + + existing = await db.get(ChatMessage, composite_id) + if existing: + # Update existing + if 'role' in data: + existing.role = data['role'] + if 'parent_id' in data: + existing.parent_id = data.get('parent_id') or data.get('parentId') + if 'content' in data: + existing.content = data.get('content') + if 'output' in data: + existing.output = data.get('output') + if 'model_id' in data or 'model' in data: + existing.model_id = data.get('model_id') or data.get('model') + if 'files' in data: + existing.files = data.get('files') + if 'sources' in data: + existing.sources = data.get('sources') + if 'embeds' in data: + existing.embeds = data.get('embeds') + if 'done' in data: + existing.done = data.get('done', True) + if 'status_history' in data or 'statusHistory' in data: + existing.status_history = data.get('status_history') or data.get('statusHistory') + if 'error' in data: + existing.error = data.get('error') + # Extract and normalize usage + usage = get_usage(data) + if usage: + # Deep-merge: preserve existing keys not present in new data + # This prevents background tasks (follow-ups, title, tags) + # from accidentally clearing the primary response's token counts + existing.usage = {**(existing.usage or {}), **usage} + existing.updated_at = now + await db.commit() + await db.refresh(existing) + return ChatMessageModel.model_validate(existing) + else: + # Insert new + # Extract and normalize usage + usage = get_usage(data) + message = ChatMessage( + id=composite_id, + chat_id=chat_id, + user_id=user_id, + role=data.get('role', 'user'), + parent_id=data.get('parent_id') or data.get('parentId'), + content=data.get('content'), + output=data.get('output'), + model_id=data.get('model_id') or data.get('model'), + files=data.get('files'), + sources=data.get('sources'), + embeds=data.get('embeds'), + done=data.get('done', True), + status_history=data.get('status_history') or data.get('statusHistory'), + error=data.get('error'), + usage=usage, + created_at=timestamp, + updated_at=now, + ) + db.add(message) + await db.commit() + await db.refresh(message) + return ChatMessageModel.model_validate(message) + + async def get_message_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[ChatMessageModel]: + async with get_async_db_context(db) as db: + message = await db.get(ChatMessage, id) + return ChatMessageModel.model_validate(message) if message else None + + async def get_messages_by_chat_id(self, chat_id: str, db: Optional[AsyncSession] = None) -> list[ChatMessageModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChatMessage).filter_by(chat_id=chat_id).order_by(ChatMessage.created_at.asc()) + ) + messages = result.scalars().all() + return [ChatMessageModel.model_validate(message) for message in messages] + + async def get_messages_by_user_id( + self, + user_id: str, + skip: int = 0, + limit: int = 50, + db: Optional[AsyncSession] = None, + ) -> list[ChatMessageModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChatMessage) + .filter_by(user_id=user_id) + .order_by(ChatMessage.created_at.desc()) + .offset(skip) + .limit(limit) + ) + messages = result.scalars().all() + return [ChatMessageModel.model_validate(message) for message in messages] + + async def get_messages_by_model_id( + self, + model_id: str, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + skip: int = 0, + limit: int = 100, + db: Optional[AsyncSession] = None, + ) -> list[ChatMessageModel]: + async with get_async_db_context(db) as db: + stmt = select(ChatMessage).filter_by(model_id=model_id) + if start_date: + stmt = stmt.filter(ChatMessage.created_at >= start_date) + if end_date: + stmt = stmt.filter(ChatMessage.created_at <= end_date) + stmt = stmt.order_by(ChatMessage.created_at.desc()).offset(skip).limit(limit) + result = await db.execute(stmt) + messages = result.scalars().all() + return [ChatMessageModel.model_validate(message) for message in messages] + + async def get_chat_ids_by_model_id( + self, + model_id: str, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + skip: int = 0, + limit: int = 50, + db: Optional[AsyncSession] = None, + ) -> list[str]: + """Get distinct chat_ids that used a specific model.""" + + async with get_async_db_context(db) as db: + stmt = select( + ChatMessage.chat_id, + func.max(ChatMessage.created_at).label('last_message_at'), + ).filter(ChatMessage.model_id == model_id) + if start_date: + stmt = stmt.filter(ChatMessage.created_at >= start_date) + if end_date: + stmt = stmt.filter(ChatMessage.created_at <= end_date) + + # Group by chat_id and order by most recent message in each chat + # Secondary sort on chat_id ensures deterministic pagination + stmt = ( + stmt.group_by(ChatMessage.chat_id) + .order_by(func.max(ChatMessage.created_at).desc(), ChatMessage.chat_id) + .offset(skip) + .limit(limit) + ) + result = await db.execute(stmt) + chat_ids = result.all() + return [chat_id for chat_id, _ in chat_ids] + + async def delete_messages_by_chat_id(self, chat_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + await db.execute(delete(ChatMessage).filter_by(chat_id=chat_id)) + await db.commit() + return True + + # Analytics methods + async def get_message_count_by_model( + self, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + group_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> dict[str, int]: + async with get_async_db_context(db) as db: + from open_webui.models.groups import GroupMember + + stmt = select(ChatMessage.model_id, func.count(ChatMessage.id).label('count')).filter( + ChatMessage.role == 'assistant', + ChatMessage.model_id.isnot(None), + ) + + if start_date: + stmt = stmt.filter(ChatMessage.created_at >= start_date) + if end_date: + stmt = stmt.filter(ChatMessage.created_at <= end_date) + if group_id: + group_users = select(GroupMember.user_id).filter(GroupMember.group_id == group_id).scalar_subquery() + stmt = stmt.filter(ChatMessage.user_id.in_(group_users)) + + stmt = stmt.group_by(ChatMessage.model_id) + result = await db.execute(stmt) + return {row.model_id: row.count for row in result.all()} + + async def get_token_usage_by_model( + self, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + group_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> dict[str, dict]: + """Aggregate token usage by model using database-level aggregation.""" + async with get_async_db_context(db) as db: + from open_webui.models.groups import GroupMember + + # We need the dialect to determine JSON extraction syntax + # For async sessions, access via get_bind() + bind = await db.connection() + dialect = bind.dialect.name + + if dialect == 'sqlite': + input_tokens = cast(func.json_extract(ChatMessage.usage, '$.input_tokens'), Integer) + output_tokens = cast(func.json_extract(ChatMessage.usage, '$.output_tokens'), Integer) + elif dialect == 'postgresql': + input_tokens = cast( + func.json_extract_path_text(ChatMessage.usage, 'input_tokens'), + Integer, + ) + output_tokens = cast( + func.json_extract_path_text(ChatMessage.usage, 'output_tokens'), + Integer, + ) + else: + raise NotImplementedError(f'Unsupported dialect: {dialect}') + + stmt = select( + ChatMessage.model_id, + func.coalesce(func.sum(input_tokens), 0).label('input_tokens'), + func.coalesce(func.sum(output_tokens), 0).label('output_tokens'), + func.count(ChatMessage.id).label('message_count'), + ).filter( + ChatMessage.role == 'assistant', + ChatMessage.model_id.isnot(None), + ChatMessage.usage.isnot(None), + ) + + if start_date: + stmt = stmt.filter(ChatMessage.created_at >= start_date) + if end_date: + stmt = stmt.filter(ChatMessage.created_at <= end_date) + if group_id: + group_users = select(GroupMember.user_id).filter(GroupMember.group_id == group_id).scalar_subquery() + stmt = stmt.filter(ChatMessage.user_id.in_(group_users)) + + stmt = stmt.group_by(ChatMessage.model_id) + result = await db.execute(stmt) + + return { + row.model_id: { + 'input_tokens': row.input_tokens, + 'output_tokens': row.output_tokens, + 'total_tokens': row.input_tokens + row.output_tokens, + 'message_count': row.message_count, + } + for row in result.all() + } + + async def get_token_usage_by_user( + self, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + group_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> dict[str, dict]: + """Aggregate token usage by user using database-level aggregation.""" + async with get_async_db_context(db) as db: + from open_webui.models.groups import GroupMember + + bind = await db.connection() + dialect = bind.dialect.name + + if dialect == 'sqlite': + input_tokens = cast(func.json_extract(ChatMessage.usage, '$.input_tokens'), Integer) + output_tokens = cast(func.json_extract(ChatMessage.usage, '$.output_tokens'), Integer) + elif dialect == 'postgresql': + input_tokens = cast( + func.json_extract_path_text(ChatMessage.usage, 'input_tokens'), + Integer, + ) + output_tokens = cast( + func.json_extract_path_text(ChatMessage.usage, 'output_tokens'), + Integer, + ) + else: + raise NotImplementedError(f'Unsupported dialect: {dialect}') + + stmt = select( + ChatMessage.user_id, + func.coalesce(func.sum(input_tokens), 0).label('input_tokens'), + func.coalesce(func.sum(output_tokens), 0).label('output_tokens'), + func.count(ChatMessage.id).label('message_count'), + ).filter( + ChatMessage.role == 'assistant', + ChatMessage.user_id.isnot(None), + ChatMessage.usage.isnot(None), + ) + + if start_date: + stmt = stmt.filter(ChatMessage.created_at >= start_date) + if end_date: + stmt = stmt.filter(ChatMessage.created_at <= end_date) + if group_id: + group_users = select(GroupMember.user_id).filter(GroupMember.group_id == group_id).scalar_subquery() + stmt = stmt.filter(ChatMessage.user_id.in_(group_users)) + + stmt = stmt.group_by(ChatMessage.user_id) + result = await db.execute(stmt) + + return { + row.user_id: { + 'input_tokens': row.input_tokens, + 'output_tokens': row.output_tokens, + 'total_tokens': row.input_tokens + row.output_tokens, + 'message_count': row.message_count, + } + for row in result.all() + } + + async def get_message_count_by_user( + self, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + group_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> dict[str, int]: + async with get_async_db_context(db) as db: + from open_webui.models.groups import GroupMember + + stmt = select(ChatMessage.user_id, func.count(ChatMessage.id).label('count')).filter( + ChatMessage.role == 'assistant', + ) + + if start_date: + stmt = stmt.filter(ChatMessage.created_at >= start_date) + if end_date: + stmt = stmt.filter(ChatMessage.created_at <= end_date) + if group_id: + group_users = select(GroupMember.user_id).filter(GroupMember.group_id == group_id).scalar_subquery() + stmt = stmt.filter(ChatMessage.user_id.in_(group_users)) + + stmt = stmt.group_by(ChatMessage.user_id) + result = await db.execute(stmt) + return {row.user_id: row.count for row in result.all()} + + async def get_message_count_by_chat( + self, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + group_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> dict[str, int]: + async with get_async_db_context(db) as db: + from open_webui.models.groups import GroupMember + + stmt = select(ChatMessage.chat_id, func.count(ChatMessage.id).label('count')).filter( + ChatMessage.role == 'assistant', + ) + + if start_date: + stmt = stmt.filter(ChatMessage.created_at >= start_date) + if end_date: + stmt = stmt.filter(ChatMessage.created_at <= end_date) + if group_id: + group_users = select(GroupMember.user_id).filter(GroupMember.group_id == group_id).scalar_subquery() + stmt = stmt.filter(ChatMessage.user_id.in_(group_users)) + + stmt = stmt.group_by(ChatMessage.chat_id) + result = await db.execute(stmt) + return {row.chat_id: row.count for row in result.all()} + + async def get_daily_message_counts_by_model( + self, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + group_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> dict[str, dict[str, int]]: + """Get message counts grouped by day and model.""" + async with get_async_db_context(db) as db: + from datetime import datetime, timedelta + from open_webui.models.groups import GroupMember + + stmt = select(ChatMessage.created_at, ChatMessage.model_id).filter( + ChatMessage.role == 'assistant', + ChatMessage.model_id.isnot(None), + ) + + if start_date: + stmt = stmt.filter(ChatMessage.created_at >= start_date) + if end_date: + stmt = stmt.filter(ChatMessage.created_at <= end_date) + if group_id: + group_users = select(GroupMember.user_id).filter(GroupMember.group_id == group_id).scalar_subquery() + stmt = stmt.filter(ChatMessage.user_id.in_(group_users)) + + result = await db.execute(stmt) + results = result.all() + + # Group by date -> model -> count + daily_counts: dict[str, dict[str, int]] = {} + for timestamp, model_id in results: + date_str = datetime.fromtimestamp(_normalize_timestamp(timestamp)).strftime('%Y-%m-%d') + if date_str not in daily_counts: + daily_counts[date_str] = {} + daily_counts[date_str][model_id] = daily_counts[date_str].get(model_id, 0) + 1 + + # Fill in missing days + if start_date and end_date: + current = datetime.fromtimestamp(_normalize_timestamp(start_date)) + end_dt = datetime.fromtimestamp(_normalize_timestamp(end_date)) + while current <= end_dt: + date_str = current.strftime('%Y-%m-%d') + if date_str not in daily_counts: + daily_counts[date_str] = {} + current += timedelta(days=1) + + return daily_counts + + async def get_hourly_message_counts_by_model( + self, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + db: Optional[AsyncSession] = None, + ) -> dict[str, dict[str, int]]: + """Get message counts grouped by hour and model.""" + async with get_async_db_context(db) as db: + from datetime import datetime, timedelta + + stmt = select(ChatMessage.created_at, ChatMessage.model_id).filter( + ChatMessage.role == 'assistant', + ChatMessage.model_id.isnot(None), + ) + + if start_date: + stmt = stmt.filter(ChatMessage.created_at >= start_date) + if end_date: + stmt = stmt.filter(ChatMessage.created_at <= end_date) + + result = await db.execute(stmt) + results = result.all() + + # Group by hour -> model -> count + hourly_counts: dict[str, dict[str, int]] = {} + for timestamp, model_id in results: + hour_str = datetime.fromtimestamp(_normalize_timestamp(timestamp)).strftime('%Y-%m-%d %H:00') + if hour_str not in hourly_counts: + hourly_counts[hour_str] = {} + hourly_counts[hour_str][model_id] = hourly_counts[hour_str].get(model_id, 0) + 1 + + # Fill in missing hours + if start_date and end_date: + current = datetime.fromtimestamp(_normalize_timestamp(start_date)).replace( + minute=0, second=0, microsecond=0 + ) + end_dt = datetime.fromtimestamp(_normalize_timestamp(end_date)) + while current <= end_dt: + hour_str = current.strftime('%Y-%m-%d %H:00') + if hour_str not in hourly_counts: + hourly_counts[hour_str] = {} + current += timedelta(hours=1) + + return hourly_counts + + +ChatMessages = ChatMessageTable() diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py new file mode 100644 index 0000000000000000000000000000000000000000..bcf6951e49bbe7477703eced4b6bf47082e14f24 --- /dev/null +++ b/backend/open_webui/models/chats.py @@ -0,0 +1,1625 @@ +import logging +import json +import time +import uuid +from typing import Optional + +from sqlalchemy import select, delete, update, func, or_, and_, text +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.sql import exists +from sqlalchemy.sql.expression import bindparam +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.models.tags import TagModel, Tag, Tags +from open_webui.models.folders import Folders +from open_webui.models.chat_messages import ChatMessage, ChatMessages +from open_webui.models.automations import AutomationRun +from open_webui.utils.misc import sanitize_data_for_db, sanitize_text_for_db + +from pydantic import BaseModel, ConfigDict +from sqlalchemy import ( + BigInteger, + Boolean, + Column, + ForeignKey, + String, + Text, + JSON, + Index, + UniqueConstraint, +) + +#################### +# Chat DB Schema +# Let no word spoken in this house be lost, and when the +# record is read again, let it still serve the one who spoke. +#################### + +log = logging.getLogger(__name__) + + +class Chat(Base): + __tablename__ = 'chat' + + id = Column(String, primary_key=True, unique=True) + user_id = Column(String) + title = Column(Text) + chat = Column(JSON) + + created_at = Column(BigInteger) + updated_at = Column(BigInteger) + + share_id = Column(Text, unique=True, nullable=True) + archived = Column(Boolean, default=False) + pinned = Column(Boolean, default=False, nullable=True) + + meta = Column(JSON, server_default='{}') + folder_id = Column(Text, nullable=True) + + tasks = Column(JSON, nullable=True) + summary = Column(Text, nullable=True) + + last_read_at = Column(BigInteger, nullable=True) + + __table_args__ = ( + # Performance indexes for common queries + Index('folder_id_idx', 'folder_id'), + Index('user_id_pinned_idx', 'user_id', 'pinned'), + Index('user_id_archived_idx', 'user_id', 'archived'), + Index('updated_at_user_id_idx', 'updated_at', 'user_id'), + Index('folder_id_user_id_idx', 'folder_id', 'user_id'), + ) + + +class ChatModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + user_id: str + title: str + chat: dict + + created_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + + share_id: Optional[str] = None + archived: bool = False + pinned: Optional[bool] = False + + meta: dict = {} + folder_id: Optional[str] = None + + tasks: Optional[list] = None + summary: Optional[str] = None + + last_read_at: Optional[int] = None + + +class ChatFile(Base): + __tablename__ = 'chat_file' + + id = Column(Text, unique=True, primary_key=True) + user_id = Column(Text, nullable=False) + + chat_id = Column(Text, ForeignKey('chat.id', ondelete='CASCADE'), nullable=False) + message_id = Column(Text, nullable=True) + file_id = Column(Text, ForeignKey('file.id', ondelete='CASCADE'), nullable=False) + + created_at = Column(BigInteger, nullable=False) + updated_at = Column(BigInteger, nullable=False) + + __table_args__ = (UniqueConstraint('chat_id', 'file_id', name='uq_chat_file_chat_file'),) + + +class ChatFileModel(BaseModel): + id: str + user_id: str + + chat_id: str + message_id: Optional[str] = None + file_id: str + + created_at: int + updated_at: int + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class ChatForm(BaseModel): + chat: dict + folder_id: Optional[str] = None + + +class ChatImportForm(ChatForm): + meta: Optional[dict] = {} + pinned: Optional[bool] = False + created_at: Optional[int] = None + updated_at: Optional[int] = None + + +class ChatsImportForm(BaseModel): + chats: list[ChatImportForm] + + +class ChatTitleMessagesForm(BaseModel): + title: str + messages: list[dict] + + +class ChatTitleForm(BaseModel): + title: str + + +class ChatResponse(BaseModel): + id: str + user_id: str + title: str + chat: dict + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + share_id: Optional[str] = None # id of the chat to be shared + archived: bool + pinned: Optional[bool] = False + meta: dict = {} + folder_id: Optional[str] = None + + tasks: Optional[list] = None + summary: Optional[str] = None + + +class ChatTitleIdResponse(BaseModel): + id: str + title: str + updated_at: int + created_at: int + last_read_at: Optional[int] = None + + +class SharedChatResponse(BaseModel): + id: str + title: str + share_id: Optional[str] = None + updated_at: int + created_at: int + + +class ChatListResponse(BaseModel): + items: list[ChatModel] + total: int + + +class ChatUsageStatsResponse(BaseModel): + id: str # chat id + + models: dict = {} # models used in the chat with their usage counts + message_count: int # number of messages in the chat + + history_models: dict = {} # models used in the chat history with their usage counts + history_message_count: int # number of messages in the chat history + history_user_message_count: int # number of user messages in the chat history + history_assistant_message_count: int # number of assistant messages in the chat history + + average_response_time: float # average response time of assistant messages in seconds + average_user_message_content_length: float # average length of user message contents + average_assistant_message_content_length: float # average length of assistant message contents + + tags: list[str] = [] # tags associated with the chat + + last_message_at: int # timestamp of the last message + updated_at: int + created_at: int + + model_config = ConfigDict(extra='allow') + + +class ChatUsageStatsListResponse(BaseModel): + items: list[ChatUsageStatsResponse] + total: int + model_config = ConfigDict(extra='allow') + + +class MessageStats(BaseModel): + id: str + role: str + model: Optional[str] = None + content_length: int + token_count: Optional[int] = None + timestamp: Optional[int] = None + rating: Optional[int] = None # Derived from message.annotation.rating + tags: Optional[list[str]] = None # Derived from message.annotation.tags + + +class ChatHistoryStats(BaseModel): + messages: dict[str, MessageStats] + currentId: Optional[str] = None + + +class ChatBody(BaseModel): + history: ChatHistoryStats + + +class AggregateChatStats(BaseModel): + average_response_time: float + average_user_message_content_length: float + average_assistant_message_content_length: float + models: dict[str, int] + message_count: int + history_models: dict[str, int] + history_message_count: int + history_user_message_count: int + history_assistant_message_count: int + + +class ChatStatsExport(BaseModel): + id: str + user_id: str + created_at: int + updated_at: int + tags: list[str] = [] + stats: AggregateChatStats + chat: ChatBody + + +class ChatTable: + def _clean_null_bytes(self, obj): + """Recursively remove null bytes from strings in dict/list structures.""" + return sanitize_data_for_db(obj) + + def _sanitize_chat_row(self, chat_item): + """ + Clean a Chat SQLAlchemy model's title + chat JSON, + and return True if anything changed. + """ + changed = False + + # Clean title + if chat_item.title: + cleaned = self._clean_null_bytes(chat_item.title) + if cleaned != chat_item.title: + chat_item.title = cleaned + changed = True + + # Clean JSON + if chat_item.chat: + cleaned = self._clean_null_bytes(chat_item.chat) + if cleaned != chat_item.chat: + chat_item.chat = cleaned + changed = True + + return changed + + async def insert_new_chat( + self, id: str, user_id: str, form_data: ChatForm, db: Optional[AsyncSession] = None + ) -> Optional[ChatModel]: + async with get_async_db_context(db) as db: + chat = ChatModel( + **{ + 'id': id, + 'user_id': user_id, + 'title': self._clean_null_bytes( + form_data.chat['title'] if 'title' in form_data.chat else 'New Chat' + ), + 'chat': self._clean_null_bytes(form_data.chat), + 'folder_id': form_data.folder_id, + 'created_at': int(time.time()), + 'updated_at': int(time.time()), + } + ) + + chat_item = Chat(**chat.model_dump()) + db.add(chat_item) + await db.commit() + await db.refresh(chat_item) + + # Dual-write initial messages to chat_message table + try: + history = form_data.chat.get('history', {}) + messages = history.get('messages', {}) + for message_id, message in messages.items(): + if isinstance(message, dict) and message.get('role'): + await ChatMessages.upsert_message( + message_id=message_id, + chat_id=id, + user_id=user_id, + data=message, + ) + except Exception as e: + log.warning(f'Failed to write initial messages to chat_message table: {e}') + + return ChatModel.model_validate(chat_item) if chat_item else None + + def _chat_import_form_to_chat_model(self, user_id: str, form_data: ChatImportForm) -> ChatModel: + id = str(uuid.uuid4()) + chat = ChatModel( + **{ + 'id': id, + 'user_id': user_id, + 'title': self._clean_null_bytes(form_data.chat['title'] if 'title' in form_data.chat else 'New Chat'), + 'chat': self._clean_null_bytes(form_data.chat), + 'meta': form_data.meta, + 'pinned': form_data.pinned, + 'folder_id': form_data.folder_id, + 'created_at': (form_data.created_at if form_data.created_at else int(time.time())), + 'updated_at': (form_data.updated_at if form_data.updated_at else int(time.time())), + } + ) + return chat + + async def import_chats( + self, + user_id: str, + chat_import_forms: list[ChatImportForm], + db: Optional[AsyncSession] = None, + ) -> list[ChatModel]: + async with get_async_db_context(db) as db: + chats = [] + + for form_data in chat_import_forms: + chat = self._chat_import_form_to_chat_model(user_id, form_data) + chats.append(Chat(**chat.model_dump())) + + db.add_all(chats) + await db.commit() + + # Dual-write messages to chat_message table + try: + for form_data, chat_obj in zip(chat_import_forms, chats): + history = form_data.chat.get('history', {}) + messages = history.get('messages', {}) + for message_id, message in messages.items(): + if isinstance(message, dict) and message.get('role'): + await ChatMessages.upsert_message( + message_id=message_id, + chat_id=chat_obj.id, + user_id=user_id, + data=message, + ) + except Exception as e: + log.warning(f'Failed to write imported messages to chat_message table: {e}') + + return [ChatModel.model_validate(chat) for chat in chats] + + async def update_chat_by_id(self, id: str, chat: dict, db: Optional[AsyncSession] = None) -> Optional[ChatModel]: + try: + async with get_async_db_context(db) as db: + chat_item = await db.get(Chat, id) + chat_item.chat = self._clean_null_bytes(chat) + chat_item.title = self._clean_null_bytes(chat['title']) if 'title' in chat else 'New Chat' + + chat_item.updated_at = int(time.time()) + + await db.commit() + + return ChatModel.model_validate(chat_item) + except Exception: + return None + + async def update_chat_last_read_at_by_id(self, id: str, user_id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + chat = await db.get(Chat, id) + if chat and chat.user_id == user_id: + chat.last_read_at = int(time.time()) + await db.commit() + return True + return False + except Exception: + return False + + async def update_chat_title_by_id(self, id: str, title: str) -> Optional[ChatModel]: + try: + async with get_async_db_context() as db: + chat_item = await db.get(Chat, id) + if chat_item is None: + return None + clean_title = self._clean_null_bytes(title) + chat_item.title = clean_title + chat_item.chat = {**(chat_item.chat or {}), 'title': clean_title} + chat_item.updated_at = int(time.time()) + await db.commit() + await db.refresh(chat_item) + return ChatModel.model_validate(chat_item) + except Exception: + return None + + async def update_chat_tags_by_id(self, id: str, tags: list[str], user) -> Optional[ChatModel]: + async with get_async_db_context() as db: + chat = await db.get(Chat, id) + if chat is None: + return None + + old_tags = chat.meta.get('tags', []) + new_tags = [t for t in tags if t.replace(' ', '_').lower() != 'none'] + new_tag_ids = [t.replace(' ', '_').lower() for t in new_tags] + + # Single meta update + chat.meta = {**chat.meta, 'tags': new_tag_ids} + await db.commit() + await db.refresh(chat) + + # Batch-create any missing tag rows + await Tags.ensure_tags_exist(new_tags, user.id, db=db) + + # Clean up orphaned old tags in one query + removed = set(old_tags) - set(new_tag_ids) + if removed: + await self.delete_orphan_tags_for_user(list(removed), user.id, db=db) + + return ChatModel.model_validate(chat) + + async def get_chat_title_by_id(self, id: str) -> Optional[str]: + async with get_async_db_context() as db: + result = await db.execute(select(Chat.title).filter_by(id=id)) + row = result.first() + if row is None: + return None + return row[0] or 'New Chat' + + async def get_messages_map_by_chat_id(self, id: str) -> Optional[dict]: + chat = await self.get_chat_by_id(id) + if chat is None: + return None + + return chat.chat.get('history', {}).get('messages', {}) or {} + + async def get_message_by_id_and_message_id(self, id: str, message_id: str) -> Optional[dict]: + chat = await self.get_chat_by_id(id) + if chat is None: + return None + + return chat.chat.get('history', {}).get('messages', {}).get(message_id, {}) + + async def upsert_message_to_chat_by_id_and_message_id( + self, id: str, message_id: str, message: dict + ) -> Optional[ChatModel]: + chat = await self.get_chat_by_id(id) + if chat is None: + return None + + # Sanitize message content for null characters before upserting + if isinstance(message.get('content'), str): + message['content'] = sanitize_text_for_db(message['content']) + + user_id = chat.user_id + chat = chat.chat + history = chat.get('history', {}) + + if message_id in history.get('messages', {}): + history['messages'][message_id] = { + **history['messages'][message_id], + **message, + } + else: + history['messages'][message_id] = message + + history['currentId'] = message_id + + chat['history'] = history + + # Dual-write to chat_message table + try: + await ChatMessages.upsert_message( + message_id=message_id, + chat_id=id, + user_id=user_id, + data=history['messages'][message_id], + ) + except Exception as e: + log.warning(f'Failed to write to chat_message table: {e}') + + return await self.update_chat_by_id(id, chat) + + async def add_message_status_to_chat_by_id_and_message_id( + self, id: str, message_id: str, status: dict + ) -> Optional[ChatModel]: + chat = await self.get_chat_by_id(id) + if chat is None: + return None + + chat = chat.chat + history = chat.get('history', {}) + + if message_id in history.get('messages', {}): + status_history = history['messages'][message_id].get('statusHistory', []) + status_history.append(status) + history['messages'][message_id]['statusHistory'] = status_history + + chat['history'] = history + return await self.update_chat_by_id(id, chat) + + async def add_message_files_by_id_and_message_id(self, id: str, message_id: str, files: list[dict]) -> list[dict]: + async with get_async_db_context() as db: + chat = await self.get_chat_by_id(id, db=db) + if chat is None: + return None + + chat = chat.chat + history = chat.get('history', {}) + + message_files = [] + + if message_id in history.get('messages', {}): + message_files = history['messages'][message_id].get('files', []) + message_files = message_files + files + history['messages'][message_id]['files'] = message_files + + chat['history'] = history + await self.update_chat_by_id(id, chat, db=db) + return message_files + + async def insert_shared_chat_by_chat_id( + self, chat_id: str, db: Optional[AsyncSession] = None + ) -> Optional[ChatModel]: + """Create a shared snapshot for a chat. Returns the original chat with share_id set.""" + from open_webui.models.shared_chats import SharedChats + + async with get_async_db_context(db) as db: + chat = await db.get(Chat, chat_id) + if not chat: + return None + + # If already shared, just update the existing snapshot + if chat.share_id: + return await self.update_shared_chat_by_chat_id(chat_id, db=db) + + shared = await SharedChats.create(chat_id, chat.user_id, db=db) + if not shared: + return None + + # Set share_id on the original chat + chat.share_id = shared.id + await db.commit() + await db.refresh(chat) + return ChatModel.model_validate(chat) + + async def update_shared_chat_by_chat_id( + self, chat_id: str, db: Optional[AsyncSession] = None + ) -> Optional[ChatModel]: + """Re-snapshot the shared chat with current chat data.""" + from open_webui.models.shared_chats import SharedChats + + try: + async with get_async_db_context(db) as db: + chat = await db.get(Chat, chat_id) + if not chat or not chat.share_id: + return await self.insert_shared_chat_by_chat_id(chat_id, db=db) + + await SharedChats.update(chat.share_id, db=db) + return ChatModel.model_validate(chat) + except Exception: + return None + + async def delete_shared_chat_by_chat_id(self, chat_id: str, db: Optional[AsyncSession] = None) -> bool: + """Delete shared snapshot for a chat.""" + from open_webui.models.shared_chats import SharedChats + + try: + return await SharedChats.delete_by_chat_id(chat_id, db=db) + except Exception: + return False + + async def unarchive_all_chats_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + await db.execute(update(Chat).filter_by(user_id=user_id).values(archived=False)) + await db.commit() + return True + except Exception: + return False + + async def update_chat_share_id_by_id( + self, id: str, share_id: Optional[str], db: Optional[AsyncSession] = None + ) -> Optional[ChatModel]: + try: + async with get_async_db_context(db) as db: + chat = await db.get(Chat, id) + chat.share_id = share_id + await db.commit() + await db.refresh(chat) + return ChatModel.model_validate(chat) + except Exception: + return None + + async def toggle_chat_pinned_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[ChatModel]: + try: + async with get_async_db_context(db) as db: + chat = await db.get(Chat, id) + chat.pinned = not chat.pinned + chat.updated_at = int(time.time()) + await db.commit() + await db.refresh(chat) + return ChatModel.model_validate(chat) + except Exception: + return None + + async def toggle_chat_archive_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[ChatModel]: + try: + async with get_async_db_context(db) as db: + chat = await db.get(Chat, id) + chat.archived = not chat.archived + chat.folder_id = None + chat.updated_at = int(time.time()) + await db.commit() + await db.refresh(chat) + return ChatModel.model_validate(chat) + except Exception: + return None + + async def archive_all_chats_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + await db.execute(update(Chat).filter_by(user_id=user_id).values(archived=True)) + await db.commit() + return True + except Exception: + return False + + async def get_archived_chat_list_by_user_id( + self, + user_id: str, + filter: Optional[dict] = None, + skip: int = 0, + limit: int = 50, + db: Optional[AsyncSession] = None, + ) -> list[ChatTitleIdResponse]: + async with get_async_db_context(db) as db: + stmt = select(Chat.id, Chat.title, Chat.updated_at, Chat.created_at).filter_by( + user_id=user_id, archived=True + ) + + if filter: + query_key = filter.get('query') + if query_key: + stmt = stmt.filter(Chat.title.ilike(f'%{query_key}%')) + + order_by = filter.get('order_by') + direction = filter.get('direction') + + if order_by and direction: + if not getattr(Chat, order_by, None): + raise ValueError('Invalid order_by field') + + if direction.lower() == 'asc': + stmt = stmt.order_by(getattr(Chat, order_by).asc(), Chat.id) + elif direction.lower() == 'desc': + stmt = stmt.order_by(getattr(Chat, order_by).desc(), Chat.id) + else: + raise ValueError('Invalid direction for ordering') + else: + stmt = stmt.order_by(Chat.updated_at.desc(), Chat.id) + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + all_chats = result.all() + return [ + ChatTitleIdResponse.model_validate( + { + 'id': chat[0], + 'title': chat[1], + 'updated_at': chat[2], + 'created_at': chat[3], + } + ) + for chat in all_chats + ] + + async def get_shared_chat_list_by_user_id( + self, + user_id: str, + filter: Optional[dict] = None, + skip: int = 0, + limit: int = 50, + db: Optional[AsyncSession] = None, + ) -> list[SharedChatResponse]: + """Delegate to SharedChats for listing shared chats by user.""" + from open_webui.models.shared_chats import SharedChats + + return await SharedChats.get_by_user_id(user_id, filter=filter, skip=skip, limit=limit, db=db) + + async def get_chat_list_by_user_id( + self, + user_id: str, + include_archived: bool = False, + filter: Optional[dict] = None, + skip: int = 0, + limit: int = 50, + db: Optional[AsyncSession] = None, + ) -> list[ChatTitleIdResponse]: + async with get_async_db_context(db) as db: + stmt = select(Chat.id, Chat.title, Chat.updated_at, Chat.created_at, Chat.last_read_at).filter_by( + user_id=user_id + ) + if not include_archived: + stmt = stmt.filter_by(archived=False) + + if filter: + query_key = filter.get('query') + if query_key: + stmt = stmt.filter(Chat.title.ilike(f'%{query_key}%')) + + order_by = filter.get('order_by') + direction = filter.get('direction') + + if order_by and direction and getattr(Chat, order_by): + if direction.lower() == 'asc': + stmt = stmt.order_by(getattr(Chat, order_by).asc(), Chat.id) + elif direction.lower() == 'desc': + stmt = stmt.order_by(getattr(Chat, order_by).desc(), Chat.id) + else: + raise ValueError('Invalid direction for ordering') + else: + stmt = stmt.order_by(Chat.updated_at.desc(), Chat.id) + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + all_chats = result.all() + return [ + ChatTitleIdResponse.model_validate( + { + 'id': chat[0], + 'title': chat[1], + 'updated_at': chat[2], + 'created_at': chat[3], + 'last_read_at': chat[4], + } + ) + for chat in all_chats + ] + + async def get_chat_title_id_list_by_user_id( + self, + user_id: str, + include_archived: bool = False, + include_folders: bool = False, + include_pinned: bool = False, + skip: Optional[int] = None, + limit: Optional[int] = None, + db: Optional[AsyncSession] = None, + ) -> list[ChatTitleIdResponse]: + async with get_async_db_context(db) as db: + stmt = select(Chat.id, Chat.title, Chat.updated_at, Chat.created_at, Chat.last_read_at).filter_by( + user_id=user_id + ) + + if not include_folders: + stmt = stmt.filter_by(folder_id=None) + + if not include_pinned: + stmt = stmt.filter(or_(Chat.pinned == False, Chat.pinned == None)) + + if not include_archived: + stmt = stmt.filter_by(archived=False) + + stmt = stmt.order_by(Chat.updated_at.desc(), Chat.id) + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + all_chats = result.all() + + return [ + ChatTitleIdResponse.model_validate( + { + 'id': chat[0], + 'title': chat[1], + 'updated_at': chat[2], + 'created_at': chat[3], + 'last_read_at': chat[4], + } + ) + for chat in all_chats + ] + + async def get_chat_list_by_chat_ids( + self, + chat_ids: list[str], + skip: int = 0, + limit: int = 50, + db: Optional[AsyncSession] = None, + ) -> list[ChatModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(Chat).filter(Chat.id.in_(chat_ids)).filter_by(archived=False).order_by(Chat.updated_at.desc()) + ) + all_chats = result.scalars().all() + return [ChatModel.model_validate(chat) for chat in all_chats] + + async def get_chat_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[ChatModel]: + try: + async with get_async_db_context(db) as db: + chat_item = await db.get(Chat, id) + if chat_item is None: + return None + + if self._sanitize_chat_row(chat_item): + await db.commit() + await db.refresh(chat_item) + + return ChatModel.model_validate(chat_item) + except Exception: + return None + + async def get_chat_by_share_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[ChatModel]: + """Look up a shared chat snapshot by its share token.""" + from open_webui.models.shared_chats import SharedChats + + try: + shared = await SharedChats.get_by_id(id, db=db) + if shared: + # Return a ChatModel-compatible view of the snapshot + return ChatModel( + id=shared.id, + user_id=shared.user_id, + title=shared.title, + chat=shared.chat, + created_at=shared.created_at, + updated_at=shared.updated_at, + share_id=shared.id, + ) + return None + except Exception: + return None + + async def get_chat_by_id_and_user_id( + self, id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> Optional[ChatModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Chat).filter_by(id=id, user_id=user_id)) + chat = result.scalars().first() + return ChatModel.model_validate(chat) if chat else None + except Exception: + return None + + async def is_chat_owner(self, id: str, user_id: str, db: Optional[AsyncSession] = None) -> bool: + """ + Lightweight ownership check — uses EXISTS subquery instead of loading + the full Chat row (which includes the potentially large JSON blob). + """ + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(exists().where(and_(Chat.id == id, Chat.user_id == user_id)))) + return result.scalar() + except Exception: + return False + + async def get_chat_folder_id(self, id: str, user_id: str, db: Optional[AsyncSession] = None) -> Optional[str]: + """ + Fetch only the folder_id column for a chat, without loading the full + JSON blob. Returns None if chat doesn't exist or doesn't belong to user. + """ + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Chat.folder_id).filter_by(id=id, user_id=user_id)) + row = result.first() + return row[0] if row else None + except Exception: + return None + + async def get_chats(self, skip: int = 0, limit: int = 50, db: Optional[AsyncSession] = None) -> list[ChatModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Chat).order_by(Chat.updated_at.desc())) + all_chats = result.scalars().all() + return [ChatModel.model_validate(chat) for chat in all_chats] + + async def get_chats_by_user_id( + self, + user_id: str, + filter: Optional[dict] = None, + skip: Optional[int] = None, + limit: Optional[int] = None, + db: Optional[AsyncSession] = None, + ) -> ChatListResponse: + async with get_async_db_context(db) as db: + stmt = select(Chat).filter_by(user_id=user_id) + + if filter: + if filter.get('updated_at'): + stmt = stmt.filter(Chat.updated_at > filter.get('updated_at')) + + order_by = filter.get('order_by') + direction = filter.get('direction') + + if order_by and direction: + if hasattr(Chat, order_by): + if direction.lower() == 'asc': + stmt = stmt.order_by(getattr(Chat, order_by).asc(), Chat.id) + elif direction.lower() == 'desc': + stmt = stmt.order_by(getattr(Chat, order_by).desc(), Chat.id) + else: + stmt = stmt.order_by(Chat.updated_at.desc(), Chat.id) + + else: + stmt = stmt.order_by(Chat.updated_at.desc(), Chat.id) + + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + + if skip is not None: + stmt = stmt.offset(skip) + if limit is not None: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + all_chats = result.scalars().all() + + return ChatListResponse( + **{ + 'items': [ChatModel.model_validate(chat) for chat in all_chats], + 'total': total, + } + ) + + async def get_pinned_chats_by_user_id( + self, user_id: str, db: Optional[AsyncSession] = None + ) -> list[ChatTitleIdResponse]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(Chat.id, Chat.title, Chat.updated_at, Chat.created_at, Chat.last_read_at) + .filter_by(user_id=user_id, pinned=True, archived=False) + .order_by(Chat.updated_at.desc()) + ) + all_chats = result.all() + return [ + ChatTitleIdResponse.model_validate( + { + 'id': chat[0], + 'title': chat[1], + 'updated_at': chat[2], + 'created_at': chat[3], + 'last_read_at': chat[4], + } + ) + for chat in all_chats + ] + + async def get_archived_chats_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> list[ChatModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(Chat).filter_by(user_id=user_id, archived=True).order_by(Chat.updated_at.desc()) + ) + return [ChatModel.model_validate(chat) for chat in result.scalars().all()] + + async def get_chats_by_user_id_and_search_text( + self, + user_id: str, + search_text: str, + include_archived: bool = False, + skip: int = 0, + limit: int = 60, + db: Optional[AsyncSession] = None, + ) -> list[ChatModel]: + """ + Filters chats based on a search query using Python, allowing pagination using skip and limit. + """ + search_text = sanitize_text_for_db(search_text).lower().strip() + + if not search_text: + return await self.get_chat_list_by_user_id( + user_id, include_archived, filter={}, skip=skip, limit=limit, db=db + ) + + search_text_words = search_text.split(' ') + + # search_text might contain 'tag:tag_name' format so we need to extract the tag_name + tag_ids = [ + word.replace('tag:', '').replace(' ', '_').lower() for word in search_text_words if word.startswith('tag:') + ] + + # Extract folder names + folders = await Folders.search_folders_by_names( + user_id, + [word.replace('folder:', '') for word in search_text_words if word.startswith('folder:')], + ) + folder_ids = [folder.id for folder in folders] + + is_pinned = None + if 'pinned:true' in search_text_words: + is_pinned = True + elif 'pinned:false' in search_text_words: + is_pinned = False + + is_archived = None + if 'archived:true' in search_text_words: + is_archived = True + elif 'archived:false' in search_text_words: + is_archived = False + + is_shared = None + if 'shared:true' in search_text_words: + is_shared = True + elif 'shared:false' in search_text_words: + is_shared = False + + search_text_words = [ + word + for word in search_text_words + if ( + not word.startswith('tag:') + and not word.startswith('folder:') + and not word.startswith('pinned:') + and not word.startswith('archived:') + and not word.startswith('shared:') + ) + ] + + search_text = ' '.join(search_text_words) + + async with get_async_db_context(db) as db: + stmt = select(Chat).filter(Chat.user_id == user_id) + + if is_archived is not None: + stmt = stmt.filter(Chat.archived == is_archived) + elif not include_archived: + stmt = stmt.filter(Chat.archived == False) + + if is_pinned is not None: + stmt = stmt.filter(Chat.pinned == is_pinned) + + if is_shared is not None: + if is_shared: + stmt = stmt.filter(Chat.share_id.isnot(None)) + else: + stmt = stmt.filter(Chat.share_id.is_(None)) + + if folder_ids: + stmt = stmt.filter(Chat.folder_id.in_(folder_ids)) + + stmt = stmt.order_by(Chat.updated_at.desc(), Chat.id) + + # Check if the database dialect is either 'sqlite' or 'postgresql' + bind = await db.connection() + dialect_name = bind.dialect.name + if dialect_name == 'sqlite': + # SQLite case: using JSON1 extension for JSON searching + sqlite_content_sql = ( + 'EXISTS (' + ' SELECT 1 ' + " FROM json_each(Chat.chat, '$.messages') AS message " + " WHERE LOWER(message.value->>'content') LIKE '%' || :content_key || '%'" + ')' + ) + sqlite_content_clause = text(sqlite_content_sql) + stmt = stmt.filter( + or_(Chat.title.ilike(bindparam('title_key')), sqlite_content_clause).params( + title_key=f'%{search_text}%', content_key=search_text + ) + ) + + # Check if there are any tags to filter + if 'none' in tag_ids: + stmt = stmt.filter( + text(""" + NOT EXISTS ( + SELECT 1 + FROM json_each(Chat.meta, '$.tags') AS tag + ) + """) + ) + elif tag_ids: + stmt = stmt.filter( + and_( + *[ + text(f""" + EXISTS ( + SELECT 1 + FROM json_each(Chat.meta, '$.tags') AS tag + WHERE tag.value = :tag_id_{tag_idx} + ) + """).params(**{f'tag_id_{tag_idx}': tag_id}) + for tag_idx, tag_id in enumerate(tag_ids) + ] + ) + ) + + elif dialect_name == 'postgresql': + # Safety filter: JSON field must not contain \u0000 + stmt = stmt.filter(text("Chat.chat::text NOT LIKE '%\\\\u0000%'")) + + # Safety filter: title must not contain actual null bytes + stmt = stmt.filter(text("Chat.title::text NOT LIKE '%\\x00%'")) + + postgres_content_sql = """ + EXISTS ( + SELECT 1 + FROM json_array_elements(Chat.chat->'messages') AS message + WHERE json_typeof(message->'content') = 'string' + AND LOWER(message->>'content') LIKE '%' || :content_key || '%' + ) + """ + + postgres_content_clause = text(postgres_content_sql) + + stmt = stmt.filter( + or_( + Chat.title.ilike(bindparam('title_key')), + postgres_content_clause, + ) + ).params(title_key=f'%{search_text}%', content_key=search_text.lower()) + + if 'none' in tag_ids: + stmt = stmt.filter( + text(""" + NOT EXISTS ( + SELECT 1 + FROM json_array_elements_text(Chat.meta->'tags') AS tag + ) + """) + ) + elif tag_ids: + stmt = stmt.filter( + and_( + *[ + text(f""" + EXISTS ( + SELECT 1 + FROM json_array_elements_text(Chat.meta->'tags') AS tag + WHERE tag = :tag_id_{tag_idx} + ) + """).params(**{f'tag_id_{tag_idx}': tag_id}) + for tag_idx, tag_id in enumerate(tag_ids) + ] + ) + ) + else: + raise NotImplementedError(f'Unsupported dialect: {dialect_name}') + + # Perform pagination at the SQL level + stmt = stmt.offset(skip).limit(limit) + result = await db.execute(stmt) + all_chats = result.scalars().all() + + log.info(f'The number of chats: {len(all_chats)}') + + # Validate and return chats + return [ChatModel.model_validate(chat) for chat in all_chats] + + async def get_chats_by_folder_id_and_user_id( + self, + folder_id: str, + user_id: str, + skip: int = 0, + limit: int = 60, + db: Optional[AsyncSession] = None, + ) -> list[ChatTitleIdResponse]: + async with get_async_db_context(db) as db: + stmt = ( + select(Chat.id, Chat.title, Chat.updated_at, Chat.created_at, Chat.last_read_at) + .filter_by(folder_id=folder_id, user_id=user_id) + .filter(or_(Chat.pinned == False, Chat.pinned == None)) + .filter_by(archived=False) + .order_by(Chat.updated_at.desc(), Chat.id) + ) + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + all_chats = result.all() + return [ + ChatTitleIdResponse.model_validate( + { + 'id': chat[0], + 'title': chat[1], + 'updated_at': chat[2], + 'created_at': chat[3], + 'last_read_at': chat[4], + } + ) + for chat in all_chats + ] + + async def get_chats_by_folder_ids_and_user_id( + self, folder_ids: list[str], user_id: str, db: Optional[AsyncSession] = None + ) -> list[ChatModel]: + async with get_async_db_context(db) as db: + stmt = ( + select(Chat) + .filter(Chat.folder_id.in_(folder_ids), Chat.user_id == user_id) + .filter(or_(Chat.pinned == False, Chat.pinned == None)) + .filter_by(archived=False) + .order_by(Chat.updated_at.desc()) + ) + + result = await db.execute(stmt) + all_chats = result.scalars().all() + return [ChatModel.model_validate(chat) for chat in all_chats] + + async def update_chat_folder_id_by_id_and_user_id( + self, id: str, user_id: str, folder_id: str, db: Optional[AsyncSession] = None + ) -> Optional[ChatModel]: + try: + async with get_async_db_context(db) as db: + chat = await db.get(Chat, id) + chat.folder_id = folder_id + chat.updated_at = int(time.time()) + chat.pinned = False + await db.commit() + await db.refresh(chat) + return ChatModel.model_validate(chat) + except Exception: + return None + + async def get_chat_tags_by_id_and_user_id( + self, id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> list[TagModel]: + async with get_async_db_context(db) as db: + stmt = select(Chat.meta).where(Chat.id == id) + result = await db.execute(stmt) + meta = result.scalar_one_or_none() + tag_ids = (meta or {}).get('tags', []) + return await Tags.get_tags_by_ids_and_user_id(tag_ids, user_id, db=db) + + async def get_chat_list_by_user_id_and_tag_name( + self, + user_id: str, + tag_name: str, + skip: int = 0, + limit: int = 50, + db: Optional[AsyncSession] = None, + ) -> list[ChatTitleIdResponse]: + async with get_async_db_context(db) as db: + stmt = select(Chat.id, Chat.title, Chat.updated_at, Chat.created_at, Chat.last_read_at).filter_by( + user_id=user_id + ) + tag_id = tag_name.replace(' ', '_').lower() + + bind = await db.connection() + dialect_name = bind.dialect.name + log.info(f'DB dialect name: {dialect_name}') + if dialect_name == 'sqlite': + stmt = stmt.filter( + text(f"EXISTS (SELECT 1 FROM json_each(Chat.meta, '$.tags') WHERE json_each.value = :tag_id)") + ).params(tag_id=tag_id) + elif dialect_name == 'postgresql': + stmt = stmt.filter( + text("EXISTS (SELECT 1 FROM json_array_elements_text(Chat.meta->'tags') elem WHERE elem = :tag_id)") + ).params(tag_id=tag_id) + else: + raise NotImplementedError(f'Unsupported dialect: {dialect_name}') + + stmt = stmt.order_by(Chat.updated_at.desc(), Chat.id) + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + all_chats = result.all() + return [ + ChatTitleIdResponse.model_validate( + { + 'id': chat[0], + 'title': chat[1], + 'updated_at': chat[2], + 'created_at': chat[3], + 'last_read_at': chat[4], + } + ) + for chat in all_chats + ] + + async def add_chat_tag_by_id_and_user_id_and_tag_name( + self, id: str, user_id: str, tag_name: str, db: Optional[AsyncSession] = None + ) -> Optional[ChatModel]: + tag_id = tag_name.replace(' ', '_').lower() + await Tags.ensure_tags_exist([tag_name], user_id, db=db) + try: + async with get_async_db_context(db) as db: + chat = await db.get(Chat, id) + if tag_id not in chat.meta.get('tags', []): + chat.meta = { + **chat.meta, + 'tags': list(set(chat.meta.get('tags', []) + [tag_id])), + } + await db.commit() + await db.refresh(chat) + return ChatModel.model_validate(chat) + except Exception: + return None + + async def count_chats_by_tag_name_and_user_id( + self, tag_name: str, user_id: str, db: Optional[AsyncSession] = None + ) -> int: + async with get_async_db_context(db) as db: + stmt = select(func.count(Chat.id)).filter_by(user_id=user_id, archived=False) + tag_id = tag_name.replace(' ', '_').lower() + + bind = await db.connection() + dialect_name = bind.dialect.name + if dialect_name == 'sqlite': + stmt = stmt.filter( + text("EXISTS (SELECT 1 FROM json_each(Chat.meta, '$.tags') WHERE json_each.value = :tag_id)") + ).params(tag_id=tag_id) + elif dialect_name == 'postgresql': + stmt = stmt.filter( + text("EXISTS (SELECT 1 FROM json_array_elements_text(Chat.meta->'tags') elem WHERE elem = :tag_id)") + ).params(tag_id=tag_id) + else: + raise NotImplementedError(f'Unsupported dialect: {dialect_name}') + + result = await db.execute(stmt) + return result.scalar() + + async def delete_orphan_tags_for_user( + self, + tag_ids: list[str], + user_id: str, + threshold: int = 0, + db: Optional[AsyncSession] = None, + ) -> None: + """Delete tag rows from *tag_ids* that appear in at most *threshold* + non-archived chats for *user_id*. One query to find orphans, one to + delete them. + + Use threshold=0 after a tag is already removed from a chat's meta. + Use threshold=1 when the chat itself is about to be deleted (the + referencing chat still exists at query time). + """ + if not tag_ids: + return + async with get_async_db_context(db) as db: + orphans = [] + for tag_id in tag_ids: + count = await self.count_chats_by_tag_name_and_user_id(tag_id, user_id, db=db) + if count <= threshold: + orphans.append(tag_id) + await Tags.delete_tags_by_ids_and_user_id(orphans, user_id, db=db) + + async def count_chats_by_folder_id_and_user_id( + self, folder_id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> int: + async with get_async_db_context(db) as db: + result = await db.execute(select(func.count(Chat.id)).filter_by(user_id=user_id, folder_id=folder_id)) + count = result.scalar() + + log.info(f"Count of chats for folder '{folder_id}': {count}") + return count + + async def delete_tag_by_id_and_user_id_and_tag_name( + self, id: str, user_id: str, tag_name: str, db: Optional[AsyncSession] = None + ) -> bool: + try: + async with get_async_db_context(db) as db: + chat = await db.get(Chat, id) + tags = chat.meta.get('tags', []) + tag_id = tag_name.replace(' ', '_').lower() + + tags = [tag for tag in tags if tag != tag_id] + chat.meta = { + **chat.meta, + 'tags': list(set(tags)), + } + await db.commit() + return True + except Exception: + return False + + async def delete_all_tags_by_id_and_user_id(self, id: str, user_id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + chat = await db.get(Chat, id) + chat.meta = { + **chat.meta, + 'tags': [], + } + await db.commit() + + return True + except Exception: + return False + + async def delete_chat_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + await db.execute(update(AutomationRun).filter_by(chat_id=id).values(chat_id=None)) + await db.execute(delete(ChatMessage).filter_by(chat_id=id)) + await db.execute(delete(Chat).filter_by(id=id)) + await db.commit() + + return True and await self.delete_shared_chat_by_chat_id(id, db=db) + except Exception: + return False + + async def delete_chat_by_id_and_user_id(self, id: str, user_id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + await db.execute(update(AutomationRun).filter_by(chat_id=id).values(chat_id=None)) + await db.execute(delete(ChatMessage).filter_by(chat_id=id)) + await db.execute(delete(Chat).filter_by(id=id, user_id=user_id)) + await db.commit() + + return True and await self.delete_shared_chat_by_chat_id(id, db=db) + except Exception: + return False + + async def delete_chats_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + await self.delete_shared_chats_by_user_id(user_id, db=db) + + chat_id_subquery = select(Chat.id).filter_by(user_id=user_id).scalar_subquery() + await db.execute( + update(AutomationRun) + .filter(AutomationRun.chat_id.in_(select(Chat.id).filter_by(user_id=user_id))) + .values(chat_id=None) + ) + await db.execute( + delete(ChatMessage).filter(ChatMessage.chat_id.in_(select(Chat.id).filter_by(user_id=user_id))) + ) + await db.execute(delete(Chat).filter_by(user_id=user_id)) + await db.commit() + + return True + except Exception: + return False + + async def delete_chats_by_user_id_and_folder_id( + self, user_id: str, folder_id: str, db: Optional[AsyncSession] = None + ) -> bool: + try: + async with get_async_db_context(db) as db: + chat_ids_stmt = select(Chat.id).filter_by(user_id=user_id, folder_id=folder_id) + await db.execute( + update(AutomationRun).filter(AutomationRun.chat_id.in_(chat_ids_stmt)).values(chat_id=None) + ) + await db.execute(delete(ChatMessage).filter(ChatMessage.chat_id.in_(chat_ids_stmt))) + await db.execute(delete(Chat).filter_by(user_id=user_id, folder_id=folder_id)) + await db.commit() + + return True + except Exception: + return False + + async def move_chats_by_user_id_and_folder_id( + self, + user_id: str, + folder_id: str, + new_folder_id: Optional[str], + db: Optional[AsyncSession] = None, + ) -> bool: + try: + async with get_async_db_context(db) as db: + await db.execute( + update(Chat).filter_by(user_id=user_id, folder_id=folder_id).values(folder_id=new_folder_id) + ) + await db.commit() + + return True + except Exception: + return False + + async def delete_shared_chats_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> bool: + """Delete all shared chat snapshots created by a user.""" + from open_webui.models.shared_chats import SharedChats, SharedChat as SharedChatTable + + try: + async with get_async_db_context(db) as db: + # Delete shared_chat rows for this user's chats + await db.execute(delete(SharedChatTable).filter_by(user_id=user_id)) + + # Clear share_id on all of this user's chats + await db.execute(update(Chat).filter_by(user_id=user_id).values(share_id=None)) + await db.commit() + + return True + except Exception: + return False + + async def insert_chat_files( + self, + chat_id: str, + message_id: str, + file_ids: list[str], + user_id: str, + db: Optional[AsyncSession] = None, + ) -> Optional[list[ChatFileModel]]: + if not file_ids: + return None + + chat_message_file_ids = { + item.id for item in await self.get_chat_files_by_chat_id_and_message_id(chat_id, message_id, db=db) + } + # Remove duplicates and existing file_ids + file_ids = list({file_id for file_id in file_ids if file_id and file_id not in chat_message_file_ids}) + if not file_ids: + return None + + try: + async with get_async_db_context(db) as db: + now = int(time.time()) + + chat_files = [ + ChatFileModel( + id=str(uuid.uuid4()), + user_id=user_id, + chat_id=chat_id, + message_id=message_id, + file_id=file_id, + created_at=now, + updated_at=now, + ) + for file_id in file_ids + ] + + results = [ChatFile(**chat_file.model_dump()) for chat_file in chat_files] + + db.add_all(results) + await db.commit() + + return chat_files + except Exception: + return None + + async def get_chat_files_by_chat_id_and_message_id( + self, chat_id: str, message_id: str, db: Optional[AsyncSession] = None + ) -> list[ChatFileModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChatFile).filter_by(chat_id=chat_id, message_id=message_id).order_by(ChatFile.created_at.asc()) + ) + all_chat_files = result.scalars().all() + return [ChatFileModel.model_validate(chat_file) for chat_file in all_chat_files] + + async def delete_chat_file(self, chat_id: str, file_id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + await db.execute(delete(ChatFile).filter_by(chat_id=chat_id, file_id=file_id)) + await db.commit() + return True + except Exception: + return False + + async def get_shared_chat_ids_by_file_id(self, file_id: str, db: Optional[AsyncSession] = None) -> list[str]: + """Return IDs of chats that contain this file and have an active share link.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(Chat.id) + .join(ChatFile, Chat.id == ChatFile.chat_id) + .filter(ChatFile.file_id == file_id, Chat.share_id.isnot(None)) + ) + return [row[0] for row in result.all()] + + async def update_chat_tasks_by_id(self, id: str, tasks: list[dict]) -> Optional[ChatModel]: + """Update the tasks list on a chat.""" + try: + async with get_async_db_context() as db: + chat = await db.get(Chat, id) + if chat is None: + return None + chat.tasks = tasks + await db.commit() + await db.refresh(chat) + return ChatModel.model_validate(chat) + except Exception: + return None + + async def get_chat_tasks_by_id(self, id: str) -> list[dict]: + """Read the tasks list from a chat (lightweight column query).""" + async with get_async_db_context() as db: + result = await db.execute(select(Chat.tasks).filter_by(id=id)) + row = result.first() + if row is None or row[0] is None: + return [] + return row[0] + + +Chats = ChatTable() diff --git a/backend/open_webui/models/feedbacks.py b/backend/open_webui/models/feedbacks.py new file mode 100644 index 0000000000000000000000000000000000000000..02f61f82ee4d39fa42d1ed811ab794fc061e8e0d --- /dev/null +++ b/backend/open_webui/models/feedbacks.py @@ -0,0 +1,467 @@ +import logging +import time +import uuid +from typing import Optional + +from sqlalchemy import select, delete, func +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.models.users import User, UserModel + +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, Text, JSON, Boolean + +log = logging.getLogger(__name__) + + +#################### +# Feedback DB Schema +#################### + + +class Feedback(Base): + __tablename__ = 'feedback' + id = Column(Text, primary_key=True, unique=True) + user_id = Column(Text) + version = Column(BigInteger, default=0) + type = Column(Text) + data = Column(JSON, nullable=True) + meta = Column(JSON, nullable=True) + snapshot = Column(JSON, nullable=True) + created_at = Column(BigInteger) + updated_at = Column(BigInteger) + + +class FeedbackModel(BaseModel): + id: str + user_id: str + version: int + type: str + data: Optional[dict] = None + meta: Optional[dict] = None + snapshot: Optional[dict] = None + created_at: int + updated_at: int + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class FeedbackResponse(BaseModel): + id: str + user_id: str + version: int + type: str + data: Optional[dict] = None + meta: Optional[dict] = None + created_at: int + updated_at: int + + +class FeedbackIdResponse(BaseModel): + id: str + user_id: str + created_at: int + updated_at: int + + +class LeaderboardFeedbackData(BaseModel): + """Minimal feedback data for leaderboard computation (excludes snapshot/meta).""" + + id: str + data: Optional[dict] = None + + +class RatingData(BaseModel): + rating: Optional[str | int] = None + model_id: Optional[str] = None + sibling_model_ids: Optional[list[str]] = None + reason: Optional[str] = None + comment: Optional[str] = None + model_config = ConfigDict(extra='allow', protected_namespaces=()) + + +class MetaData(BaseModel): + arena: Optional[bool] = None + chat_id: Optional[str] = None + message_id: Optional[str] = None + tags: Optional[list[str]] = None + model_config = ConfigDict(extra='allow') + + +class SnapshotData(BaseModel): + chat: Optional[dict] = None + model_config = ConfigDict(extra='allow') + + +class FeedbackForm(BaseModel): + type: str + data: Optional[RatingData] = None + meta: Optional[dict] = None + snapshot: Optional[SnapshotData] = None + model_config = ConfigDict(extra='allow') + + +class UserResponse(BaseModel): + id: str + name: str + email: str + role: str = 'pending' + + last_active_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +class FeedbackUserResponse(FeedbackResponse): + user: Optional[UserResponse] = None + + +class FeedbackListResponse(BaseModel): + items: list[FeedbackUserResponse] + total: int + + +class ModelHistoryEntry(BaseModel): + date: str + won: int + lost: int + + +class ModelHistoryResponse(BaseModel): + model_id: str + history: list[ModelHistoryEntry] + + +class FeedbackTable: + async def insert_new_feedback( + self, user_id: str, form_data: FeedbackForm, db: Optional[AsyncSession] = None + ) -> Optional[FeedbackModel]: + async with get_async_db_context(db) as db: + id = str(uuid.uuid4()) + feedback = FeedbackModel( + **{ + 'id': id, + 'user_id': user_id, + 'version': 0, + **form_data.model_dump(), + 'created_at': int(time.time()), + 'updated_at': int(time.time()), + } + ) + try: + result = Feedback(**feedback.model_dump()) + db.add(result) + await db.commit() + await db.refresh(result) + if result: + return FeedbackModel.model_validate(result) + else: + return None + except Exception as e: + log.exception(f'Error creating a new feedback: {e}') + return None + + async def get_feedback_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[FeedbackModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback).filter_by(id=id)) + feedback = result.scalars().first() + if not feedback: + return None + return FeedbackModel.model_validate(feedback) + except Exception: + return None + + async def get_feedback_by_id_and_user_id( + self, id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> Optional[FeedbackModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback).filter_by(id=id, user_id=user_id)) + feedback = result.scalars().first() + if not feedback: + return None + return FeedbackModel.model_validate(feedback) + except Exception: + return None + + async def get_feedbacks_by_chat_id(self, chat_id: str, db: Optional[AsyncSession] = None) -> list[FeedbackModel]: + """Get all feedbacks for a specific chat.""" + try: + async with get_async_db_context(db) as db: + # meta.chat_id stores the chat reference + result = await db.execute( + select(Feedback) + .filter(Feedback.meta['chat_id'].as_string() == chat_id) + .order_by(Feedback.created_at.desc()) + ) + feedbacks = result.scalars().all() + return [FeedbackModel.model_validate(fb) for fb in feedbacks] + except Exception: + return [] + + async def get_feedback_items( + self, + filter: dict = {}, + skip: int = 0, + limit: int = 30, + db: Optional[AsyncSession] = None, + ) -> FeedbackListResponse: + async with get_async_db_context(db) as db: + stmt = select(Feedback, User).join(User, Feedback.user_id == User.id) + + if filter: + # Apply model_id filter (exact match) + model_id = filter.get('model_id') + if model_id: + stmt = stmt.filter(Feedback.data['model_id'].as_string() == model_id) + + order_by = filter.get('order_by') + direction = filter.get('direction') + + if order_by == 'username': + if direction == 'asc': + stmt = stmt.order_by(User.name.asc()) + else: + stmt = stmt.order_by(User.name.desc()) + elif order_by == 'model_id': + if direction == 'asc': + stmt = stmt.order_by(Feedback.data['model_id'].as_string().asc()) + else: + stmt = stmt.order_by(Feedback.data['model_id'].as_string().desc()) + elif order_by == 'rating': + if direction == 'asc': + stmt = stmt.order_by(Feedback.data['rating'].as_string().asc()) + else: + stmt = stmt.order_by(Feedback.data['rating'].as_string().desc()) + elif order_by == 'updated_at': + if direction == 'asc': + stmt = stmt.order_by(Feedback.updated_at.asc()) + else: + stmt = stmt.order_by(Feedback.updated_at.desc()) + + else: + stmt = stmt.order_by(Feedback.created_at.desc()) + + # Count BEFORE pagination + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + items = result.all() + + feedbacks = [] + for feedback, user in items: + feedback_model = FeedbackModel.model_validate(feedback) + user_model = UserResponse.model_validate(user) + feedbacks.append(FeedbackUserResponse(**feedback_model.model_dump(), user=user_model)) + + return FeedbackListResponse(items=feedbacks, total=total) + + async def get_all_feedbacks(self, db: Optional[AsyncSession] = None) -> list[FeedbackModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback).order_by(Feedback.updated_at.desc())) + return [FeedbackModel.model_validate(feedback) for feedback in result.scalars().all()] + + async def get_all_feedback_ids(self, db: Optional[AsyncSession] = None) -> list[FeedbackIdResponse]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(Feedback.id, Feedback.user_id, Feedback.created_at, Feedback.updated_at).order_by( + Feedback.updated_at.desc() + ) + ) + return [ + FeedbackIdResponse( + id=row.id, + user_id=row.user_id, + created_at=row.created_at, + updated_at=row.updated_at, + ) + for row in result.all() + ] + + async def get_distinct_model_ids(self, db: Optional[AsyncSession] = None) -> list[str]: + """Get distinct model_ids from feedback data for filter dropdowns.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(Feedback.data['model_id'].as_string()) + .filter(Feedback.data['model_id'].as_string().isnot(None)) + .distinct() + ) + rows = result.all() + return sorted([row[0] for row in rows if row[0]]) + + async def get_feedbacks_for_leaderboard(self, db: Optional[AsyncSession] = None) -> list[LeaderboardFeedbackData]: + """Fetch only id and data for leaderboard computation (excludes snapshot/meta).""" + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback.id, Feedback.data)) + return [LeaderboardFeedbackData(id=row.id, data=row.data) for row in result.all()] + + async def get_model_evaluation_history( + self, model_id: str, days: int = 30, db: Optional[AsyncSession] = None + ) -> list[ModelHistoryEntry]: + """ + Get daily wins/losses for a specific model over the past N days. + If days=0, returns all time data starting from first feedback. + Returns: [{"date": "2026-01-08", "won": 5, "lost": 2}, ...] + """ + from datetime import datetime, timedelta + from collections import defaultdict + + async with get_async_db_context(db) as db: + if days == 0: + # All time - no cutoff + result = await db.execute(select(Feedback.created_at, Feedback.data)) + else: + cutoff = int(time.time()) - (days * 86400) + result = await db.execute( + select(Feedback.created_at, Feedback.data).filter(Feedback.created_at >= cutoff) + ) + rows = result.all() + + daily_counts = defaultdict(lambda: {'won': 0, 'lost': 0}) + first_date = None + + for created_at, data in rows: + if not data: + continue + if data.get('model_id') != model_id: + continue + + rating_str = str(data.get('rating', '')) + if rating_str not in ('1', '-1'): + continue + + date_str = datetime.fromtimestamp(created_at).strftime('%Y-%m-%d') + if rating_str == '1': + daily_counts[date_str]['won'] += 1 + else: + daily_counts[date_str]['lost'] += 1 + + # Track first date for this model + if first_date is None or date_str < first_date: + first_date = date_str + + # Generate date range + result = [] + today = datetime.now().date() + + if days == 0 and first_date: + # All time: start from first feedback date + start_date = datetime.strptime(first_date, '%Y-%m-%d').date() + num_days = (today - start_date).days + 1 + else: + # Fixed range + num_days = days + start_date = today - timedelta(days=days - 1) + + for i in range(num_days): + d = start_date + timedelta(days=i) + date_str = d.strftime('%Y-%m-%d') + counts = daily_counts.get(date_str, {'won': 0, 'lost': 0}) + result.append(ModelHistoryEntry(date=date_str, won=counts['won'], lost=counts['lost'])) + + return result + + async def get_feedbacks_by_type(self, type: str, db: Optional[AsyncSession] = None) -> list[FeedbackModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback).filter_by(type=type).order_by(Feedback.updated_at.desc())) + return [FeedbackModel.model_validate(feedback) for feedback in result.scalars().all()] + + async def get_feedbacks_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> list[FeedbackModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback).filter_by(user_id=user_id).order_by(Feedback.updated_at.desc())) + return [FeedbackModel.model_validate(feedback) for feedback in result.scalars().all()] + + async def update_feedback_by_id( + self, id: str, form_data: FeedbackForm, db: Optional[AsyncSession] = None + ) -> Optional[FeedbackModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback).filter_by(id=id)) + feedback = result.scalars().first() + if not feedback: + return None + + if form_data.data: + feedback.data = form_data.data.model_dump() + if form_data.meta: + feedback.meta = form_data.meta + if form_data.snapshot: + feedback.snapshot = form_data.snapshot.model_dump() + + feedback.updated_at = int(time.time()) + + await db.commit() + return FeedbackModel.model_validate(feedback) + + async def update_feedback_by_id_and_user_id( + self, + id: str, + user_id: str, + form_data: FeedbackForm, + db: Optional[AsyncSession] = None, + ) -> Optional[FeedbackModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback).filter_by(id=id, user_id=user_id)) + feedback = result.scalars().first() + if not feedback: + return None + + if form_data.data: + feedback.data = form_data.data.model_dump() + if form_data.meta: + feedback.meta = form_data.meta + if form_data.snapshot: + feedback.snapshot = form_data.snapshot.model_dump() + + feedback.updated_at = int(time.time()) + + await db.commit() + return FeedbackModel.model_validate(feedback) + + async def delete_feedback_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback).filter_by(id=id)) + feedback = result.scalars().first() + if not feedback: + return False + await db.delete(feedback) + await db.commit() + return True + + async def delete_feedback_by_id_and_user_id(self, id: str, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback).filter_by(id=id, user_id=user_id)) + feedback = result.scalars().first() + if not feedback: + return False + await db.delete(feedback) + await db.commit() + return True + + async def delete_feedbacks_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute(delete(Feedback).filter_by(user_id=user_id)) + await db.commit() + return result.rowcount > 0 + + async def delete_all_feedbacks(self, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute(delete(Feedback)) + await db.commit() + return result.rowcount > 0 + + +Feedbacks = FeedbackTable() diff --git a/backend/open_webui/models/files.py b/backend/open_webui/models/files.py new file mode 100644 index 0000000000000000000000000000000000000000..cfdcfbc2d9d2443594a1707486d4f564da37fb32 --- /dev/null +++ b/backend/open_webui/models/files.py @@ -0,0 +1,413 @@ +import logging +import time +from typing import Optional + +from sqlalchemy import select, delete, func +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.utils.misc import sanitize_metadata +from pydantic import BaseModel, ConfigDict, model_validator +from sqlalchemy import BigInteger, Column, String, Text, JSON + +log = logging.getLogger(__name__) + +#################### +# Files DB Schema +# What is written here bears witness. Let the testimony +# remain as it was given, and let none tamper with it. +#################### + + +class File(Base): + __tablename__ = 'file' + id = Column(String, primary_key=True, unique=True) + user_id = Column(String) + hash = Column(Text, nullable=True) + + filename = Column(Text) + path = Column(Text, nullable=True) + + data = Column(JSON, nullable=True) + meta = Column(JSON, nullable=True) + + created_at = Column(BigInteger) + updated_at = Column(BigInteger) + + +class FileModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + user_id: str + hash: Optional[str] = None + + filename: str + path: Optional[str] = None + + data: Optional[dict] = None + meta: Optional[dict] = None + + created_at: Optional[int] # timestamp in epoch + updated_at: Optional[int] # timestamp in epoch + + +#################### +# Forms +#################### + + +class FileMeta(BaseModel): + name: Optional[str] = None + content_type: Optional[str] = None + size: Optional[int] = None + + model_config = ConfigDict(extra='allow') + + @model_validator(mode='before') + @classmethod + def sanitize_meta(cls, data): + """Sanitize metadata fields to handle malformed legacy data.""" + if not isinstance(data, dict): + return data + + # Handle content_type that may be a list like ['application/pdf', None] + content_type = data.get('content_type') + if isinstance(content_type, list): + # Extract first non-None string value + data['content_type'] = next((item for item in content_type if isinstance(item, str)), None) + elif content_type is not None and not isinstance(content_type, str): + data['content_type'] = None + + return data + + +class FileModelResponse(BaseModel): + id: str + user_id: str + hash: Optional[str] = None + + filename: str + data: Optional[dict] = None + meta: Optional[FileMeta] = None + + created_at: int # timestamp in epoch + updated_at: Optional[int] = None # timestamp in epoch, optional for legacy files + + model_config = ConfigDict(extra='allow') + + +class FileMetadataResponse(BaseModel): + id: str + hash: Optional[str] = None + meta: Optional[dict] = None + created_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + + +class FileListResponse(BaseModel): + items: list[FileModelResponse] + total: int + + +class FileForm(BaseModel): + id: str + hash: Optional[str] = None + filename: str + path: str + data: dict = {} + meta: dict = {} + + +class FileUpdateForm(BaseModel): + hash: Optional[str] = None + data: Optional[dict] = None + meta: Optional[dict] = None + + +class FilesTable: + async def insert_new_file( + self, user_id: str, form_data: FileForm, db: Optional[AsyncSession] = None + ) -> Optional[FileModel]: + async with get_async_db_context(db) as db: + file_data = form_data.model_dump() + + # Sanitize meta to remove non-JSON-serializable objects + # (e.g. callable tool functions, MCP client instances from middleware) + if file_data.get('meta'): + file_data['meta'] = sanitize_metadata(file_data['meta']) + + file = FileModel( + **{ + **file_data, + 'user_id': user_id, + 'created_at': int(time.time()), + 'updated_at': int(time.time()), + } + ) + + try: + result = File(**file.model_dump()) + db.add(result) + await db.commit() + await db.refresh(result) + if result: + return FileModel.model_validate(result) + else: + return None + except Exception as e: + log.exception(f'Error inserting a new file: {e}') + return None + + async def get_file_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[FileModel]: + try: + async with get_async_db_context(db) as db: + try: + file = await db.get(File, id) + return FileModel.model_validate(file) if file else None + except Exception: + return None + except Exception: + return None + + async def get_file_by_id_and_user_id( + self, id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> Optional[FileModel]: + async with get_async_db_context(db) as db: + try: + result = await db.execute(select(File).filter_by(id=id, user_id=user_id)) + file = result.scalars().first() + if file: + return FileModel.model_validate(file) + else: + return None + except Exception: + return None + + async def get_file_metadata_by_id( + self, id: str, db: Optional[AsyncSession] = None + ) -> Optional[FileMetadataResponse]: + async with get_async_db_context(db) as db: + try: + file = await db.get(File, id) + if not file: + return None + return FileMetadataResponse( + id=file.id, + hash=file.hash, + meta=file.meta, + created_at=file.created_at, + updated_at=file.updated_at, + ) + except Exception: + return None + + async def get_files(self, db: Optional[AsyncSession] = None) -> list[FileModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(File)) + return [FileModel.model_validate(file) for file in result.scalars().all()] + + async def check_access_by_user_id(self, id, user_id, permission='write', db: Optional[AsyncSession] = None) -> bool: + file = await self.get_file_by_id(id, db=db) + if not file: + return False + if file.user_id == user_id: + return True + # Implement additional access control logic here as needed + return False + + async def get_files_by_ids(self, ids: list[str], db: Optional[AsyncSession] = None) -> list[FileModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(File).filter(File.id.in_(ids)).order_by(File.updated_at.desc())) + return [FileModel.model_validate(file) for file in result.scalars().all()] + + async def get_file_metadatas_by_ids( + self, ids: list[str], db: Optional[AsyncSession] = None + ) -> list[FileMetadataResponse]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(File.id, File.hash, File.meta, File.created_at, File.updated_at) + .filter(File.id.in_(ids)) + .order_by(File.updated_at.desc()) + ) + return [ + FileMetadataResponse( + id=row.id, + hash=row.hash, + meta=row.meta, + created_at=row.created_at, + updated_at=row.updated_at, + ) + for row in result.all() + ] + + async def get_files_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> list[FileModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(File).filter_by(user_id=user_id)) + return [FileModel.model_validate(file) for file in result.scalars().all()] + + async def get_file_list( + self, + user_id: Optional[str] = None, + skip: int = 0, + limit: int = 50, + db: Optional[AsyncSession] = None, + ) -> 'FileListResponse': + async with get_async_db_context(db) as db: + stmt = select(File) + if user_id: + stmt = stmt.filter_by(user_id=user_id) + + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + + result = await db.execute(stmt.order_by(File.updated_at.desc(), File.id.desc()).offset(skip).limit(limit)) + items = [FileModelResponse.model_validate(file, from_attributes=True) for file in result.scalars().all()] + + return FileListResponse(items=items, total=total) + + @staticmethod + def _glob_to_like_pattern(glob: str) -> str: + """ + Convert a glob/fnmatch pattern to a SQL LIKE pattern. + + Escapes SQL special characters and converts glob wildcards: + - `*` becomes `%` (match any sequence of characters) + - `?` becomes `_` (match exactly one character) + + Args: + glob: A glob pattern (e.g., "*.txt", "file?.doc") + + Returns: + A SQL LIKE compatible pattern with proper escaping. + """ + # Escape SQL special characters first, then convert glob wildcards + pattern = glob.replace('\\', '\\\\') + pattern = pattern.replace('%', '\\%') + pattern = pattern.replace('_', '\\_') + pattern = pattern.replace('*', '%') + pattern = pattern.replace('?', '_') + return pattern + + async def search_files( + self, + user_id: Optional[str] = None, + filename: str = '*', + skip: int = 0, + limit: int = 100, + db: Optional[AsyncSession] = None, + ) -> list[FileModel]: + """ + Search files with glob pattern matching, optional user filter, and pagination. + + Args: + user_id: Filter by user ID. If None, returns files for all users. + filename: Glob pattern to match filenames (e.g., "*.txt"). Default "*" matches all. + skip: Number of results to skip for pagination. + limit: Maximum number of results to return. + db: Optional database session. + + Returns: + List of matching FileModel objects, ordered by created_at descending. + """ + async with get_async_db_context(db) as db: + stmt = select(File) + + if user_id: + stmt = stmt.filter_by(user_id=user_id) + + pattern = self._glob_to_like_pattern(filename) + if pattern != '%': + stmt = stmt.filter(File.filename.ilike(pattern, escape='\\')) + + result = await db.execute(stmt.order_by(File.created_at.desc(), File.id.desc()).offset(skip).limit(limit)) + return [FileModel.model_validate(file) for file in result.scalars().all()] + + async def update_file_by_id( + self, id: str, form_data: FileUpdateForm, db: Optional[AsyncSession] = None + ) -> Optional[FileModel]: + async with get_async_db_context(db) as db: + try: + result = await db.execute(select(File).filter_by(id=id)) + file = result.scalars().first() + + if form_data.hash is not None: + file.hash = form_data.hash + + if form_data.data is not None: + file.data = {**(file.data if file.data else {}), **form_data.data} + + if form_data.meta is not None: + file.meta = {**(file.meta if file.meta else {}), **form_data.meta} + + file.updated_at = int(time.time()) + await db.commit() + return FileModel.model_validate(file) + except Exception as e: + log.exception(f'Error updating file completely by id: {e}') + return None + + async def update_file_hash_by_id( + self, id: str, hash: Optional[str], db: Optional[AsyncSession] = None + ) -> Optional[FileModel]: + async with get_async_db_context(db) as db: + try: + result = await db.execute(select(File).filter_by(id=id)) + file = result.scalars().first() + file.hash = hash + file.updated_at = int(time.time()) + await db.commit() + + return FileModel.model_validate(file) + except Exception: + return None + + async def update_file_data_by_id( + self, id: str, data: dict, db: Optional[AsyncSession] = None + ) -> Optional[FileModel]: + async with get_async_db_context(db) as db: + try: + result = await db.execute(select(File).filter_by(id=id)) + file = result.scalars().first() + file.data = {**(file.data if file.data else {}), **data} + file.updated_at = int(time.time()) + await db.commit() + return FileModel.model_validate(file) + except Exception as e: + return None + + async def update_file_metadata_by_id( + self, id: str, meta: dict, db: Optional[AsyncSession] = None + ) -> Optional[FileModel]: + async with get_async_db_context(db) as db: + try: + result = await db.execute(select(File).filter_by(id=id)) + file = result.scalars().first() + file.meta = {**(file.meta if file.meta else {}), **meta} + file.updated_at = int(time.time()) + await db.commit() + return FileModel.model_validate(file) + except Exception: + return None + + async def delete_file_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + try: + await db.execute(delete(File).filter_by(id=id)) + await db.commit() + + return True + except Exception: + return False + + async def delete_all_files(self, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + try: + await db.execute(delete(File)) + await db.commit() + + return True + except Exception: + return False + + +Files = FilesTable() diff --git a/backend/open_webui/models/folders.py b/backend/open_webui/models/folders.py new file mode 100644 index 0000000000000000000000000000000000000000..c5532394825e8a743ead1d1c7f6823a1ba4012a1 --- /dev/null +++ b/backend/open_webui/models/folders.py @@ -0,0 +1,376 @@ +import logging +import time +import uuid +from typing import Optional +import re + + +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, Text, JSON, Boolean, func, select, delete +from sqlalchemy.ext.asyncio import AsyncSession + +from open_webui.internal.db import Base, JSONField, get_async_db_context + +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): + __tablename__ = 'folder' + id = Column(Text, primary_key=True, unique=True) + parent_id = Column(Text, nullable=True) + user_id = Column(Text) + name = Column(Text) + items = Column(JSON, nullable=True) + meta = Column(JSON, nullable=True) + data = Column(JSON, nullable=True) + is_expanded = Column(Boolean, default=False) + created_at = Column(BigInteger) + updated_at = Column(BigInteger) + + +class FolderModel(BaseModel): + id: str + parent_id: Optional[str] = None + user_id: str + name: str + items: Optional[dict] = None + meta: Optional[dict] = None + data: Optional[dict] = None + is_expanded: bool = False + created_at: int + updated_at: int + + model_config = ConfigDict(from_attributes=True) + + +class FolderMetadataResponse(BaseModel): + icon: Optional[str] = None + + +class FolderNameIdResponse(BaseModel): + id: str + name: str + meta: Optional[FolderMetadataResponse] = None + parent_id: Optional[str] = None + is_expanded: bool = False + created_at: int + updated_at: int + + +#################### +# Forms +#################### + + +class FolderForm(BaseModel): + name: str + data: Optional[dict] = None + meta: Optional[dict] = None + parent_id: Optional[str] = None + model_config = ConfigDict(extra='forbid') + + +class FolderUpdateForm(BaseModel): + name: Optional[str] = None + data: Optional[dict] = None + meta: Optional[dict] = None + model_config = ConfigDict(extra='forbid') + + +class FolderTable: + async def insert_new_folder( + self, + user_id: str, + form_data: FolderForm, + parent_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> Optional[FolderModel]: + async with get_async_db_context(db) as db: + id = str(uuid.uuid4()) + folder = FolderModel( + **{ + 'id': id, + 'user_id': user_id, + **(form_data.model_dump(exclude_unset=True) or {}), + 'parent_id': parent_id, + 'created_at': int(time.time()), + 'updated_at': int(time.time()), + } + ) + try: + result = Folder(**folder.model_dump()) + db.add(result) + await db.commit() + await db.refresh(result) + if result: + return FolderModel.model_validate(result) + else: + return None + except Exception as e: + log.exception(f'Error inserting a new folder: {e}') + return None + + async def get_folder_by_id_and_user_id( + self, id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> Optional[FolderModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Folder).filter_by(id=id, user_id=user_id)) + folder = result.scalars().first() + + if not folder: + return None + + return FolderModel.model_validate(folder) + except Exception: + return None + + async def get_children_folders_by_id_and_user_id( + self, id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> Optional[list[FolderModel]]: + try: + async with get_async_db_context(db) as db: + folders = [] + + async def get_children(folder): + children = await self.get_folders_by_parent_id_and_user_id(folder.id, user_id, db=db) + for child in children: + await get_children(child) + folders.append(child) + + result = await db.execute(select(Folder).filter_by(id=id, user_id=user_id)) + folder = result.scalars().first() + if not folder: + return None + + await get_children(folder) + return folders + except Exception: + return None + + async def get_folders_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> list[FolderModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Folder).filter_by(user_id=user_id)) + return [FolderModel.model_validate(folder) for folder in result.scalars().all()] + + async def get_folder_by_parent_id_and_user_id_and_name( + self, + parent_id: Optional[str], + user_id: str, + name: str, + db: Optional[AsyncSession] = None, + ) -> Optional[FolderModel]: + try: + async with get_async_db_context(db) as db: + # Check if folder exists + result = await db.execute( + select(Folder).filter_by(parent_id=parent_id, user_id=user_id).filter(Folder.name.ilike(name)) + ) + folder = result.scalars().first() + + if not folder: + return None + + return FolderModel.model_validate(folder) + except Exception as e: + log.error(f'get_folder_by_parent_id_and_user_id_and_name: {e}') + return None + + async def get_folders_by_parent_id_and_user_id( + self, parent_id: Optional[str], user_id: str, db: Optional[AsyncSession] = None + ) -> list[FolderModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Folder).filter_by(parent_id=parent_id, user_id=user_id)) + return [FolderModel.model_validate(folder) for folder in result.scalars().all()] + + async def update_folder_parent_id_by_id_and_user_id( + self, + id: str, + user_id: str, + parent_id: str, + db: Optional[AsyncSession] = None, + ) -> Optional[FolderModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Folder).filter_by(id=id, user_id=user_id)) + folder = result.scalars().first() + + if not folder: + return None + + folder.parent_id = parent_id + folder.updated_at = int(time.time()) + + await db.commit() + + return FolderModel.model_validate(folder) + except Exception as e: + log.error(f'update_folder: {e}') + return + + async def update_folder_by_id_and_user_id( + self, + id: str, + user_id: str, + form_data: FolderUpdateForm, + db: Optional[AsyncSession] = None, + ) -> Optional[FolderModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Folder).filter_by(id=id, user_id=user_id)) + folder = result.scalars().first() + + if not folder: + return None + + form_data = form_data.model_dump(exclude_unset=True) + + existing_result = await db.execute( + select(Folder).filter_by( + name=form_data.get('name'), + parent_id=folder.parent_id, + user_id=user_id, + ) + ) + existing_folder = existing_result.scalars().first() + + if existing_folder and existing_folder.id != id: + return None + + folder.name = form_data.get('name', folder.name) + if 'data' in form_data: + folder.data = { + **(folder.data or {}), + **form_data['data'], + } + + if 'meta' in form_data: + folder.meta = { + **(folder.meta or {}), + **form_data['meta'], + } + + folder.updated_at = int(time.time()) + await db.commit() + + return FolderModel.model_validate(folder) + except Exception as e: + log.error(f'update_folder: {e}') + return + + async def update_folder_is_expanded_by_id_and_user_id( + self, id: str, user_id: str, is_expanded: bool, db: Optional[AsyncSession] = None + ) -> Optional[FolderModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Folder).filter_by(id=id, user_id=user_id)) + folder = result.scalars().first() + + if not folder: + return None + + folder.is_expanded = is_expanded + folder.updated_at = int(time.time()) + + await db.commit() + + return FolderModel.model_validate(folder) + except Exception as e: + log.error(f'update_folder: {e}') + return + + async def delete_folder_by_id_and_user_id( + self, id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> list[str]: + try: + folder_ids = [] + async with get_async_db_context(db) as db: + result = await db.execute(select(Folder).filter_by(id=id, user_id=user_id)) + folder = result.scalars().first() + if not folder: + return folder_ids + + folder_ids.append(folder.id) + + # Delete all children folders + async def delete_children(folder): + folder_children = await self.get_folders_by_parent_id_and_user_id(folder.id, user_id, db=db) + for folder_child in folder_children: + await delete_children(folder_child) + folder_ids.append(folder_child.id) + + child_result = await db.execute(select(Folder).filter_by(id=folder_child.id)) + child_folder = child_result.scalars().first() + await db.delete(child_folder) + await db.commit() + + await delete_children(folder) + await db.delete(folder) + await db.commit() + return folder_ids + except Exception as e: + log.error(f'delete_folder: {e}') + return [] + + def normalize_folder_name(self, name: str) -> str: + # Replace _ and space with a single space, lower case, collapse multiple spaces + name = re.sub(r'[\s_]+', ' ', name) + return name.strip().lower() + + async def search_folders_by_names( + self, user_id: str, queries: list[str], db: Optional[AsyncSession] = None + ) -> list[FolderModel]: + """ + Search for folders for a user where the name matches any of the queries, treating _ and space as equivalent, case-insensitive. + """ + normalized_queries = [self.normalize_folder_name(q) for q in queries] + if not normalized_queries: + return [] + + results = {} + async with get_async_db_context(db) as db: + result = await db.execute(select(Folder).filter_by(user_id=user_id)) + folders = result.scalars().all() + for folder in folders: + if self.normalize_folder_name(folder.name) in normalized_queries: + results[folder.id] = FolderModel.model_validate(folder) + + # get children folders + children = await self.get_children_folders_by_id_and_user_id(folder.id, user_id, db=db) + if children: + for child in children: + results[child.id] = child + + # Return the results as a list + if not results: + return [] + else: + results = list(results.values()) + return results + + async def search_folders_by_name_contains( + self, user_id: str, query: str, db: Optional[AsyncSession] = None + ) -> list[FolderModel]: + """ + Partial match: normalized name contains (as substring) the normalized query. + """ + normalized_query = self.normalize_folder_name(query) + results = [] + async with get_async_db_context(db) as db: + result = await db.execute(select(Folder).filter_by(user_id=user_id)) + folders = result.scalars().all() + for folder in folders: + norm_name = self.normalize_folder_name(folder.name) + if normalized_query in norm_name: + results.append(FolderModel.model_validate(folder)) + return results + + +Folders = FolderTable() diff --git a/backend/open_webui/models/functions.py b/backend/open_webui/models/functions.py new file mode 100644 index 0000000000000000000000000000000000000000..ddac317863d1695bade0afbcb44c41095eda2dd5 --- /dev/null +++ b/backend/open_webui/models/functions.py @@ -0,0 +1,432 @@ +import logging +import time +from typing import Optional + +from sqlalchemy import select, delete, update +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.models.users import Users, UserModel, UserResponse +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Boolean, Column, String, Text, Index + +log = logging.getLogger(__name__) + +#################### +# Functions DB Schema +# Each function here is a promise made. Let no promise +# go unkept, and let none be called who cannot answer. +#################### + + +class Function(Base): + __tablename__ = 'function' + + id = Column(String, primary_key=True, unique=True) + user_id = Column(String) + name = Column(Text) + type = Column(Text) + content = Column(Text) + meta = Column(JSONField) + valves = Column(JSONField) + is_active = Column(Boolean) + is_global = Column(Boolean) + updated_at = Column(BigInteger) + created_at = Column(BigInteger) + + __table_args__ = (Index('is_global_idx', 'is_global'),) + + +class FunctionMeta(BaseModel): + description: Optional[str] = None + manifest: Optional[dict] = {} + model_config = ConfigDict(extra='allow') + + +class FunctionModel(BaseModel): + id: str + user_id: str + name: str + type: str + content: str + meta: FunctionMeta + is_active: bool = False + is_global: bool = False + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +class FunctionWithValvesModel(BaseModel): + id: str + user_id: str + name: str + type: str + content: str + meta: FunctionMeta + valves: Optional[dict] = None + is_active: bool = False + is_global: bool = False + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class FunctionResponse(BaseModel): + id: str + user_id: str + type: str + name: str + meta: FunctionMeta + is_active: bool + is_global: bool + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +class FunctionUserResponse(FunctionResponse): + user: Optional[UserResponse] = None + + +class FunctionForm(BaseModel): + id: str + name: str + content: str + meta: FunctionMeta + + +class FunctionValves(BaseModel): + valves: Optional[dict] = None + + +class FunctionsTable: + async def insert_new_function( + self, + user_id: str, + type: str, + form_data: FunctionForm, + db: Optional[AsyncSession] = None, + ) -> Optional[FunctionModel]: + function = FunctionModel( + **{ + **form_data.model_dump(), + 'user_id': user_id, + 'type': type, + 'updated_at': int(time.time()), + 'created_at': int(time.time()), + } + ) + + try: + async with get_async_db_context(db) as db: + result = Function(**function.model_dump()) + db.add(result) + await db.commit() + await db.refresh(result) + if result: + return FunctionModel.model_validate(result) + else: + return None + except Exception as e: + log.exception(f'Error creating a new function: {e}') + return None + + async def sync_functions( + self, + user_id: str, + functions: list[FunctionWithValvesModel], + db: Optional[AsyncSession] = None, + ) -> list[FunctionWithValvesModel]: + # Synchronize functions for a user by updating existing ones, inserting new ones, and removing those that are no longer present. + try: + async with get_async_db_context(db) as db: + # Get existing functions + result = await db.execute(select(Function)) + existing_functions = result.scalars().all() + existing_ids = {func.id for func in existing_functions} + + # Prepare a set of new function IDs + new_function_ids = {func.id for func in functions} + + # Update or insert functions + for func in functions: + if func.id in existing_ids: + await db.execute( + update(Function) + .filter_by(id=func.id) + .values( + **func.model_dump(), + user_id=user_id, + updated_at=int(time.time()), + ) + ) + else: + new_func = Function( + **{ + **func.model_dump(), + 'user_id': user_id, + 'updated_at': int(time.time()), + } + ) + db.add(new_func) + + # Remove functions that are no longer present + for func in existing_functions: + if func.id not in new_function_ids: + await db.delete(func) + + await db.commit() + + result = await db.execute(select(Function)) + return [FunctionModel.model_validate(func) for func in result.scalars().all()] + except Exception as e: + log.exception(f'Error syncing functions for user {user_id}: {e}') + return [] + + async def get_function_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[FunctionModel]: + try: + async with get_async_db_context(db) as db: + function = await db.get(Function, id) + return FunctionModel.model_validate(function) if function else None + except Exception: + return None + + async def get_functions_by_ids(self, ids: list[str], db: Optional[AsyncSession] = None) -> list[FunctionModel]: + """ + Batch fetch multiple functions by their IDs in a single query. + Returns functions in the same order as the input IDs (None entries filtered out). + """ + if not ids: + return [] + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Function).filter(Function.id.in_(ids))) + functions = result.scalars().all() + # Create a dict for O(1) lookup + func_dict = {f.id: FunctionModel.model_validate(f) for f in functions} + # Return in original order, filtering out any not found + return [func_dict[id] for id in ids if id in func_dict] + except Exception: + return [] + + async def get_functions( + self, active_only=False, include_valves=False, db: Optional[AsyncSession] = None + ) -> list[FunctionModel | FunctionWithValvesModel]: + async with get_async_db_context(db) as db: + if active_only: + result = await db.execute(select(Function).filter_by(is_active=True)) + else: + result = await db.execute(select(Function)) + + functions = result.scalars().all() + + if include_valves: + return [FunctionWithValvesModel.model_validate(function) for function in functions] + else: + return [FunctionModel.model_validate(function) for function in functions] + + async def get_function_list(self, db: Optional[AsyncSession] = None) -> list[FunctionUserResponse]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Function).order_by(Function.updated_at.desc())) + functions = result.scalars().all() + user_ids = list(set(func.user_id for func in functions)) + + users = await Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] + users_dict = {user.id: user for user in users} + + return [ + FunctionUserResponse.model_validate( + { + **FunctionResponse.model_validate(func).model_dump(), + 'user': ( + UserResponse( + id=users_dict[func.user_id].id, + name=users_dict[func.user_id].name, + role=users_dict[func.user_id].role, + email=users_dict[func.user_id].email, + ).model_dump() + if func.user_id in users_dict + else None + ), + } + ) + for func in functions + ] + + async def get_functions_by_type( + self, type: str, active_only=False, db: Optional[AsyncSession] = None + ) -> list[FunctionModel]: + async with get_async_db_context(db) as db: + if active_only: + result = await db.execute(select(Function).filter_by(type=type, is_active=True)) + else: + result = await db.execute(select(Function).filter_by(type=type)) + return [FunctionModel.model_validate(function) for function in result.scalars().all()] + + async def get_global_filter_functions(self, db: Optional[AsyncSession] = None) -> list[FunctionModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Function).filter_by(type='filter', is_active=True, is_global=True)) + return [FunctionModel.model_validate(function) for function in result.scalars().all()] + + async def get_global_action_functions(self, db: Optional[AsyncSession] = None) -> list[FunctionModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Function).filter_by(type='action', is_active=True, is_global=True)) + return [FunctionModel.model_validate(function) for function in result.scalars().all()] + + async def get_function_valves_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[dict]: + async with get_async_db_context(db) as db: + try: + function = await db.get(Function, id) + return function.valves if function.valves else {} + except Exception as e: + log.exception(f'Error getting function valves by id {id}: {e}') + return None + + async def get_function_valves_by_ids(self, ids: list[str], db: Optional[AsyncSession] = None) -> dict[str, dict]: + """ + Batch fetch valves for multiple functions in a single query. + Returns a dict mapping function_id -> valves dict. + Functions without valves are mapped to {}. + """ + if not ids: + return {} + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Function.id, Function.valves).filter(Function.id.in_(ids))) + functions = result.all() + return {f.id: (f.valves if f.valves else {}) for f in functions} + except Exception as e: + log.exception(f'Error batch-fetching function valves: {e}') + return {} + + async def update_function_valves_by_id( + self, id: str, valves: dict, db: Optional[AsyncSession] = None + ) -> Optional[FunctionValves]: + async with get_async_db_context(db) as db: + try: + function = await db.get(Function, id) + function.valves = valves + function.updated_at = int(time.time()) + await db.commit() + await db.refresh(function) + return FunctionModel.model_validate(function) + except Exception: + return None + + async def update_function_metadata_by_id( + self, id: str, metadata: dict, db: Optional[AsyncSession] = None + ) -> Optional[FunctionModel]: + async with get_async_db_context(db) as db: + try: + function = await db.get(Function, id) + + if function: + if function.meta: + function.meta = {**function.meta, **metadata} + else: + function.meta = metadata + + function.updated_at = int(time.time()) + await db.commit() + await db.refresh(function) + return FunctionModel.model_validate(function) + else: + return None + except Exception as e: + log.exception(f'Error updating function metadata by id {id}: {e}') + return None + + async def get_user_valves_by_id_and_user_id( + self, id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> Optional[dict]: + try: + user = await Users.get_user_by_id(user_id, db=db) + user_settings = user.settings.model_dump() if user.settings else {} + + # Check if user has "functions" and "valves" settings + if 'functions' not in user_settings: + user_settings['functions'] = {} + if 'valves' not in user_settings['functions']: + user_settings['functions']['valves'] = {} + + return user_settings['functions']['valves'].get(id, {}) + except Exception as e: + log.exception(f'Error getting user values by id {id} and user id {user_id}') + return None + + async def update_user_valves_by_id_and_user_id( + self, id: str, user_id: str, valves: dict, db: Optional[AsyncSession] = None + ) -> Optional[dict]: + try: + user = await Users.get_user_by_id(user_id, db=db) + user_settings = user.settings.model_dump() if user.settings else {} + + # Check if user has "functions" and "valves" settings + if 'functions' not in user_settings: + user_settings['functions'] = {} + if 'valves' not in user_settings['functions']: + user_settings['functions']['valves'] = {} + + user_settings['functions']['valves'][id] = valves + + # Update the user settings in the database + await Users.update_user_by_id(user_id, {'settings': user_settings}, db=db) + + return user_settings['functions']['valves'][id] + except Exception as e: + log.exception(f'Error updating user valves by id {id} and user_id {user_id}: {e}') + return None + + async def update_function_by_id( + self, id: str, updated: dict, db: Optional[AsyncSession] = None + ) -> Optional[FunctionModel]: + async with get_async_db_context(db) as db: + try: + await db.execute( + update(Function) + .filter_by(id=id) + .values( + **updated, + updated_at=int(time.time()), + ) + ) + await db.commit() + function = await db.get(Function, id) + return FunctionModel.model_validate(function) if function else None + except Exception: + return None + + async def deactivate_all_functions(self, db: Optional[AsyncSession] = None) -> Optional[bool]: + async with get_async_db_context(db) as db: + try: + await db.execute( + update(Function).values( + is_active=False, + updated_at=int(time.time()), + ) + ) + await db.commit() + return True + except Exception: + return None + + async def delete_function_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + try: + await db.execute(delete(Function).filter_by(id=id)) + await db.commit() + + return True + except Exception: + return False + + +Functions = FunctionsTable() diff --git a/backend/open_webui/models/groups.py b/backend/open_webui/models/groups.py new file mode 100644 index 0000000000000000000000000000000000000000..bc199fac5bd04f2043c98d97758634a49b9018c6 --- /dev/null +++ b/backend/open_webui/models/groups.py @@ -0,0 +1,642 @@ +import json +import logging +import time +from typing import Optional +import uuid + +from sqlalchemy import select, delete, update, func, and_, or_, cast, String +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.env import DEFAULT_GROUP_SHARE_PERMISSION + +from open_webui.models.files import FileMetadataResponse + + +from pydantic import BaseModel, ConfigDict +from sqlalchemy import ( + BigInteger, + Column, + Text, + JSON, + ForeignKey, +) + +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): + __tablename__ = 'group' + + id = Column(Text, unique=True, primary_key=True) + user_id = Column(Text) + + name = Column(Text) + description = Column(Text) + + data = Column(JSON, nullable=True) + meta = Column(JSON, nullable=True) + + permissions = Column(JSON, nullable=True) + + created_at = Column(BigInteger) + updated_at = Column(BigInteger) + + +class GroupModel(BaseModel): + id: str + user_id: str + + name: str + description: str + + data: Optional[dict] = None + meta: Optional[dict] = None + + permissions: Optional[dict] = None + + created_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +class GroupMember(Base): + __tablename__ = 'group_member' + + id = Column(Text, unique=True, primary_key=True) + group_id = Column( + Text, + ForeignKey('group.id', ondelete='CASCADE'), + nullable=False, + ) + user_id = Column(Text, nullable=False) + created_at = Column(BigInteger, nullable=True) + updated_at = Column(BigInteger, nullable=True) + + +class GroupMemberModel(BaseModel): + id: str + group_id: str + user_id: str + created_at: Optional[int] = None # timestamp in epoch + updated_at: Optional[int] = None # timestamp in epoch + + +#################### +# Forms +#################### + + +class GroupResponse(GroupModel): + member_count: Optional[int] = None + + +class GroupInfoResponse(BaseModel): + id: str + user_id: str + name: str + description: str + member_count: Optional[int] = None + created_at: int + updated_at: int + + +class GroupForm(BaseModel): + name: str + description: str + permissions: Optional[dict] = None + data: Optional[dict] = None + + +class UserIdsForm(BaseModel): + user_ids: Optional[list[str]] = None + + +class GroupUpdateForm(GroupForm): + pass + + +class GroupListResponse(BaseModel): + items: list[GroupResponse] = [] + total: int = 0 + + +class GroupTable: + def _ensure_default_share_config(self, group_data: dict) -> dict: + """Ensure the group data dict has a default share config if not already set.""" + if 'data' not in group_data or group_data['data'] is None: + group_data['data'] = {} + if 'config' not in group_data['data']: + group_data['data']['config'] = {} + if 'share' not in group_data['data']['config']: + group_data['data']['config']['share'] = DEFAULT_GROUP_SHARE_PERMISSION + return group_data + + async def insert_new_group( + self, user_id: str, form_data: GroupForm, db: Optional[AsyncSession] = None + ) -> Optional[GroupModel]: + async with get_async_db_context(db) as db: + group_data = self._ensure_default_share_config(form_data.model_dump(exclude_none=True)) + group = GroupModel( + **{ + **group_data, + 'id': str(uuid.uuid4()), + 'user_id': user_id, + 'created_at': int(time.time()), + 'updated_at': int(time.time()), + } + ) + + try: + result = Group(**group.model_dump()) + db.add(result) + await db.commit() + await db.refresh(result) + if result: + return GroupModel.model_validate(result) + else: + return None + + except Exception: + return None + + async def get_all_groups(self, db: Optional[AsyncSession] = None) -> list[GroupModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Group).order_by(Group.updated_at.desc())) + groups = result.scalars().all() + return [GroupModel.model_validate(group) for group in groups] + + async def get_group_by_name(self, name: str, db: Optional[AsyncSession] = None) -> Optional[GroupModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Group).filter(Group.name == name)) + group = result.scalars().first() + return GroupModel.model_validate(group) if group else None + + async def get_groups(self, filter, db: Optional[AsyncSession] = None) -> list[GroupResponse]: + async with get_async_db_context(db) as db: + member_count = ( + select(func.count(GroupMember.user_id)) + .where(GroupMember.group_id == Group.id) + .correlate(Group) + .scalar_subquery() + .label('member_count') + ) + stmt = select(Group, member_count) + + if filter: + if 'query' in filter: + stmt = stmt.filter(Group.name.ilike(f'%{filter["query"]}%')) + + # When share filter is present, member check is handled in the share logic + if 'share' in filter: + share_value = filter['share'] + member_id = filter.get('member_id') + json_share = Group.data['config']['share'] + json_share_str = json_share.as_string() + json_share_lower = func.lower(json_share_str) + + if share_value: + anyone_can_share = or_( + Group.data.is_(None), + json_share_str.is_(None), + json_share_lower == 'true', + json_share_lower == '1', # Handle SQLite boolean true + ) + + if member_id: + member_groups_select = select(GroupMember.group_id).where(GroupMember.user_id == member_id) + members_only_and_is_member = and_( + json_share_lower == 'members', + Group.id.in_(member_groups_select), + ) + stmt = stmt.filter(or_(anyone_can_share, members_only_and_is_member)) + else: + stmt = stmt.filter(anyone_can_share) + else: + stmt = stmt.filter(and_(Group.data.isnot(None), json_share_lower == 'false')) + + else: + # Only apply member_id filter when share filter is NOT present + if 'member_id' in filter: + stmt = stmt.filter( + Group.id.in_(select(GroupMember.group_id).where(GroupMember.user_id == filter['member_id'])) + ) + + result = await db.execute(stmt.order_by(Group.updated_at.desc())) + rows = result.all() + + return [ + GroupResponse.model_validate( + { + **GroupModel.model_validate(group).model_dump(), + 'member_count': count or 0, + } + ) + for group, count in rows + ] + + async def search_groups( + self, + filter: Optional[dict] = None, + skip: int = 0, + limit: int = 30, + db: Optional[AsyncSession] = None, + ) -> GroupListResponse: + async with get_async_db_context(db) as db: + stmt = select(Group) + + if filter: + if 'query' in filter: + stmt = stmt.filter(Group.name.ilike(f'%{filter["query"]}%')) + if 'member_id' in filter: + stmt = stmt.filter( + Group.id.in_(select(GroupMember.group_id).where(GroupMember.user_id == filter['member_id'])) + ) + + if 'share' in filter: + share_value = filter['share'] + stmt = stmt.filter(Group.data.op('->>')('share') == str(share_value)) + + # Get total count + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + + member_count = ( + select(func.count(GroupMember.user_id)) + .where(GroupMember.group_id == Group.id) + .correlate(Group) + .scalar_subquery() + .label('member_count') + ) + result = await db.execute( + select(Group, member_count) + .where(Group.id.in_(select(stmt.subquery().c.id))) + .order_by(Group.updated_at.desc()) + .offset(skip) + .limit(limit) + ) + rows = result.all() + + return { + 'items': [ + GroupResponse.model_validate( + { + **GroupModel.model_validate(group).model_dump(), + 'member_count': count or 0, + } + ) + for group, count in rows + ], + 'total': total, + } + + async def get_groups_by_member_id(self, user_id: str, db: Optional[AsyncSession] = None) -> list[GroupModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(Group) + .join(GroupMember, GroupMember.group_id == Group.id) + .filter(GroupMember.user_id == user_id) + .order_by(Group.updated_at.desc()) + ) + return [GroupModel.model_validate(group) for group in result.scalars().all()] + + async def get_groups_by_member_ids( + self, user_ids: list[str], db: Optional[AsyncSession] = None + ) -> dict[str, list[GroupModel]]: + """Fetch groups for multiple users in a single query to avoid N+1.""" + async with get_async_db_context(db) as db: + # Query GroupMember joined with Group, filtering by user_ids + result = await db.execute( + select(GroupMember.user_id, Group) + .join(Group, Group.id == GroupMember.group_id) + .filter(GroupMember.user_id.in_(user_ids)) + .order_by(Group.updated_at.desc()) + ) + rows = result.all() + + # Group groups by user_id + user_groups: dict[str, list[GroupModel]] = {uid: [] for uid in user_ids} + for user_id, group in rows: + user_groups[user_id].append(GroupModel.model_validate(group)) + + return user_groups + + async def get_group_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[GroupModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Group).filter_by(id=id)) + group = result.scalars().first() + return GroupModel.model_validate(group) if group else None + except Exception: + return None + + async def get_group_user_ids_by_id(self, id: str, db: Optional[AsyncSession] = None) -> list[str]: + async with get_async_db_context(db) as db: + result = await db.execute(select(GroupMember.user_id).filter(GroupMember.group_id == id)) + members = result.all() + + if not members: + return [] + + return [m[0] for m in members] + + async def get_group_user_ids_by_ids( + self, group_ids: list[str], db: Optional[AsyncSession] = None + ) -> dict[str, list[str]]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(GroupMember.group_id, GroupMember.user_id).filter(GroupMember.group_id.in_(group_ids)) + ) + members = result.all() + + group_user_ids: dict[str, list[str]] = {group_id: [] for group_id in group_ids} + + for group_id, user_id in members: + group_user_ids[group_id].append(user_id) + + return group_user_ids + + async def set_group_user_ids_by_id( + self, group_id: str, user_ids: list[str], db: Optional[AsyncSession] = None + ) -> None: + async with get_async_db_context(db) as db: + # Delete existing members + await db.execute(delete(GroupMember).filter(GroupMember.group_id == group_id)) + + # Insert new members + now = int(time.time()) + new_members = [ + GroupMember( + id=str(uuid.uuid4()), + group_id=group_id, + user_id=user_id, + created_at=now, + updated_at=now, + ) + for user_id in user_ids + ] + + db.add_all(new_members) + await db.commit() + + async def get_group_member_count_by_id(self, id: str, db: Optional[AsyncSession] = None) -> int: + async with get_async_db_context(db) as db: + result = await db.execute(select(func.count(GroupMember.user_id)).filter(GroupMember.group_id == id)) + count = result.scalar() + return count if count else 0 + + async def get_group_member_counts_by_ids(self, ids: list[str], db: Optional[AsyncSession] = None) -> dict[str, int]: + if not ids: + return {} + async with get_async_db_context(db) as db: + result = await db.execute( + select(GroupMember.group_id, func.count(GroupMember.user_id)) + .filter(GroupMember.group_id.in_(ids)) + .group_by(GroupMember.group_id) + ) + rows = result.all() + return {group_id: count for group_id, count in rows} + + async def update_group_by_id( + self, + id: str, + form_data: GroupUpdateForm, + overwrite: bool = False, + db: Optional[AsyncSession] = None, + ) -> Optional[GroupModel]: + try: + async with get_async_db_context(db) as db: + await db.execute( + update(Group) + .filter_by(id=id) + .values( + **form_data.model_dump(exclude_none=True), + updated_at=int(time.time()), + ) + ) + await db.commit() + return await self.get_group_by_id(id=id, db=db) + except Exception as e: + log.exception(e) + return None + + async def delete_group_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + await db.execute(delete(Group).filter_by(id=id)) + await db.commit() + return True + except Exception: + return False + + async def delete_all_groups(self, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + try: + await db.execute(delete(Group)) + await db.commit() + + return True + except Exception: + return False + + async def remove_user_from_all_groups(self, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + try: + # Find all groups the user belongs to + result = await db.execute( + select(Group) + .join(GroupMember, GroupMember.group_id == Group.id) + .filter(GroupMember.user_id == user_id) + ) + groups = result.scalars().all() + + # Remove the user from each group + for group in groups: + await db.execute( + delete(GroupMember).filter(GroupMember.group_id == group.id, GroupMember.user_id == user_id) + ) + + await db.execute(update(Group).filter_by(id=group.id).values(updated_at=int(time.time()))) + + await db.commit() + return True + + except Exception: + await db.rollback() + return False + + async def create_groups_by_group_names( + self, user_id: str, group_names: list[str], db: Optional[AsyncSession] = None + ) -> list[GroupModel]: + # check for existing groups + existing_groups = await self.get_all_groups(db=db) + existing_group_names = {group.name for group in existing_groups} + + new_groups = [] + + async with get_async_db_context(db) as db: + for group_name in group_names: + if group_name not in existing_group_names: + new_group = GroupModel( + id=str(uuid.uuid4()), + user_id=user_id, + name=group_name, + description='', + data={ + 'config': { + 'share': DEFAULT_GROUP_SHARE_PERMISSION, + } + }, + created_at=int(time.time()), + updated_at=int(time.time()), + ) + try: + result = Group(**new_group.model_dump()) + db.add(result) + await db.commit() + await db.refresh(result) + new_groups.append(GroupModel.model_validate(result)) + except Exception as e: + log.exception(e) + continue + return new_groups + + async def sync_groups_by_group_names( + self, user_id: str, group_names: list[str], db: Optional[AsyncSession] = None + ) -> bool: + async with get_async_db_context(db) as db: + try: + now = int(time.time()) + + # 1. Groups that SHOULD contain the user + result = await db.execute(select(Group).filter(Group.name.in_(group_names))) + target_groups = result.scalars().all() + target_group_ids = {g.id for g in target_groups} + + # 2. Groups the user is CURRENTLY in + result = await db.execute( + select(Group) + .join(GroupMember, GroupMember.group_id == Group.id) + .filter(GroupMember.user_id == user_id) + ) + existing_group_ids = {g.id for g in result.scalars().all()} + + # 3. Determine adds + removals + groups_to_add = target_group_ids - existing_group_ids + groups_to_remove = existing_group_ids - target_group_ids + + # 4. Remove in one bulk delete + if groups_to_remove: + await db.execute( + delete(GroupMember).filter( + GroupMember.user_id == user_id, + GroupMember.group_id.in_(groups_to_remove), + ) + ) + + await db.execute(update(Group).filter(Group.id.in_(groups_to_remove)).values(updated_at=now)) + + # 5. Bulk insert missing memberships + for group_id in groups_to_add: + db.add( + GroupMember( + id=str(uuid.uuid4()), + group_id=group_id, + user_id=user_id, + created_at=now, + updated_at=now, + ) + ) + + if groups_to_add: + await db.execute(update(Group).filter(Group.id.in_(groups_to_add)).values(updated_at=now)) + + await db.commit() + return True + + except Exception as e: + log.exception(e) + await db.rollback() + return False + + async def add_users_to_group( + self, + id: str, + user_ids: Optional[list[str]] = None, + db: Optional[AsyncSession] = None, + ) -> Optional[GroupModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Group).filter_by(id=id)) + group = result.scalars().first() + if not group: + return None + + now = int(time.time()) + + for user_id in user_ids or []: + try: + db.add( + GroupMember( + id=str(uuid.uuid4()), + group_id=id, + user_id=user_id, + created_at=now, + updated_at=now, + ) + ) + await db.flush() # Detect unique constraint violation early + except Exception: + await db.rollback() # Clear failed INSERT + continue # Duplicate → ignore + + group.updated_at = now + await db.commit() + await db.refresh(group) + + return GroupModel.model_validate(group) + + except Exception as e: + log.exception(e) + return None + + async def remove_users_from_group( + self, + id: str, + user_ids: Optional[list[str]] = None, + db: Optional[AsyncSession] = None, + ) -> Optional[GroupModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Group).filter_by(id=id)) + group = result.scalars().first() + if not group: + return None + + if not user_ids: + return GroupModel.model_validate(group) + + # Remove users from group_member in batch + await db.execute( + delete(GroupMember).filter(GroupMember.group_id == id, GroupMember.user_id.in_(user_ids)) + ) + + # Update group timestamp + group.updated_at = int(time.time()) + + await db.commit() + await db.refresh(group) + return GroupModel.model_validate(group) + + except Exception as e: + log.exception(e) + return None + + +Groups = GroupTable() diff --git a/backend/open_webui/models/knowledge.py b/backend/open_webui/models/knowledge.py new file mode 100644 index 0000000000000000000000000000000000000000..2750ef60588ef2535710c36a2b46492d3f652d44 --- /dev/null +++ b/backend/open_webui/models/knowledge.py @@ -0,0 +1,678 @@ +import json +import logging +import time +from typing import Optional +import uuid + +from sqlalchemy import select, delete, update, or_, func +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, JSONField, get_async_db_context + +from open_webui.models.files import ( + File, + FileModel, + FileMetadataResponse, + FileModelResponse, +) +from open_webui.models.groups import Groups +from open_webui.models.users import User, UserModel, Users, UserResponse +from open_webui.models.access_grants import AccessGrantModel, AccessGrants + + +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy import ( + BigInteger, + Column, + ForeignKey, + String, + Text, + JSON, + UniqueConstraint, +) + +log = logging.getLogger(__name__) + +#################### +# 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): + __tablename__ = 'knowledge' + + id = Column(Text, unique=True, primary_key=True) + user_id = Column(Text) + + name = Column(Text) + description = Column(Text) + + meta = Column(JSON, nullable=True) + + created_at = Column(BigInteger) + updated_at = Column(BigInteger) + + +class KnowledgeModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + user_id: str + + name: str + description: str + + meta: Optional[dict] = None + + access_grants: list[AccessGrantModel] = Field(default_factory=list) + + created_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + + +class KnowledgeFile(Base): + __tablename__ = 'knowledge_file' + + id = Column(Text, unique=True, primary_key=True) + + knowledge_id = Column(Text, ForeignKey('knowledge.id', ondelete='CASCADE'), nullable=False) + file_id = Column(Text, ForeignKey('file.id', ondelete='CASCADE'), nullable=False) + user_id = Column(Text, nullable=False) + + created_at = Column(BigInteger, nullable=False) + updated_at = Column(BigInteger, nullable=False) + + __table_args__ = (UniqueConstraint('knowledge_id', 'file_id', name='uq_knowledge_file_knowledge_file'),) + + +class KnowledgeFileModel(BaseModel): + id: str + knowledge_id: str + file_id: str + user_id: str + + created_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### +class KnowledgeUserModel(KnowledgeModel): + user: Optional[UserResponse] = None + + +class KnowledgeResponse(KnowledgeModel): + files: Optional[list[FileMetadataResponse | dict]] = None + + +class KnowledgeUserResponse(KnowledgeUserModel): + pass + + +class KnowledgeForm(BaseModel): + name: str + description: str + access_grants: Optional[list[dict]] = None + + +class FileUserResponse(FileModelResponse): + user: Optional[UserResponse] = None + + +class KnowledgeListResponse(BaseModel): + items: list[KnowledgeUserModel] + total: int + + +class KnowledgeFileListResponse(BaseModel): + items: list[FileUserResponse] + total: int + + +class KnowledgeTable: + async def _get_access_grants(self, knowledge_id: str, db: Optional[AsyncSession] = None) -> list[AccessGrantModel]: + return await AccessGrants.get_grants_by_resource('knowledge', knowledge_id, db=db) + + async def _to_knowledge_model( + self, + knowledge: Knowledge, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[AsyncSession] = None, + ) -> KnowledgeModel: + knowledge_data = KnowledgeModel.model_validate(knowledge).model_dump(exclude={'access_grants'}) + knowledge_data['access_grants'] = ( + access_grants if access_grants is not None else await self._get_access_grants(knowledge_data['id'], db=db) + ) + return KnowledgeModel.model_validate(knowledge_data) + + async def insert_new_knowledge( + self, user_id: str, form_data: KnowledgeForm, db: Optional[AsyncSession] = None + ) -> Optional[KnowledgeModel]: + async with get_async_db_context(db) as db: + knowledge = KnowledgeModel( + **{ + **form_data.model_dump(exclude={'access_grants'}), + 'id': str(uuid.uuid4()), + 'user_id': user_id, + 'created_at': int(time.time()), + 'updated_at': int(time.time()), + 'access_grants': [], + } + ) + + try: + result = Knowledge(**knowledge.model_dump(exclude={'access_grants'})) + db.add(result) + await db.commit() + await db.refresh(result) + await AccessGrants.set_access_grants('knowledge', result.id, form_data.access_grants, db=db) + if result: + return await self._to_knowledge_model(result, db=db) + else: + return None + except Exception: + return None + + async def get_knowledge_bases( + self, skip: int = 0, limit: int = 30, db: Optional[AsyncSession] = None + ) -> list[KnowledgeUserModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Knowledge).order_by(Knowledge.updated_at.desc())) + all_knowledge = result.scalars().all() + user_ids = list(set(knowledge.user_id for knowledge in all_knowledge)) + knowledge_ids = [knowledge.id for knowledge in all_knowledge] + + users = await Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] + users_dict = {user.id: user for user in users} + grants_map = await AccessGrants.get_grants_by_resources('knowledge', knowledge_ids, db=db) + + knowledge_bases = [] + for knowledge in all_knowledge: + user = users_dict.get(knowledge.user_id) + knowledge_bases.append( + KnowledgeUserModel.model_validate( + { + **( + await self._to_knowledge_model( + knowledge, + access_grants=grants_map.get(knowledge.id, []), + db=db, + ) + ).model_dump(), + 'user': user.model_dump() if user else None, + } + ) + ) + return knowledge_bases + + async def search_knowledge_bases( + self, + user_id: str, + filter: dict, + skip: int = 0, + limit: int = 30, + db: Optional[AsyncSession] = None, + ) -> KnowledgeListResponse: + try: + async with get_async_db_context(db) as db: + stmt = select(Knowledge, User).outerjoin(User, User.id == Knowledge.user_id) + + if filter: + query_key = filter.get('query') + if query_key: + stmt = stmt.filter( + or_( + Knowledge.name.ilike(f'%{query_key}%'), + Knowledge.description.ilike(f'%{query_key}%'), + User.name.ilike(f'%{query_key}%'), + User.email.ilike(f'%{query_key}%'), + User.username.ilike(f'%{query_key}%'), + ) + ) + + view_option = filter.get('view_option') + if view_option == 'created': + stmt = stmt.filter(Knowledge.user_id == user_id) + elif view_option == 'shared': + stmt = stmt.filter(Knowledge.user_id != user_id) + + stmt = AccessGrants.has_permission_filter( + db=db, + query=stmt, + DocumentModel=Knowledge, + filter=filter, + resource_type='knowledge', + permission='read', + ) + + stmt = stmt.order_by(Knowledge.updated_at.desc(), Knowledge.id.asc()) + + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + items = result.all() + + knowledge_ids = [kb.id for kb, _ in items] + grants_map = await AccessGrants.get_grants_by_resources('knowledge', knowledge_ids, db=db) + + knowledge_bases = [] + for knowledge_base, user in items: + knowledge_bases.append( + KnowledgeUserModel.model_validate( + { + **( + await self._to_knowledge_model( + knowledge_base, + access_grants=grants_map.get(knowledge_base.id, []), + db=db, + ) + ).model_dump(), + 'user': (UserModel.model_validate(user).model_dump() if user else None), + } + ) + ) + + return KnowledgeListResponse(items=knowledge_bases, total=total) + except Exception as e: + print(e) + return KnowledgeListResponse(items=[], total=0) + + async def search_knowledge_files( + self, filter: dict, skip: int = 0, limit: int = 30, db: Optional[AsyncSession] = None + ) -> KnowledgeFileListResponse: + """ + Scalable version: search files across all knowledge bases the user has + READ access to, without loading all KBs or using large IN() lists. + """ + try: + async with get_async_db_context(db) as db: + # Base query: join Knowledge → KnowledgeFile → File + stmt = ( + select(File, User, Knowledge) + .join(KnowledgeFile, File.id == KnowledgeFile.file_id) + .join(Knowledge, KnowledgeFile.knowledge_id == Knowledge.id) + .outerjoin(User, User.id == KnowledgeFile.user_id) + ) + + # Apply access-control directly to the joined query + stmt = AccessGrants.has_permission_filter( + db=db, + query=stmt, + DocumentModel=Knowledge, + filter=filter, + resource_type='knowledge', + permission='read', + ) + + # Apply filename search + if filter: + q = filter.get('query') + if q: + stmt = stmt.filter(File.filename.ilike(f'%{q}%')) + + # Order by file changes + stmt = stmt.order_by(File.updated_at.desc(), File.id.asc()) + + # Count before pagination + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + rows = result.all() + + items = [] + for file, user, knowledge in rows: + items.append( + FileUserResponse( + **FileModel.model_validate(file).model_dump(), + user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None), + collection=(await self._to_knowledge_model(knowledge, db=db)).model_dump(), + ) + ) + + return KnowledgeFileListResponse(items=items, total=total) + + except Exception as e: + print('search_knowledge_files error:', e) + return KnowledgeFileListResponse(items=[], total=0) + + async def check_access_by_user_id(self, id, user_id, permission='write', db: Optional[AsyncSession] = None) -> bool: + knowledge = await self.get_knowledge_by_id(id, db=db) + if not knowledge: + return False + if knowledge.user_id == user_id: + return True + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} + return await AccessGrants.has_access( + user_id=user_id, + resource_type='knowledge', + resource_id=knowledge.id, + permission=permission, + user_group_ids=user_group_ids, + db=db, + ) + + async def get_knowledge_bases_by_user_id( + self, user_id: str, permission: str = 'write', db: Optional[AsyncSession] = None + ) -> list[KnowledgeUserModel]: + knowledge_bases = await self.get_knowledge_bases(db=db) + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} + + result = [] + for knowledge_base in knowledge_bases: + if knowledge_base.user_id == user_id: + result.append(knowledge_base) + elif await AccessGrants.has_access( + user_id=user_id, + resource_type='knowledge', + resource_id=knowledge_base.id, + permission=permission, + user_group_ids=user_group_ids, + db=db, + ): + result.append(knowledge_base) + return result + + async def get_knowledge_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[KnowledgeModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Knowledge).filter_by(id=id)) + knowledge = result.scalars().first() + return await self._to_knowledge_model(knowledge, db=db) if knowledge else None + except Exception: + return None + + async def get_knowledge_by_id_and_user_id( + self, id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> Optional[KnowledgeModel]: + knowledge = await self.get_knowledge_by_id(id, db=db) + if not knowledge: + return None + + if knowledge.user_id == user_id: + return knowledge + + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} + if await AccessGrants.has_access( + user_id=user_id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + user_group_ids=user_group_ids, + db=db, + ): + return knowledge + return None + + async def get_knowledges_by_file_id(self, file_id: str, db: Optional[AsyncSession] = None) -> list[KnowledgeModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute( + select(Knowledge) + .join(KnowledgeFile, Knowledge.id == KnowledgeFile.knowledge_id) + .filter(KnowledgeFile.file_id == file_id) + ) + knowledges = result.scalars().all() + knowledge_ids = [k.id for k in knowledges] + grants_map = await AccessGrants.get_grants_by_resources('knowledge', knowledge_ids, db=db) + return [ + await self._to_knowledge_model( + knowledge, + access_grants=grants_map.get(knowledge.id, []), + db=db, + ) + for knowledge in knowledges + ] + except Exception: + return [] + + async def search_files_by_id( + self, + knowledge_id: str, + user_id: str, + filter: dict, + skip: int = 0, + limit: int = 30, + db: Optional[AsyncSession] = None, + ) -> KnowledgeFileListResponse: + try: + async with get_async_db_context(db) as db: + stmt = ( + select(File, User) + .join(KnowledgeFile, File.id == KnowledgeFile.file_id) + .outerjoin(User, User.id == KnowledgeFile.user_id) + .filter(KnowledgeFile.knowledge_id == knowledge_id) + ) + + # Default sort: updated_at descending + primary_sort = File.updated_at.desc() + + if filter: + query_key = filter.get('query') + if query_key: + stmt = stmt.filter(or_(File.filename.ilike(f'%{query_key}%'))) + + view_option = filter.get('view_option') + if view_option == 'created': + stmt = stmt.filter(KnowledgeFile.user_id == user_id) + elif view_option == 'shared': + stmt = stmt.filter(KnowledgeFile.user_id != user_id) + + order_by = filter.get('order_by') + direction = filter.get('direction') + is_asc = direction == 'asc' + + if order_by == 'name': + primary_sort = File.filename.asc() if is_asc else File.filename.desc() + elif order_by == 'created_at': + primary_sort = File.created_at.asc() if is_asc else File.created_at.desc() + elif order_by == 'updated_at': + primary_sort = File.updated_at.asc() if is_asc else File.updated_at.desc() + + # Apply sort with secondary key for deterministic pagination + stmt = stmt.order_by(primary_sort, File.id.asc()) + + # Count BEFORE pagination + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + items = result.all() + + files = [] + for file, user in items: + files.append( + FileUserResponse( + **FileModel.model_validate(file).model_dump(), + user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None), + ) + ) + + return KnowledgeFileListResponse(items=files, total=total) + except Exception as e: + print(e) + return KnowledgeFileListResponse(items=[], total=0) + + async def get_files_by_id(self, knowledge_id: str, db: Optional[AsyncSession] = None) -> list[FileModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute( + select(File) + .join(KnowledgeFile, File.id == KnowledgeFile.file_id) + .filter(KnowledgeFile.knowledge_id == knowledge_id) + ) + files = result.scalars().all() + return [FileModel.model_validate(file) for file in files] + except Exception: + return [] + + async def get_file_metadatas_by_id( + self, knowledge_id: str, db: Optional[AsyncSession] = None + ) -> list[FileMetadataResponse]: + try: + files = await self.get_files_by_id(knowledge_id, db=db) + return [FileMetadataResponse(**file.model_dump()) for file in files] + except Exception: + return [] + + async def add_file_to_knowledge_by_id( + self, + knowledge_id: str, + file_id: str, + user_id: str, + db: Optional[AsyncSession] = None, + ) -> Optional[KnowledgeFileModel]: + async with get_async_db_context(db) as db: + knowledge_file = KnowledgeFileModel( + **{ + 'id': str(uuid.uuid4()), + 'knowledge_id': knowledge_id, + 'file_id': file_id, + 'user_id': user_id, + 'created_at': int(time.time()), + 'updated_at': int(time.time()), + } + ) + + try: + result = KnowledgeFile(**knowledge_file.model_dump()) + db.add(result) + await db.commit() + await db.refresh(result) + if result: + return KnowledgeFileModel.model_validate(result) + else: + return None + except Exception: + return None + + async def has_file(self, knowledge_id: str, file_id: str, db: Optional[AsyncSession] = None) -> bool: + """Check whether a file belongs to a knowledge base.""" + try: + async with get_async_db_context(db) as db: + result = await db.execute( + select(KnowledgeFile).filter_by(knowledge_id=knowledge_id, file_id=file_id).limit(1) + ) + return result.scalars().first() is not None + except Exception: + return False + + async def remove_file_from_knowledge_by_id( + self, knowledge_id: str, file_id: str, db: Optional[AsyncSession] = None + ) -> bool: + try: + async with get_async_db_context(db) as db: + await db.execute(delete(KnowledgeFile).filter_by(knowledge_id=knowledge_id, file_id=file_id)) + await db.commit() + return True + except Exception: + return False + + async def reset_knowledge_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[KnowledgeModel]: + try: + async with get_async_db_context(db) as db: + # Delete all knowledge_file entries for this knowledge_id + await db.execute(delete(KnowledgeFile).filter_by(knowledge_id=id)) + await db.commit() + + # Update the knowledge entry's updated_at timestamp + await db.execute(update(Knowledge).filter_by(id=id).values(updated_at=int(time.time()))) + await db.commit() + + return await self.get_knowledge_by_id(id=id, db=db) + except Exception as e: + log.exception(e) + return None + + async def update_knowledge_by_id( + self, + id: str, + form_data: KnowledgeForm, + overwrite: bool = False, + db: Optional[AsyncSession] = None, + ) -> Optional[KnowledgeModel]: + try: + async with get_async_db_context(db) as db: + await db.execute( + update(Knowledge) + .filter_by(id=id) + .values( + **form_data.model_dump(exclude={'access_grants'}), + updated_at=int(time.time()), + ) + ) + await db.commit() + if form_data.access_grants is not None: + await AccessGrants.set_access_grants('knowledge', id, form_data.access_grants, db=db) + return await self.get_knowledge_by_id(id=id, db=db) + except Exception as e: + log.exception(e) + return None + + async def update_knowledge_data_by_id( + self, id: str, data: dict, db: Optional[AsyncSession] = None + ) -> Optional[KnowledgeModel]: + try: + async with get_async_db_context(db) as db: + await db.execute( + update(Knowledge) + .filter_by(id=id) + .values( + data=data, + updated_at=int(time.time()), + ) + ) + await db.commit() + return await self.get_knowledge_by_id(id=id, db=db) + except Exception as e: + log.exception(e) + return None + + async def delete_knowledge_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + await AccessGrants.revoke_all_access('knowledge', id, db=db) + await db.execute(delete(Knowledge).filter_by(id=id)) + await db.commit() + return True + except Exception: + return False + + async def delete_all_knowledge(self, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + try: + result = await db.execute(select(Knowledge.id)) + knowledge_ids = [row[0] for row in result.all()] + for knowledge_id in knowledge_ids: + await AccessGrants.revoke_all_access('knowledge', knowledge_id, db=db) + await db.execute(delete(Knowledge)) + await db.commit() + + return True + except Exception: + return False + + +Knowledges = KnowledgeTable() diff --git a/backend/open_webui/models/memories.py b/backend/open_webui/models/memories.py new file mode 100644 index 0000000000000000000000000000000000000000..e9568268002f65c5dcf654b6c099c15ec2e79674 --- /dev/null +++ b/backend/open_webui/models/memories.py @@ -0,0 +1,156 @@ +import time +import uuid +from typing import Optional + +from sqlalchemy import select, delete +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, get_async_db_context +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, String, Text + +#################### +# Memory DB Schema +# What was learned at cost should not need to be paid +# for again. Let the memory hold. +#################### + + +class Memory(Base): + __tablename__ = 'memory' + + id = Column(String, primary_key=True, unique=True) + user_id = Column(String) + content = Column(Text) + updated_at = Column(BigInteger) + created_at = Column(BigInteger) + + +class MemoryModel(BaseModel): + id: str + user_id: str + content: str + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class MemoriesTable: + async def insert_new_memory( + self, + user_id: str, + content: str, + db: Optional[AsyncSession] = None, + ) -> Optional[MemoryModel]: + async with get_async_db_context(db) as db: + id = str(uuid.uuid4()) + + memory = MemoryModel( + **{ + 'id': id, + 'user_id': user_id, + 'content': content, + 'created_at': int(time.time()), + 'updated_at': int(time.time()), + } + ) + result = Memory(**memory.model_dump()) + db.add(result) + await db.commit() + await db.refresh(result) + if result: + return MemoryModel.model_validate(result) + else: + return None + + async def update_memory_by_id_and_user_id( + self, + id: str, + user_id: str, + content: str, + db: Optional[AsyncSession] = None, + ) -> Optional[MemoryModel]: + async with get_async_db_context(db) as db: + try: + memory = await db.get(Memory, id) + if not memory or memory.user_id != user_id: + return None + + memory.content = content + memory.updated_at = int(time.time()) + + await db.commit() + await db.refresh(memory) + return MemoryModel.model_validate(memory) + except Exception: + return None + + async def get_memories(self, db: Optional[AsyncSession] = None) -> list[MemoryModel]: + async with get_async_db_context(db) as db: + try: + result = await db.execute(select(Memory)) + memories = result.scalars().all() + return [MemoryModel.model_validate(memory) for memory in memories] + except Exception: + return None + + async def get_memories_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> list[MemoryModel]: + async with get_async_db_context(db) as db: + try: + result = await db.execute(select(Memory).filter_by(user_id=user_id)) + memories = result.scalars().all() + return [MemoryModel.model_validate(memory) for memory in memories] + except Exception: + return None + + async def get_memory_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[MemoryModel]: + async with get_async_db_context(db) as db: + try: + memory = await db.get(Memory, id) + return MemoryModel.model_validate(memory) if memory else None + except Exception: + return None + + async def delete_memory_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + try: + await db.execute(delete(Memory).filter_by(id=id)) + await db.commit() + + return True + + except Exception: + return False + + async def delete_memories_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + try: + await db.execute(delete(Memory).filter_by(user_id=user_id)) + await db.commit() + + return True + except Exception: + return False + + async def delete_memory_by_id_and_user_id(self, id: str, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + try: + memory = await db.get(Memory, id) + if not memory or memory.user_id != user_id: + return None + + # Delete the memory + await db.delete(memory) + await db.commit() + + return True + except Exception: + return False + + +Memories = MemoriesTable() diff --git a/backend/open_webui/models/messages.py b/backend/open_webui/models/messages.py new file mode 100644 index 0000000000000000000000000000000000000000..7f33a72effa721a729ff86d94574e3ecd7e6549d --- /dev/null +++ b/backend/open_webui/models/messages.py @@ -0,0 +1,564 @@ +import json +import time +import uuid +from typing import Optional + +from sqlalchemy import select, delete, func +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.models.tags import TagModel, Tag, Tags +from open_webui.models.users import Users, User, UserNameResponse +from open_webui.models.channels import Channels, ChannelMember + + +from pydantic import BaseModel, ConfigDict, field_validator +from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON +from sqlalchemy import or_, func, and_, text +from sqlalchemy.sql import exists + +#################### +# Message DB Schema +#################### + + +class MessageReaction(Base): + __tablename__ = 'message_reaction' + id = Column(Text, primary_key=True, unique=True) + user_id = Column(Text) + message_id = Column(Text) + name = Column(Text) + created_at = Column(BigInteger) + + +class MessageReactionModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + user_id: str + message_id: str + name: str + created_at: int # timestamp in epoch + + +class Message(Base): + __tablename__ = 'message' + id = Column(Text, primary_key=True, unique=True) + + user_id = Column(Text) + channel_id = Column(Text, nullable=True) + + reply_to_id = Column(Text, nullable=True) + parent_id = Column(Text, nullable=True) + + # Pins + is_pinned = Column(Boolean, nullable=False, default=False) + pinned_at = Column(BigInteger, nullable=True) + pinned_by = Column(Text, nullable=True) + + content = Column(Text) + data = Column(JSON, nullable=True) + meta = Column(JSON, nullable=True) + + created_at = Column(BigInteger) # time_ns + updated_at = Column(BigInteger) # time_ns + + +class MessageModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + user_id: str + channel_id: Optional[str] = None + + reply_to_id: Optional[str] = None + parent_id: Optional[str] = None + + # Pins + is_pinned: bool = False + pinned_by: Optional[str] = None + pinned_at: Optional[int] = None # timestamp in epoch (time_ns) + + content: str + data: Optional[dict] = None + meta: Optional[dict] = None + + created_at: int # timestamp in epoch (time_ns) + updated_at: int # timestamp in epoch (time_ns) + + +#################### +# Forms +#################### + + +class MessageForm(BaseModel): + temp_id: Optional[str] = None + content: str + reply_to_id: Optional[str] = None + parent_id: Optional[str] = None + data: Optional[dict] = None + meta: Optional[dict] = None + + +class Reactions(BaseModel): + name: str + users: list[dict] + count: int + + +class MessageUserResponse(MessageModel): + user: Optional[UserNameResponse] = None + + +class MessageUserSlimResponse(MessageUserResponse): + data: bool | None = None + + @field_validator('data', mode='before') + def convert_data_to_bool(cls, v): + # No data or not a dict → False + if not isinstance(v, dict): + return False + + # True if ANY value in the dict is non-empty + return any(bool(val) for val in v.values()) + + +class MessageReplyToResponse(MessageUserResponse): + reply_to_message: Optional[MessageUserSlimResponse] = None + + +class MessageWithReactionsResponse(MessageUserSlimResponse): + reactions: list[Reactions] + + +class MessageResponse(MessageReplyToResponse): + latest_reply_at: Optional[int] + reply_count: int + reactions: list[Reactions] + + +class MessageTable: + async def insert_new_message( + self, + form_data: MessageForm, + channel_id: str, + user_id: str, + db: Optional[AsyncSession] = None, + ) -> Optional[MessageModel]: + async with get_async_db_context(db) as db: + channel_member = await Channels.join_channel(channel_id, user_id) + + id = str(uuid.uuid4()) + ts = int(time.time_ns()) + + message = MessageModel( + **{ + 'id': id, + 'user_id': user_id, + 'channel_id': channel_id, + 'reply_to_id': form_data.reply_to_id, + 'parent_id': form_data.parent_id, + 'is_pinned': False, + 'pinned_at': None, + 'pinned_by': None, + 'content': form_data.content, + 'data': form_data.data, + 'meta': form_data.meta, + 'created_at': ts, + 'updated_at': ts, + } + ) + result = Message(**message.model_dump()) + + db.add(result) + await db.commit() + await db.refresh(result) + return MessageModel.model_validate(result) if result else None + + async def get_message_by_id( + self, + id: str, + include_thread_replies: Optional[bool] = True, + db: Optional[AsyncSession] = None, + ) -> Optional[MessageResponse]: + async with get_async_db_context(db) as db: + message = await db.get(Message, id) + if not message: + return None + + reply_to_message = ( + await self.get_message_by_id(message.reply_to_id, include_thread_replies=False, db=db) + if message.reply_to_id + else None + ) + + reactions = await self.get_reactions_by_message_id(id, db=db) + + thread_replies = [] + if include_thread_replies: + thread_replies = await self.get_thread_replies_by_message_id(id, db=db) + + # Check if message was sent by webhook (webhook info in meta takes precedence) + webhook_info = message.meta.get('webhook') if message.meta else None + if webhook_info and webhook_info.get('id'): + # Look up webhook by ID to get current name + webhook = await Channels.get_webhook_by_id(webhook_info.get('id'), db=db) + if webhook: + user_info = { + 'id': webhook.id, + 'name': webhook.name, + 'role': 'webhook', + } + else: + # Webhook was deleted, use placeholder + user_info = { + 'id': webhook_info.get('id'), + 'name': 'Deleted Webhook', + 'role': 'webhook', + } + else: + user = await Users.get_user_by_id(message.user_id, db=db) + user_info = user.model_dump() if user else None + + return MessageResponse.model_validate( + { + **MessageModel.model_validate(message).model_dump(), + 'user': user_info, + 'reply_to_message': (reply_to_message.model_dump() if reply_to_message else None), + 'latest_reply_at': (thread_replies[0].created_at if thread_replies else None), + 'reply_count': len(thread_replies), + 'reactions': reactions, + } + ) + + async def _resolve_user_info(self, message: Message, db: AsyncSession) -> Optional[dict]: + """Resolve user info from message, handling webhook messages.""" + webhook_info = message.meta.get('webhook') if message.meta else None + if webhook_info and webhook_info.get('id'): + webhook = await Channels.get_webhook_by_id(webhook_info.get('id'), db=db) + if webhook: + return { + 'id': webhook.id, + 'name': webhook.name, + 'role': 'webhook', + } + else: + return { + 'id': webhook_info.get('id'), + 'name': 'Deleted Webhook', + 'role': 'webhook', + } + return None + + async def get_thread_replies_by_message_id( + self, id: str, db: Optional[AsyncSession] = None + ) -> list[MessageReplyToResponse]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Message).filter_by(parent_id=id).order_by(Message.created_at.desc())) + all_messages = result.scalars().all() + + messages = [] + for message in all_messages: + reply_to_message = ( + await self.get_message_by_id(message.reply_to_id, include_thread_replies=False, db=db) + if message.reply_to_id + else None + ) + + user_info = await self._resolve_user_info(message, db) + + messages.append( + MessageReplyToResponse.model_validate( + { + **MessageModel.model_validate(message).model_dump(), + 'user': user_info, + 'reply_to_message': (reply_to_message.model_dump() if reply_to_message else None), + } + ) + ) + return messages + + async def get_reply_user_ids_by_message_id(self, id: str, db: Optional[AsyncSession] = None) -> list[str]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Message.user_id).filter_by(parent_id=id)) + return [row[0] for row in result.all()] + + async def get_messages_by_channel_id( + self, + channel_id: str, + skip: int = 0, + limit: int = 50, + db: Optional[AsyncSession] = None, + ) -> list[MessageReplyToResponse]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(Message) + .filter_by(channel_id=channel_id, parent_id=None) + .order_by(Message.created_at.desc()) + .offset(skip) + .limit(limit) + ) + all_messages = result.scalars().all() + + messages = [] + for message in all_messages: + reply_to_message = ( + await self.get_message_by_id(message.reply_to_id, include_thread_replies=False, db=db) + if message.reply_to_id + else None + ) + + user_info = await self._resolve_user_info(message, db) + + messages.append( + MessageReplyToResponse.model_validate( + { + **MessageModel.model_validate(message).model_dump(), + 'user': user_info, + 'reply_to_message': (reply_to_message.model_dump() if reply_to_message else None), + } + ) + ) + return messages + + async def get_messages_by_parent_id( + self, + channel_id: str, + parent_id: str, + skip: int = 0, + limit: int = 50, + db: Optional[AsyncSession] = None, + ) -> list[MessageReplyToResponse]: + async with get_async_db_context(db) as db: + message = await db.get(Message, parent_id) + + if not message: + return [] + + result = await db.execute( + select(Message) + .filter_by(channel_id=channel_id, parent_id=parent_id) + .order_by(Message.created_at.desc()) + .offset(skip) + .limit(limit) + ) + all_messages = list(result.scalars().all()) + + # If length of all_messages is less than limit, then add the parent message + if len(all_messages) < limit: + all_messages.append(message) + + messages = [] + for message in all_messages: + reply_to_message = ( + await self.get_message_by_id(message.reply_to_id, include_thread_replies=False, db=db) + if message.reply_to_id + else None + ) + + user_info = await self._resolve_user_info(message, db) + + messages.append( + MessageReplyToResponse.model_validate( + { + **MessageModel.model_validate(message).model_dump(), + 'user': user_info, + 'reply_to_message': (reply_to_message.model_dump() if reply_to_message else None), + } + ) + ) + return messages + + async def get_last_message_by_channel_id( + self, channel_id: str, db: Optional[AsyncSession] = None + ) -> Optional[MessageModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(Message).filter_by(channel_id=channel_id).order_by(Message.created_at.desc()).limit(1) + ) + message = result.scalars().first() + return MessageModel.model_validate(message) if message else None + + async def get_pinned_messages_by_channel_id( + self, + channel_id: str, + skip: int = 0, + limit: int = 50, + db: Optional[AsyncSession] = None, + ) -> list[MessageModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(Message) + .filter_by(channel_id=channel_id, is_pinned=True) + .order_by(Message.pinned_at.desc()) + .offset(skip) + .limit(limit) + ) + all_messages = result.scalars().all() + return [MessageModel.model_validate(message) for message in all_messages] + + async def update_message_by_id( + self, id: str, form_data: MessageForm, db: Optional[AsyncSession] = None + ) -> Optional[MessageModel]: + async with get_async_db_context(db) as db: + message = await db.get(Message, id) + message.content = form_data.content + message.data = { + **(message.data if message.data else {}), + **(form_data.data if form_data.data else {}), + } + message.meta = { + **(message.meta if message.meta else {}), + **(form_data.meta if form_data.meta else {}), + } + message.updated_at = int(time.time_ns()) + await db.commit() + await db.refresh(message) + return MessageModel.model_validate(message) if message else None + + async def update_is_pinned_by_id( + self, + id: str, + is_pinned: bool, + pinned_by: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> Optional[MessageModel]: + async with get_async_db_context(db) as db: + message = await db.get(Message, id) + message.is_pinned = is_pinned + message.pinned_at = int(time.time_ns()) if is_pinned else None + message.pinned_by = pinned_by if is_pinned else None + await db.commit() + await db.refresh(message) + return MessageModel.model_validate(message) if message else None + + async def get_unread_message_count( + self, + channel_id: str, + user_id: str, + last_read_at: Optional[int] = None, + db: Optional[AsyncSession] = None, + ) -> int: + async with get_async_db_context(db) as db: + stmt = select(func.count(Message.id)).filter( + Message.channel_id == channel_id, + Message.parent_id == None, # only count top-level messages + Message.created_at > (last_read_at if last_read_at else 0), + ) + if user_id: + stmt = stmt.filter(Message.user_id != user_id) + result = await db.execute(stmt) + return result.scalar() + + async def add_reaction_to_message( + self, id: str, user_id: str, name: str, db: Optional[AsyncSession] = None + ) -> Optional[MessageReactionModel]: + async with get_async_db_context(db) as db: + # check for existing reaction + result = await db.execute(select(MessageReaction).filter_by(message_id=id, user_id=user_id, name=name)) + existing_reaction = result.scalars().first() + if existing_reaction: + return MessageReactionModel.model_validate(existing_reaction) + + reaction_id = str(uuid.uuid4()) + reaction = MessageReactionModel( + id=reaction_id, + user_id=user_id, + message_id=id, + name=name, + created_at=int(time.time_ns()), + ) + result = MessageReaction(**reaction.model_dump()) + db.add(result) + await db.commit() + await db.refresh(result) + return MessageReactionModel.model_validate(result) if result else None + + async def get_reactions_by_message_id(self, id: str, db: Optional[AsyncSession] = None) -> list[Reactions]: + async with get_async_db_context(db) as db: + # JOIN User so all user info is fetched in one query + result = await db.execute( + select(MessageReaction, User) + .join(User, MessageReaction.user_id == User.id) + .filter(MessageReaction.message_id == id) + ) + results = result.all() + + reactions = {} + + for reaction, user in results: + if reaction.name not in reactions: + reactions[reaction.name] = { + 'name': reaction.name, + 'users': [], + 'count': 0, + } + + reactions[reaction.name]['users'].append( + { + 'id': user.id, + 'name': user.name, + } + ) + reactions[reaction.name]['count'] += 1 + + return [Reactions(**reaction) for reaction in reactions.values()] + + async def remove_reaction_by_id_and_user_id_and_name( + self, id: str, user_id: str, name: str, db: Optional[AsyncSession] = None + ) -> bool: + async with get_async_db_context(db) as db: + await db.execute(delete(MessageReaction).filter_by(message_id=id, user_id=user_id, name=name)) + await db.commit() + return True + + async def delete_reactions_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + await db.execute(delete(MessageReaction).filter_by(message_id=id)) + await db.commit() + return True + + async def delete_replies_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + await db.execute(delete(Message).filter_by(parent_id=id)) + await db.commit() + return True + + async def delete_message_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + await db.execute(delete(Message).filter_by(id=id)) + + # Delete all reactions to this message + await db.execute(delete(MessageReaction).filter_by(message_id=id)) + + await db.commit() + return True + + async def search_messages_by_channel_ids( + self, + channel_ids: list[str], + query: str, + start_timestamp: Optional[int] = None, + end_timestamp: Optional[int] = None, + limit: int = 10, + db: Optional[AsyncSession] = None, + ) -> list[MessageModel]: + """Search messages in specified channels by content.""" + async with get_async_db_context(db) as db: + stmt = select(Message).filter( + Message.channel_id.in_(channel_ids), + Message.content.ilike(f'%{query}%'), + ) + + if start_timestamp: + stmt = stmt.filter(Message.created_at >= start_timestamp) + if end_timestamp: + stmt = stmt.filter(Message.created_at <= end_timestamp) + + stmt = stmt.order_by(Message.created_at.desc()).limit(limit) + result = await db.execute(stmt) + messages = result.scalars().all() + return [MessageModel.model_validate(msg) for msg in messages] + + +Messages = MessageTable() diff --git a/backend/open_webui/models/models.py b/backend/open_webui/models/models.py new file mode 100644 index 0000000000000000000000000000000000000000..79c13153ac804a797fcec97fac5377296d6c040f --- /dev/null +++ b/backend/open_webui/models/models.py @@ -0,0 +1,603 @@ +import json +import logging +import time +from typing import Optional + +from sqlalchemy import select, delete, update, or_, func, String, cast +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, JSONField, get_async_db_context + +from open_webui.models.groups import Groups +from open_webui.models.users import User, UserModel, Users, UserResponse +from open_webui.models.access_grants import AccessGrantModel, AccessGrants + + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy import BigInteger, Column, Text, Boolean + +log = logging.getLogger(__name__) + + +#################### +# Models DB Schema +# A misconfigured model wastes the time of everyone +# who trusts it. Let what is set here be set with care. +#################### + + +# ModelParams is a model for the data stored in the params field of the Model table +class ModelParams(BaseModel): + model_config = ConfigDict(extra='allow') + pass + + +# ModelMeta is a model for the data stored in the meta field of the Model table +class ModelMeta(BaseModel): + profile_image_url: Optional[str] = '/static/favicon.png' + + description: Optional[str] = None + """ + User-facing description of the model. + """ + + capabilities: Optional[dict] = None + + model_config = ConfigDict(extra='allow') + + @model_validator(mode='before') + @classmethod + def normalize_tags(cls, data): + if isinstance(data, dict) and 'tags' in data: + raw_tags = data['tags'] + if isinstance(raw_tags, list): + normalized = [] + for tag in raw_tags: + if isinstance(tag, str): + normalized.append({'name': tag}) + elif isinstance(tag, dict) and 'name' in tag: + normalized.append(tag) + data['tags'] = normalized + return data + + +class Model(Base): + __tablename__ = 'model' + + id = Column(Text, primary_key=True, unique=True) + """ + The model's id as used in the API. If set to an existing model, it will override the model. + """ + user_id = Column(Text) + + base_model_id = Column(Text, nullable=True) + """ + An optional pointer to the actual model that should be used when proxying requests. + """ + + name = Column(Text) + """ + The human-readable display name of the model. + """ + + params = Column(JSONField) + """ + Holds a JSON encoded blob of parameters, see `ModelParams`. + """ + + meta = Column(JSONField) + """ + Holds a JSON encoded blob of metadata, see `ModelMeta`. + """ + + is_active = Column(Boolean, default=True) + + updated_at = Column(BigInteger) + created_at = Column(BigInteger) + + +class ModelModel(BaseModel): + id: str + user_id: str + base_model_id: Optional[str] = None + + name: str + params: ModelParams + meta: ModelMeta + + access_grants: list[AccessGrantModel] = Field(default_factory=list) + + is_active: bool + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class ModelUserResponse(ModelModel): + user: Optional[UserResponse] = None + + +class ModelAccessResponse(ModelUserResponse): + write_access: Optional[bool] = False + + +class ModelResponse(ModelModel): + pass + + +class ModelListResponse(BaseModel): + items: list[ModelUserResponse] + total: int + + +class ModelAccessListResponse(BaseModel): + items: list[ModelAccessResponse] + total: int + + +class ModelForm(BaseModel): + model_config = ConfigDict(extra='ignore') + + id: str + base_model_id: Optional[str] = None + name: str + meta: ModelMeta + params: ModelParams + access_grants: Optional[list[dict]] = None + is_active: bool = True + + +class ModelsTable: + async def _get_access_grants(self, model_id: str, db: Optional[AsyncSession] = None) -> list[AccessGrantModel]: + return await AccessGrants.get_grants_by_resource('model', model_id, db=db) + + async def _to_model_model( + self, + model: Model, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[AsyncSession] = None, + ) -> ModelModel: + model_data = ModelModel.model_validate(model).model_dump(exclude={'access_grants'}) + model_data['access_grants'] = ( + access_grants if access_grants is not None else await self._get_access_grants(model_data['id'], db=db) + ) + return ModelModel.model_validate(model_data) + + async def insert_new_model( + self, form_data: ModelForm, user_id: str, db: Optional[AsyncSession] = None + ) -> Optional[ModelModel]: + try: + async with get_async_db_context(db) as db: + result = Model( + **{ + **form_data.model_dump(exclude={'access_grants'}), + 'user_id': user_id, + 'created_at': int(time.time()), + 'updated_at': int(time.time()), + } + ) + db.add(result) + await db.commit() + await db.refresh(result) + await AccessGrants.set_access_grants('model', result.id, form_data.access_grants, db=db) + + if result: + return await self._to_model_model(result, db=db) + else: + return None + except Exception as e: + log.exception(f'Failed to insert a new model: {e}') + return None + + async def get_all_models(self, db: Optional[AsyncSession] = None) -> list[ModelModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Model)) + all_models = result.scalars().all() + model_ids = [model.id for model in all_models] + grants_map = await AccessGrants.get_grants_by_resources('model', model_ids, db=db) + return [ + await self._to_model_model(model, access_grants=grants_map.get(model.id, []), db=db) + for model in all_models + ] + + async def get_models(self, db: Optional[AsyncSession] = None) -> list[ModelUserResponse]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Model).filter(Model.base_model_id != None)) + all_models = result.scalars().all() + + user_ids = list(set(model.user_id for model in all_models)) + model_ids = [model.id for model in all_models] + + users = await Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] + users_dict = {user.id: user for user in users} + grants_map = await AccessGrants.get_grants_by_resources('model', model_ids, db=db) + + models = [] + for model in all_models: + user = users_dict.get(model.user_id) + models.append( + ModelUserResponse.model_validate( + { + **( + await self._to_model_model( + model, + access_grants=grants_map.get(model.id, []), + db=db, + ) + ).model_dump(), + 'user': user.model_dump() if user else None, + } + ) + ) + return models + + async def get_base_models(self, db: Optional[AsyncSession] = None) -> list[ModelModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Model).filter(Model.base_model_id == None)) + all_models = result.scalars().all() + model_ids = [model.id for model in all_models] + grants_map = await AccessGrants.get_grants_by_resources('model', model_ids, db=db) + return [ + await self._to_model_model(model, access_grants=grants_map.get(model.id, []), db=db) + for model in all_models + ] + + async def get_models_by_user_id( + self, user_id: str, permission: str = 'write', db: Optional[AsyncSession] = None + ) -> list[ModelUserResponse]: + models = await self.get_models(db=db) + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} + + result = [] + for model in models: + if model.user_id == user_id: + result.append(model) + elif await AccessGrants.has_access( + user_id=user_id, + resource_type='model', + resource_id=model.id, + permission=permission, + user_group_ids=user_group_ids, + db=db, + ): + result.append(model) + return result + + def _has_permission(self, db, query, filter: dict, permission: str = 'read'): + return AccessGrants.has_permission_filter( + db=db, + query=query, + DocumentModel=Model, + filter=filter, + resource_type='model', + permission=permission, + ) + + async def search_models( + self, + user_id: str, + filter: dict = {}, + skip: int = 0, + limit: int = 30, + db: Optional[AsyncSession] = None, + ) -> ModelListResponse: + async with get_async_db_context(db) as db: + stmt = select(Model, User).outerjoin(User, User.id == Model.user_id) + stmt = stmt.filter(Model.base_model_id != None) + + if filter: + query_key = filter.get('query') + if query_key: + stmt = stmt.filter( + or_( + Model.name.ilike(f'%{query_key}%'), + Model.base_model_id.ilike(f'%{query_key}%'), + User.name.ilike(f'%{query_key}%'), + User.email.ilike(f'%{query_key}%'), + User.username.ilike(f'%{query_key}%'), + ) + ) + + view_option = filter.get('view_option') + if view_option == 'created': + stmt = stmt.filter(Model.user_id == user_id) + elif view_option == 'shared': + stmt = stmt.filter(Model.user_id != user_id) + + # Apply access control filtering + stmt = self._has_permission( + db, + stmt, + filter, + permission='read', + ) + + tag = filter.get('tag') + if tag: + # SQLite stores JSON text via json.dumps(ensure_ascii=True), + # so non-ASCII chars are \uXXXX-escaped. PostgreSQL native JSONB + # stores literal Unicode. Use the right pattern for each. + if db.bind.dialect.name == 'sqlite': + if tag.isascii(): + meta_text = func.lower(cast(Model.meta, String)) + pattern = f'%{json.dumps(tag.lower())}%' + else: + meta_text = cast(Model.meta, String) + pattern = f'%{json.dumps(tag)}%' + else: + meta_text = func.lower(cast(Model.meta, String)) + pattern = f'%{json.dumps(tag.lower(), ensure_ascii=False)}%' + stmt = stmt.filter(meta_text.like(pattern)) + + order_by = filter.get('order_by') + direction = filter.get('direction') + + if order_by == 'name': + if direction == 'asc': + stmt = stmt.order_by(Model.name.asc()) + else: + stmt = stmt.order_by(Model.name.desc()) + elif order_by == 'created_at': + if direction == 'asc': + stmt = stmt.order_by(Model.created_at.asc()) + else: + stmt = stmt.order_by(Model.created_at.desc()) + elif order_by == 'updated_at': + if direction == 'asc': + stmt = stmt.order_by(Model.updated_at.asc()) + else: + stmt = stmt.order_by(Model.updated_at.desc()) + + else: + stmt = stmt.order_by(Model.created_at.desc()) + + # Count BEFORE pagination + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + items = result.all() + + model_ids = [model.id for model, _ in items] + grants_map = await AccessGrants.get_grants_by_resources('model', model_ids, db=db) + + models = [] + for model, user in items: + models.append( + ModelUserResponse( + **( + await self._to_model_model( + model, + access_grants=grants_map.get(model.id, []), + db=db, + ) + ).model_dump(), + user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None), + ) + ) + + return ModelListResponse(items=models, total=total) + + async def get_model_meta_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[tuple[dict, int]]: + """Return (meta, updated_at) for a model, skipping access grant resolution.""" + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Model.meta, Model.updated_at).filter_by(id=id)) + return result.first() + except Exception: + return None + + async def get_all_tags( + self, + user_id: str, + is_admin: bool = False, + db: Optional[AsyncSession] = None, + ) -> set[str]: + """Extract unique tag names from model meta, querying only the meta column.""" + async with get_async_db_context(db) as db: + stmt = select(Model.meta).filter(Model.base_model_id != None) + + if not is_admin: + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = [group.id for group in user_groups] + + filter_dict = {'user_id': user_id} + if user_group_ids: + filter_dict['group_ids'] = user_group_ids + + stmt = self._has_permission(db, stmt, filter_dict, permission='read') + + result = await db.execute(stmt) + rows = result.scalars().all() + + tags_set: set[str] = set() + for meta in rows: + if not meta: + continue + for tag in meta.get('tags', []): + try: + name = tag.get('name') if isinstance(tag, dict) else str(tag) + if name: + tags_set.add(name) + except Exception: + continue + + return tags_set + + async def get_model_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[ModelModel]: + try: + async with get_async_db_context(db) as db: + model = await db.get(Model, id) + return await self._to_model_model(model, db=db) if model else None + except Exception: + return None + + async def get_models_by_ids(self, ids: list[str], db: Optional[AsyncSession] = None) -> list[ModelModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Model).filter(Model.id.in_(ids))) + models = result.scalars().all() + model_ids = [model.id for model in models] + grants_map = await AccessGrants.get_grants_by_resources('model', model_ids, db=db) + return [ + await self._to_model_model( + model, + access_grants=grants_map.get(model.id, []), + db=db, + ) + for model in models + ] + except Exception: + return [] + + async def toggle_model_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[ModelModel]: + async with get_async_db_context(db) as db: + try: + result = await db.execute(select(Model).filter_by(id=id)) + model = result.scalars().first() + if not model: + return None + + model.is_active = not model.is_active + model.updated_at = int(time.time()) + await db.commit() + await db.refresh(model) + + return await self._to_model_model(model, db=db) + except Exception: + return None + + async def update_model_by_id( + self, id: str, model: ModelForm, db: Optional[AsyncSession] = None + ) -> Optional[ModelModel]: + try: + async with get_async_db_context(db) as db: + # update only the fields that are present in the model + data = model.model_dump(exclude={'id', 'access_grants'}) + data['updated_at'] = int(time.time()) + await db.execute(update(Model).filter_by(id=id).values(**data)) + + await db.commit() + if model.access_grants is not None: + await AccessGrants.set_access_grants('model', id, model.access_grants, db=db) + + return await self.get_model_by_id(id, db=db) + except Exception as e: + log.exception(f'Failed to update the model by id {id}: {e}') + return None + + async def update_model_updated_at_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[ModelModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Model).filter_by(id=id)) + model_obj = result.scalars().first() + if not model_obj: + return None + model_obj.updated_at = int(time.time()) + await db.commit() + await db.refresh(model_obj) + return await self._to_model_model(model_obj, db=db) + except Exception as e: + log.exception(f'Failed to update the model updated_at by id {id}: {e}') + return None + + async def delete_model_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + await AccessGrants.revoke_all_access('model', id, db=db) + await db.execute(delete(Model).filter_by(id=id)) + await db.commit() + + return True + except Exception: + return False + + async def delete_all_models(self, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Model.id)) + model_ids = [row[0] for row in result.all()] + for model_id in model_ids: + await AccessGrants.revoke_all_access('model', model_id, db=db) + await db.execute(delete(Model)) + await db.commit() + + return True + except Exception: + return False + + async def sync_models( + self, user_id: str, models: list[ModelModel], db: Optional[AsyncSession] = None + ) -> list[ModelModel]: + try: + async with get_async_db_context(db) as db: + # Get existing models + result = await db.execute(select(Model)) + existing_models = result.scalars().all() + existing_ids = {model.id for model in existing_models} + + # Prepare a set of new model IDs + new_model_ids = {model.id for model in models} + + # Update or insert models + for model in models: + if model.id in existing_ids: + await db.execute( + update(Model) + .filter_by(id=model.id) + .values( + **model.model_dump(exclude={'access_grants'}), + user_id=user_id, + updated_at=int(time.time()), + ) + ) + else: + new_model = Model( + **{ + **model.model_dump(exclude={'access_grants'}), + 'user_id': user_id, + 'updated_at': int(time.time()), + } + ) + db.add(new_model) + await AccessGrants.set_access_grants('model', model.id, model.access_grants, db=db) + + # Remove models that are no longer present + for model in existing_models: + if model.id not in new_model_ids: + await AccessGrants.revoke_all_access('model', model.id, db=db) + await db.delete(model) + + await db.commit() + + result = await db.execute(select(Model)) + all_models = result.scalars().all() + model_ids = [model.id for model in all_models] + grants_map = await AccessGrants.get_grants_by_resources('model', model_ids, db=db) + return [ + await self._to_model_model( + model, + access_grants=grants_map.get(model.id, []), + db=db, + ) + for model in all_models + ] + except Exception as e: + log.exception(f'Error syncing models for user {user_id}: {e}') + return [] + + +Models = ModelsTable() diff --git a/backend/open_webui/models/notes.py b/backend/open_webui/models/notes.py new file mode 100644 index 0000000000000000000000000000000000000000..1a34750a7d6adc106948f7c2eda46d2936bcc346 --- /dev/null +++ b/backend/open_webui/models/notes.py @@ -0,0 +1,361 @@ +import json +import time +import uuid +from typing import Optional +from functools import lru_cache + +from sqlalchemy import Boolean, select, delete, update, or_, func, cast +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, get_async_db_context +from open_webui.models.groups import Groups +from open_webui.models.users import User, UserModel, Users, UserResponse +from open_webui.models.access_grants import AccessGrantModel, AccessGrants + + +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy import BigInteger, Column, Text, JSON + +#################### +# Note DB Schema +#################### + + +class Note(Base): + __tablename__ = 'note' + + id = Column(Text, primary_key=True, unique=True) + user_id = Column(Text) + + title = Column(Text) + data = Column(JSON, nullable=True) + meta = Column(JSON, nullable=True) + is_pinned = Column(Boolean, default=False, nullable=True) + + created_at = Column(BigInteger) + updated_at = Column(BigInteger) + + +class NoteModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + user_id: str + + title: str + data: Optional[dict] = None + meta: Optional[dict] = None + is_pinned: Optional[bool] = False + + access_grants: list[AccessGrantModel] = Field(default_factory=list) + + created_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + + +#################### +# Forms +#################### + + +class NoteForm(BaseModel): + title: str + data: Optional[dict] = None + meta: Optional[dict] = None + access_grants: Optional[list[dict]] = None + + +class NoteUpdateForm(BaseModel): + title: Optional[str] = None + data: Optional[dict] = None + meta: Optional[dict] = None + access_grants: Optional[list[dict]] = None + + +class NoteUserResponse(NoteModel): + user: Optional[UserResponse] = None + + +class NoteItemResponse(BaseModel): + id: str + title: str + data: Optional[dict] + is_pinned: Optional[bool] = False + updated_at: int + created_at: int + user: Optional[UserResponse] = None + + +class NoteListResponse(BaseModel): + items: list[NoteUserResponse] + total: int + + +class NoteTable: + async def _get_access_grants(self, note_id: str, db: Optional[AsyncSession] = None) -> list[AccessGrantModel]: + return await AccessGrants.get_grants_by_resource('note', note_id, db=db) + + async def _to_note_model( + self, + note: Note, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[AsyncSession] = None, + ) -> NoteModel: + note_data = NoteModel.model_validate(note).model_dump(exclude={'access_grants'}) + note_data['access_grants'] = ( + access_grants if access_grants is not None else await self._get_access_grants(note_data['id'], db=db) + ) + return NoteModel.model_validate(note_data) + + def _has_permission(self, db, query, filter: dict, permission: str = 'read'): + return AccessGrants.has_permission_filter( + db=db, + query=query, + DocumentModel=Note, + filter=filter, + resource_type='note', + permission=permission, + ) + + async def insert_new_note( + self, user_id: str, form_data: NoteForm, db: Optional[AsyncSession] = None + ) -> Optional[NoteModel]: + async with get_async_db_context(db) as db: + note = NoteModel( + **{ + 'id': str(uuid.uuid4()), + 'user_id': user_id, + **form_data.model_dump(exclude={'access_grants'}), + 'created_at': int(time.time_ns()), + 'updated_at': int(time.time_ns()), + 'access_grants': [], + } + ) + + new_note = Note(**note.model_dump(exclude={'access_grants'})) + + db.add(new_note) + await db.commit() + await AccessGrants.set_access_grants('note', note.id, form_data.access_grants, db=db) + return await self._to_note_model(new_note, db=db) + + async def get_notes(self, skip: int = 0, limit: int = 50, db: Optional[AsyncSession] = None) -> list[NoteModel]: + async with get_async_db_context(db) as db: + stmt = select(Note).order_by(Note.updated_at.desc()) + if skip is not None: + stmt = stmt.offset(skip) + if limit is not None: + stmt = stmt.limit(limit) + result = await db.execute(stmt) + notes = result.scalars().all() + note_ids = [note.id for note in notes] + grants_map = await AccessGrants.get_grants_by_resources('note', note_ids, db=db) + return [await self._to_note_model(note, access_grants=grants_map.get(note.id, []), db=db) for note in notes] + + async def search_notes( + self, + user_id: str, + filter: dict = {}, + skip: int = 0, + limit: int = 30, + db: Optional[AsyncSession] = None, + ) -> NoteListResponse: + async with get_async_db_context(db) as db: + stmt = select(Note, User).outerjoin(User, User.id == Note.user_id) + if filter: + query_key = filter.get('query') + if query_key: + # Split query into individual words and normalize each + # (strip hyphens so "todo" matches "to-do"). + # All words must match somewhere in title OR content (AND semantics). + search_words = query_key.split() + normalized_words = [w.replace('-', '') for w in search_words if w.replace('-', '')] + for word in normalized_words: + stmt = stmt.filter( + or_( + func.replace(func.replace(Note.title, '-', ''), ' ', '').ilike(f'%{word}%'), + func.replace( + func.replace(cast(Note.data['content']['md'], Text), '-', ''), + ' ', + '', + ).ilike(f'%{word}%'), + ) + ) + + view_option = filter.get('view_option') + if view_option == 'created': + stmt = stmt.filter(Note.user_id == user_id) + elif view_option == 'shared': + stmt = stmt.filter(Note.user_id != user_id) + + # Apply access control filtering + if 'permission' in filter: + permission = filter['permission'] + else: + permission = 'write' + + stmt = self._has_permission( + db, + stmt, + filter, + permission=permission, + ) + + order_by = filter.get('order_by') + direction = filter.get('direction') + + if order_by == 'name': + if direction == 'asc': + stmt = stmt.order_by(Note.title.asc()) + else: + stmt = stmt.order_by(Note.title.desc()) + elif order_by == 'created_at': + if direction == 'asc': + stmt = stmt.order_by(Note.created_at.asc()) + else: + stmt = stmt.order_by(Note.created_at.desc()) + elif order_by == 'updated_at': + if direction == 'asc': + stmt = stmt.order_by(Note.updated_at.asc()) + else: + stmt = stmt.order_by(Note.updated_at.desc()) + else: + stmt = stmt.order_by(Note.updated_at.desc()) + + else: + stmt = stmt.order_by(Note.updated_at.desc()) + + # Count BEFORE pagination + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + items = result.all() + + note_ids = [note.id for note, _ in items] + grants_map = await AccessGrants.get_grants_by_resources('note', note_ids, db=db) + + notes = [] + for note, user in items: + notes.append( + NoteUserResponse( + **( + await self._to_note_model( + note, + access_grants=grants_map.get(note.id, []), + db=db, + ) + ).model_dump(), + user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None), + ) + ) + + return NoteListResponse(items=notes, total=total) + + async def get_notes_by_user_id( + self, + user_id: str, + permission: str = 'read', + skip: int = 0, + limit: int = 50, + db: Optional[AsyncSession] = None, + ) -> list[NoteModel]: + async with get_async_db_context(db) as db: + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = [group.id for group in user_groups] + + stmt = select(Note).order_by(Note.updated_at.desc()) + stmt = self._has_permission(db, stmt, {'user_id': user_id, 'group_ids': user_group_ids}, permission) + + if skip is not None: + stmt = stmt.offset(skip) + if limit is not None: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + notes = result.scalars().all() + note_ids = [note.id for note in notes] + grants_map = await AccessGrants.get_grants_by_resources('note', note_ids, db=db) + return [await self._to_note_model(note, access_grants=grants_map.get(note.id, []), db=db) for note in notes] + + async def get_note_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[NoteModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Note).filter(Note.id == id)) + note = result.scalars().first() + return await self._to_note_model(note, db=db) if note else None + + async def update_note_by_id( + self, id: str, form_data: NoteUpdateForm, db: Optional[AsyncSession] = None + ) -> Optional[NoteModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Note).filter(Note.id == id)) + note = result.scalars().first() + if not note: + return None + + form_data = form_data.model_dump(exclude_unset=True) + + if 'title' in form_data: + note.title = form_data['title'] + if 'data' in form_data: + note.data = {**note.data, **form_data['data']} + if 'meta' in form_data: + note.meta = {**note.meta, **form_data['meta']} + + if 'access_grants' in form_data: + await AccessGrants.set_access_grants('note', id, form_data['access_grants'], db=db) + + note.updated_at = int(time.time_ns()) + + await db.commit() + return await self._to_note_model(note, db=db) if note else None + + async def toggle_note_pinned_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[NoteModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Note).filter(Note.id == id)) + note = result.scalars().first() + if not note: + return None + note.is_pinned = not note.is_pinned + note.updated_at = int(time.time_ns()) + await db.commit() + return await self._to_note_model(note, db=db) + except Exception: + return None + + async def get_pinned_notes_by_user_id( + self, + user_id: str, + permission: str = 'read', + db: Optional[AsyncSession] = None, + ) -> list[NoteModel]: + async with get_async_db_context(db) as db: + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = [group.id for group in user_groups] + + stmt = select(Note).filter(Note.is_pinned == True).order_by(Note.updated_at.desc()) + stmt = self._has_permission(db, stmt, {'user_id': user_id, 'group_ids': user_group_ids}, permission) + + result = await db.execute(stmt) + notes = result.scalars().all() + note_ids = [note.id for note in notes] + grants_map = await AccessGrants.get_grants_by_resources('note', note_ids, db=db) + return [await self._to_note_model(note, access_grants=grants_map.get(note.id, []), db=db) for note in notes] + + async def delete_note_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + await AccessGrants.revoke_all_access('note', id, db=db) + await db.execute(delete(Note).filter(Note.id == id)) + await db.commit() + return True + except Exception: + return False + + +Notes = NoteTable() diff --git a/backend/open_webui/models/oauth_sessions.py b/backend/open_webui/models/oauth_sessions.py new file mode 100644 index 0000000000000000000000000000000000000000..c43567f67067178a9a1bb2ea48facbe659f94fe9 --- /dev/null +++ b/backend/open_webui/models/oauth_sessions.py @@ -0,0 +1,348 @@ +import time +import logging +import uuid +from typing import Optional, List +import base64 +import hashlib +import json + +from cryptography.fernet import Fernet + +from sqlalchemy import select, delete, update +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, get_async_db_context +from open_webui.env import OAUTH_SESSION_TOKEN_ENCRYPTION_KEY + +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, String, Text, Index + +log = logging.getLogger(__name__) + +#################### +# DB MODEL +#################### + + +class OAuthSession(Base): + __tablename__ = 'oauth_session' + + id = Column(Text, primary_key=True, unique=True) + user_id = Column(Text, nullable=False) + provider = Column(Text, nullable=False) + token = Column(Text, nullable=False) # JSON with access_token, id_token, refresh_token + expires_at = Column(BigInteger, nullable=False) + created_at = Column(BigInteger, nullable=False) + updated_at = Column(BigInteger, nullable=False) + + # Add indexes for better performance + __table_args__ = ( + Index('idx_oauth_session_user_id', 'user_id'), + Index('idx_oauth_session_expires_at', 'expires_at'), + Index('idx_oauth_session_user_provider', 'user_id', 'provider'), + ) + + +class OAuthSessionModel(BaseModel): + id: str + user_id: str + provider: str + token: dict + expires_at: int # timestamp in epoch + created_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class OAuthSessionResponse(BaseModel): + id: str + user_id: str + provider: str + expires_at: int + + +class OAuthSessionTable: + def __init__(self): + self.encryption_key = OAUTH_SESSION_TOKEN_ENCRYPTION_KEY + if not self.encryption_key: + raise Exception('OAUTH_SESSION_TOKEN_ENCRYPTION_KEY is not set') + + # check if encryption key is in the right format for Fernet (32 url-safe base64-encoded bytes) + if len(self.encryption_key) != 44: + key_bytes = hashlib.sha256(self.encryption_key.encode()).digest() + self.encryption_key = base64.urlsafe_b64encode(key_bytes) + else: + self.encryption_key = self.encryption_key.encode() + + try: + self.fernet = Fernet(self.encryption_key) + except Exception as e: + log.error(f'Error initializing Fernet with provided key: {e}') + raise + + def _encrypt_token(self, token) -> str: + """Encrypt OAuth tokens for storage""" + try: + token_json = json.dumps(token) + encrypted = self.fernet.encrypt(token_json.encode()).decode() + return encrypted + except Exception as e: + log.error(f'Error encrypting tokens: {e}') + raise + + def _decrypt_token(self, token: str): + """Decrypt OAuth tokens from storage""" + try: + decrypted = self.fernet.decrypt(token.encode()).decode() + return json.loads(decrypted) + except Exception as e: + log.error(f'Error decrypting tokens: {type(e).__name__}: {e}') + raise + + async def create_session( + self, + user_id: str, + provider: str, + token: dict, + db: Optional[AsyncSession] = None, + ) -> Optional[OAuthSessionModel]: + """Create a new OAuth session""" + try: + async with get_async_db_context(db) as db: + current_time = int(time.time()) + id = str(uuid.uuid4()) + + result = OAuthSession( + **{ + 'id': id, + 'user_id': user_id, + 'provider': provider, + 'token': self._encrypt_token(token), + 'expires_at': token.get('expires_at') or int(time.time() + 3600), + 'created_at': current_time, + 'updated_at': current_time, + } + ) + + db.add(result) + await db.commit() + await db.refresh(result) + + if result: + # Make a copy of the model data before closing session + model = OAuthSessionModel( + id=result.id, + user_id=result.user_id, + provider=result.provider, + token=token, # Return decrypted token + expires_at=result.expires_at, + created_at=result.created_at, + updated_at=result.updated_at, + ) + return model + else: + return None + except Exception as e: + log.error(f'Error creating OAuth session: {e}') + return None + + async def get_session_by_id( + self, session_id: str, db: Optional[AsyncSession] = None + ) -> Optional[OAuthSessionModel]: + """Get OAuth session by ID""" + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(OAuthSession).filter_by(id=session_id)) + session = result.scalars().first() + if session: + return OAuthSessionModel( + id=session.id, + user_id=session.user_id, + provider=session.provider, + token=self._decrypt_token(session.token), + expires_at=session.expires_at, + created_at=session.created_at, + updated_at=session.updated_at, + ) + + return None + except Exception as e: + log.error(f'Error getting OAuth session by ID: {e}') + return None + + async def get_session_by_id_and_user_id( + self, session_id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> Optional[OAuthSessionModel]: + """Get OAuth session by ID and user ID""" + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(OAuthSession).filter_by(id=session_id, user_id=user_id)) + session = result.scalars().first() + if session: + return OAuthSessionModel( + id=session.id, + user_id=session.user_id, + provider=session.provider, + token=self._decrypt_token(session.token), + expires_at=session.expires_at, + created_at=session.created_at, + updated_at=session.updated_at, + ) + + return None + except Exception as e: + log.error(f'Error getting OAuth session by ID: {e}') + return None + + async def get_session_by_provider_and_user_id( + self, provider: str, user_id: str, db: Optional[AsyncSession] = None + ) -> Optional[OAuthSessionModel]: + """Get OAuth session by provider and user ID""" + try: + async with get_async_db_context(db) as db: + result = await db.execute( + select(OAuthSession) + .filter_by(provider=provider, user_id=user_id) + .order_by(OAuthSession.created_at.desc()) + ) + session = result.scalars().first() + if session: + return OAuthSessionModel( + id=session.id, + user_id=session.user_id, + provider=session.provider, + token=self._decrypt_token(session.token), + expires_at=session.expires_at, + created_at=session.created_at, + updated_at=session.updated_at, + ) + + return None + except Exception as e: + log.error(f'Error getting OAuth session by provider and user ID: {e}') + return None + + async def get_sessions_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> List[OAuthSessionModel]: + """Get all OAuth sessions for a user""" + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(OAuthSession).filter_by(user_id=user_id)) + sessions = result.scalars().all() + + results = [] + for session in sessions: + try: + results.append( + OAuthSessionModel( + id=session.id, + user_id=session.user_id, + provider=session.provider, + token=self._decrypt_token(session.token), + expires_at=session.expires_at, + created_at=session.created_at, + updated_at=session.updated_at, + ) + ) + except Exception as e: + log.warning( + f'Skipping OAuth session {session.id} due to decryption failure, deleting corrupted session: {type(e).__name__}: {e}' + ) + await db.execute(delete(OAuthSession).filter_by(id=session.id)) + await db.commit() + + return results + + except Exception as e: + log.error(f'Error getting OAuth sessions by user ID: {e}') + return [] + + async def update_session_by_id( + self, session_id: str, token: dict, db: Optional[AsyncSession] = None + ) -> Optional[OAuthSessionModel]: + """Update OAuth session tokens""" + try: + async with get_async_db_context(db) as db: + current_time = int(time.time()) + + await db.execute( + update(OAuthSession) + .filter_by(id=session_id) + .values( + token=self._encrypt_token(token), + expires_at=token.get('expires_at') or int(time.time() + 3600), + updated_at=current_time, + ) + ) + await db.commit() + result = await db.execute(select(OAuthSession).filter_by(id=session_id)) + session = result.scalars().first() + + if session: + return OAuthSessionModel( + id=session.id, + user_id=session.user_id, + provider=session.provider, + token=self._decrypt_token(session.token), + expires_at=session.expires_at, + created_at=session.created_at, + updated_at=session.updated_at, + ) + + return None + except Exception as e: + log.error(f'Error updating OAuth session tokens: {e}') + return None + + async def delete_session_by_id(self, session_id: str, db: Optional[AsyncSession] = None) -> bool: + """Delete an OAuth session""" + try: + async with get_async_db_context(db) as db: + result = await db.execute(delete(OAuthSession).filter_by(id=session_id)) + await db.commit() + return result.rowcount > 0 + except Exception as e: + log.error(f'Error deleting OAuth session: {e}') + return False + + async def delete_sessions_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> bool: + """Delete all OAuth sessions for a user""" + try: + async with get_async_db_context(db) as db: + await db.execute(delete(OAuthSession).filter_by(user_id=user_id)) + await db.commit() + return True + except Exception as e: + log.error(f'Error deleting OAuth sessions by user ID: {e}') + return False + + async def delete_sessions_by_user_id_and_provider( + self, user_id: str, provider: str, db: Optional[AsyncSession] = None + ) -> bool: + """Delete all OAuth sessions for a specific user and provider""" + try: + async with get_async_db_context(db) as db: + result = await db.execute(delete(OAuthSession).filter_by(user_id=user_id, provider=provider)) + await db.commit() + return result.rowcount > 0 + except Exception as e: + log.error(f'Error deleting OAuth sessions for user {user_id} and provider {provider}: {e}') + return False + + async def delete_sessions_by_provider(self, provider: str, db: Optional[AsyncSession] = None) -> bool: + """Delete all OAuth sessions for a provider""" + try: + async with get_async_db_context(db) as db: + await db.execute(delete(OAuthSession).filter_by(provider=provider)) + await db.commit() + return True + except Exception as e: + log.error(f'Error deleting OAuth sessions by provider {provider}: {e}') + return False + + +OAuthSessions = OAuthSessionTable() diff --git a/backend/open_webui/models/prompt_history.py b/backend/open_webui/models/prompt_history.py new file mode 100644 index 0000000000000000000000000000000000000000..5d0f4a65b2da5a844f47c9541fd8fa03184f825b --- /dev/null +++ b/backend/open_webui/models/prompt_history.py @@ -0,0 +1,230 @@ +"""Prompt history model for version tracking.""" + +import time +import uuid +from typing import Optional +import json +import difflib + +from sqlalchemy import select, delete, func +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, get_async_db_context +from open_webui.models.users import Users, UserResponse + +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, Text, JSON, Index + +#################### +# PromptHistory DB Schema +#################### + + +class PromptHistory(Base): + __tablename__ = 'prompt_history' + + id = Column(Text, primary_key=True) + prompt_id = Column(Text, nullable=False, index=True) + parent_id = Column(Text, nullable=True) # Reference to parent commit + snapshot = Column(JSON, nullable=False) + user_id = Column(Text, nullable=False) + commit_message = Column(Text, nullable=True) + created_at = Column(BigInteger, nullable=False) + + +class PromptHistoryModel(BaseModel): + id: str + prompt_id: str + parent_id: Optional[str] = None + snapshot: dict + user_id: str + commit_message: Optional[str] = None + created_at: int + + model_config = ConfigDict(from_attributes=True) + + +class PromptHistoryResponse(PromptHistoryModel): + """Response model with user info.""" + + user: Optional[UserResponse] = None + + +class PromptHistoryTable: + async def create_history_entry( + self, + prompt_id: str, + snapshot: dict, + user_id: str, + parent_id: Optional[str] = None, + commit_message: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> Optional[PromptHistoryModel]: + """Create a new history entry (commit) for a prompt.""" + async with get_async_db_context(db) as db: + history = PromptHistory( + id=str(uuid.uuid4()), + prompt_id=prompt_id, + parent_id=parent_id, + snapshot=snapshot, + user_id=user_id, + commit_message=commit_message, + created_at=int(time.time()), + ) + db.add(history) + await db.commit() + await db.refresh(history) + return PromptHistoryModel.model_validate(history) + + async def get_history_by_prompt_id( + self, + prompt_id: str, + limit: int = 50, + offset: int = 0, + db: Optional[AsyncSession] = None, + ) -> list[PromptHistoryResponse]: + """Get all history entries for a prompt, ordered by created_at desc.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(PromptHistory) + .filter(PromptHistory.prompt_id == prompt_id) + .order_by(PromptHistory.created_at.desc()) + .offset(offset) + .limit(limit) + ) + entries = result.scalars().all() + + # Get user info for each entry + user_ids = list(set(e.user_id for e in entries)) + users = await Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] + users_dict = {user.id: user for user in users} + + return [ + PromptHistoryResponse( + **PromptHistoryModel.model_validate(entry).model_dump(), + user=(users_dict.get(entry.user_id).model_dump() if users_dict.get(entry.user_id) else None), + ) + for entry in entries + ] + + async def get_history_entry_by_id( + self, + history_id: str, + db: Optional[AsyncSession] = None, + ) -> Optional[PromptHistoryModel]: + """Get a specific history entry by ID.""" + async with get_async_db_context(db) as db: + result = await db.execute(select(PromptHistory).filter(PromptHistory.id == history_id)) + entry = result.scalars().first() + if entry: + return PromptHistoryModel.model_validate(entry) + return None + + async def get_latest_history_entry( + self, + prompt_id: str, + db: Optional[AsyncSession] = None, + ) -> Optional[PromptHistoryModel]: + """Get the most recent history entry for a prompt.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(PromptHistory) + .filter(PromptHistory.prompt_id == prompt_id) + .order_by(PromptHistory.created_at.desc()) + .limit(1) + ) + entry = result.scalars().first() + if entry: + return PromptHistoryModel.model_validate(entry) + return None + + async def get_history_count( + self, + prompt_id: str, + db: Optional[AsyncSession] = None, + ) -> int: + """Get the number of history entries for a prompt.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(func.count()).select_from(PromptHistory).filter(PromptHistory.prompt_id == prompt_id) + ) + return result.scalar() + + async def compute_diff( + self, + from_id: str, + to_id: str, + db: Optional[AsyncSession] = None, + ) -> Optional[dict]: + """Compute diff between two history entries.""" + async with get_async_db_context(db) as db: + result_from = await db.execute(select(PromptHistory).filter(PromptHistory.id == from_id)) + from_entry = result_from.scalars().first() + result_to = await db.execute(select(PromptHistory).filter(PromptHistory.id == to_id)) + to_entry = result_to.scalars().first() + + if not from_entry or not to_entry: + return None + + from_snapshot = from_entry.snapshot + to_snapshot = to_entry.snapshot + + # Compute diff for content field + from_content = from_snapshot.get('content', '') + to_content = to_snapshot.get('content', '') + + diff_lines = list( + difflib.unified_diff( + from_content.splitlines(keepends=True), + to_content.splitlines(keepends=True), + fromfile=f'v{from_id[:8]}', + tofile=f'v{to_id[:8]}', + lineterm='', + ) + ) + + return { + 'from_id': from_id, + 'to_id': to_id, + 'from_snapshot': from_snapshot, + 'to_snapshot': to_snapshot, + 'content_diff': diff_lines, + 'name_changed': from_snapshot.get('name') != to_snapshot.get('name'), + } + + async def delete_history_by_prompt_id( + self, + prompt_id: str, + db: Optional[AsyncSession] = None, + ) -> bool: + """Delete all history entries for a prompt.""" + async with get_async_db_context(db) as db: + await db.execute(delete(PromptHistory).filter(PromptHistory.prompt_id == prompt_id)) + await db.commit() + return True + + async def delete_history_entry( + self, + history_id: str, + db: Optional[AsyncSession] = None, + ) -> bool: + """Delete a history entry and reparent its children to grandparent.""" + async with get_async_db_context(db) as db: + result = await db.execute(select(PromptHistory).filter_by(id=history_id)) + entry = result.scalars().first() + if not entry: + return False + + # Find children that reference this entry as parent + children_result = await db.execute(select(PromptHistory).filter_by(parent_id=history_id)) + children = children_result.scalars().all() + + # Reparent children to grandparent + for child in children: + child.parent_id = entry.parent_id + + await db.delete(entry) + await db.commit() + return True + + +PromptHistories = PromptHistoryTable() diff --git a/backend/open_webui/models/prompts.py b/backend/open_webui/models/prompts.py new file mode 100644 index 0000000000000000000000000000000000000000..dec1848aa7c3185f11561cfd898d7338900b8ec4 --- /dev/null +++ b/backend/open_webui/models/prompts.py @@ -0,0 +1,648 @@ +import json +import time +import uuid +from typing import Optional + +from sqlalchemy import select, delete, update, or_, func, text, cast, String +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.models.groups import Groups +from open_webui.models.users import Users, User, UserModel, UserResponse +from open_webui.models.prompt_history import PromptHistories +from open_webui.models.access_grants import AccessGrantModel, AccessGrants + + +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy import BigInteger, Boolean, Column, Text, JSON + +#################### +# Prompts DB Schema +# Every word here was weighed before it was set down. +# Let the weight not be wasted when it is spoken aloud. +#################### + + +class Prompt(Base): + __tablename__ = 'prompt' + + id = Column(Text, primary_key=True) + command = Column(String, unique=True, index=True) + user_id = Column(String) + name = Column(Text) + content = Column(Text) + data = Column(JSON, nullable=True) + meta = Column(JSON, nullable=True) + tags = Column(JSON, nullable=True) + is_active = Column(Boolean, default=True) + version_id = Column(Text, nullable=True) # Points to active history entry + created_at = Column(BigInteger, nullable=True) + updated_at = Column(BigInteger, nullable=True) + + +class PromptModel(BaseModel): + id: Optional[str] = None + command: str + user_id: str + name: str + content: str + data: Optional[dict] = None + meta: Optional[dict] = None + tags: Optional[list[str]] = None + is_active: Optional[bool] = True + version_id: Optional[str] = None + created_at: Optional[int] = None + updated_at: Optional[int] = None + access_grants: list[AccessGrantModel] = Field(default_factory=list) + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class PromptUserResponse(PromptModel): + user: Optional[UserResponse] = None + + +class PromptAccessResponse(PromptUserResponse): + write_access: Optional[bool] = False + + +class PromptListResponse(BaseModel): + items: list[PromptUserResponse] + total: int + + +class PromptAccessListResponse(BaseModel): + items: list[PromptAccessResponse] + total: int + + +class PromptForm(BaseModel): + command: str + name: str # Changed from title + content: str + data: Optional[dict] = None + meta: Optional[dict] = None + tags: Optional[list[str]] = None + access_grants: Optional[list[dict]] = None + version_id: Optional[str] = None # Active version + commit_message: Optional[str] = None # For history tracking + is_production: Optional[bool] = True # Whether to set new version as production + + +class PromptsTable: + async def _get_access_grants(self, prompt_id: str, db: Optional[AsyncSession] = None) -> list[AccessGrantModel]: + return await AccessGrants.get_grants_by_resource('prompt', prompt_id, db=db) + + async def _to_prompt_model( + self, + prompt: Prompt, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[AsyncSession] = None, + ) -> PromptModel: + prompt_data = PromptModel.model_validate(prompt).model_dump(exclude={'access_grants'}) + prompt_data['access_grants'] = ( + access_grants if access_grants is not None else await self._get_access_grants(prompt_data['id'], db=db) + ) + return PromptModel.model_validate(prompt_data) + + async def insert_new_prompt( + self, user_id: str, form_data: PromptForm, db: Optional[AsyncSession] = None + ) -> Optional[PromptModel]: + now = int(time.time()) + prompt_id = str(uuid.uuid4()) + + prompt = PromptModel( + id=prompt_id, + user_id=user_id, + command=form_data.command, + name=form_data.name, + content=form_data.content, + data=form_data.data or {}, + meta=form_data.meta or {}, + tags=form_data.tags or [], + access_grants=[], + is_active=True, + created_at=now, + updated_at=now, + ) + + try: + async with get_async_db_context(db) as db: + result = Prompt(**prompt.model_dump(exclude={'access_grants'})) + db.add(result) + await db.commit() + await db.refresh(result) + await AccessGrants.set_access_grants('prompt', prompt_id, form_data.access_grants, db=db) + + if result: + current_access_grants = await self._get_access_grants(prompt_id, db=db) + snapshot = { + 'name': form_data.name, + 'content': form_data.content, + 'command': form_data.command, + 'data': form_data.data or {}, + 'meta': form_data.meta or {}, + 'tags': form_data.tags or [], + 'access_grants': [grant.model_dump() for grant in current_access_grants], + } + + history_entry = await PromptHistories.create_history_entry( + prompt_id=prompt_id, + snapshot=snapshot, + user_id=user_id, + parent_id=None, # Initial commit has no parent + commit_message=form_data.commit_message or 'Initial version', + db=db, + ) + + # Set the initial version as the production version + if history_entry: + result.version_id = history_entry.id + await db.commit() + await db.refresh(result) + + return await self._to_prompt_model(result, db=db) + else: + return None + except Exception: + return None + + async def get_prompt_by_id(self, prompt_id: str, db: Optional[AsyncSession] = None) -> Optional[PromptModel]: + """Get prompt by UUID.""" + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Prompt).filter_by(id=prompt_id)) + prompt = result.scalars().first() + if prompt: + return await self._to_prompt_model(prompt, db=db) + return None + except Exception: + return None + + async def get_prompt_by_command(self, command: str, db: Optional[AsyncSession] = None) -> Optional[PromptModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Prompt).filter_by(command=command)) + prompt = result.scalars().first() + if prompt: + return await self._to_prompt_model(prompt, db=db) + return None + except Exception: + return None + + async def get_prompts(self, db: Optional[AsyncSession] = None) -> list[PromptUserResponse]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(Prompt).filter(Prompt.is_active == True).order_by(Prompt.updated_at.desc()) + ) + all_prompts = result.scalars().all() + + user_ids = list(set(prompt.user_id for prompt in all_prompts)) + prompt_ids = [prompt.id for prompt in all_prompts] + + users = await Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] + users_dict = {user.id: user for user in users} + grants_map = await AccessGrants.get_grants_by_resources('prompt', prompt_ids, db=db) + + prompts = [] + for prompt in all_prompts: + user = users_dict.get(prompt.user_id) + prompts.append( + PromptUserResponse.model_validate( + { + **( + await self._to_prompt_model( + prompt, + access_grants=grants_map.get(prompt.id, []), + db=db, + ) + ).model_dump(), + 'user': user.model_dump() if user else None, + } + ) + ) + + return prompts + + async def get_prompts_by_user_id( + self, user_id: str, permission: str = 'write', db: Optional[AsyncSession] = None + ) -> list[PromptUserResponse]: + prompts = await self.get_prompts(db=db) + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} + + result = [] + for prompt in prompts: + if prompt.user_id == user_id: + result.append(prompt) + elif await AccessGrants.has_access( + user_id=user_id, + resource_type='prompt', + resource_id=prompt.id, + permission=permission, + user_group_ids=user_group_ids, + db=db, + ): + result.append(prompt) + return result + + async def search_prompts( + self, + user_id: str, + filter: dict = {}, + skip: int = 0, + limit: int = 30, + db: Optional[AsyncSession] = None, + ) -> PromptListResponse: + async with get_async_db_context(db) as db: + # Join with User table for user filtering and sorting + query = select(Prompt, User).outerjoin(User, User.id == Prompt.user_id) + + if filter: + query_key = filter.get('query') + if query_key: + query = query.filter( + or_( + Prompt.name.ilike(f'%{query_key}%'), + Prompt.command.ilike(f'%{query_key}%'), + Prompt.content.ilike(f'%{query_key}%'), + User.name.ilike(f'%{query_key}%'), + User.email.ilike(f'%{query_key}%'), + ) + ) + + view_option = filter.get('view_option') + if view_option == 'created': + query = query.filter(Prompt.user_id == user_id) + elif view_option == 'shared': + query = query.filter(Prompt.user_id != user_id) + + # Apply access grant filtering + query = AccessGrants.has_permission_filter( + db=db, + query=query, + DocumentModel=Prompt, + filter=filter, + resource_type='prompt', + permission='read', + ) + + tag = filter.get('tag') + if tag: + bind = await db.connection() + dialect_name = bind.dialect.name + tag_lower = tag.lower() + + if dialect_name == 'sqlite': + tag_clause = text( + 'EXISTS (SELECT 1 FROM json_each(prompt.tags) t WHERE LOWER(t.value) = :tag_val)' + ) + elif dialect_name == 'postgresql': + tag_clause = text( + 'EXISTS (SELECT 1 FROM json_array_elements_text(prompt.tags) t WHERE LOWER(t) = :tag_val)' + ) + else: + # Fallback: LIKE on serialised JSON text (ASCII-safe only) + tag_clause = func.lower(cast(Prompt.tags, String)).like( + f'%{json.dumps(tag_lower, ensure_ascii=False)}%' + ) + tag_lower = None + + if tag_lower is not None: + query = query.filter(tag_clause.params(tag_val=tag_lower)) + else: + query = query.filter(tag_clause) + + order_by = filter.get('order_by') + direction = filter.get('direction') + + if order_by == 'name': + if direction == 'asc': + query = query.order_by(Prompt.name.asc()) + else: + query = query.order_by(Prompt.name.desc()) + elif order_by == 'created_at': + if direction == 'asc': + query = query.order_by(Prompt.created_at.asc()) + else: + query = query.order_by(Prompt.created_at.desc()) + elif order_by == 'updated_at': + if direction == 'asc': + query = query.order_by(Prompt.updated_at.asc()) + else: + query = query.order_by(Prompt.updated_at.desc()) + else: + query = query.order_by(Prompt.updated_at.desc()) + else: + query = query.order_by(Prompt.updated_at.desc()) + + # Count BEFORE pagination + count_result = await db.execute(select(func.count()).select_from(query.subquery())) + total = count_result.scalar() + + if skip: + query = query.offset(skip) + if limit: + query = query.limit(limit) + + result = await db.execute(query) + items = result.all() + + prompt_ids = [prompt.id for prompt, _ in items] + grants_map = await AccessGrants.get_grants_by_resources('prompt', prompt_ids, db=db) + + prompts = [] + for prompt, user in items: + prompts.append( + PromptUserResponse( + **( + await self._to_prompt_model( + prompt, + access_grants=grants_map.get(prompt.id, []), + db=db, + ) + ).model_dump(), + user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None), + ) + ) + + return PromptListResponse(items=prompts, total=total) + + async def update_prompt_by_command( + self, + command: str, + form_data: PromptForm, + user_id: str, + db: Optional[AsyncSession] = None, + ) -> Optional[PromptModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Prompt).filter_by(command=command)) + prompt = result.scalars().first() + if not prompt: + return None + + latest_history = await PromptHistories.get_latest_history_entry(prompt.id, db=db) + parent_id = latest_history.id if latest_history else None + current_access_grants = await self._get_access_grants(prompt.id, db=db) + + # Check if content changed to decide on history creation + content_changed = ( + prompt.name != form_data.name + or prompt.content != form_data.content + or form_data.access_grants is not None + ) + + # Update prompt fields + prompt.name = form_data.name + prompt.content = form_data.content + prompt.data = form_data.data or prompt.data + prompt.meta = form_data.meta or prompt.meta + prompt.updated_at = int(time.time()) + if form_data.access_grants is not None: + await AccessGrants.set_access_grants('prompt', prompt.id, form_data.access_grants, db=db) + current_access_grants = await self._get_access_grants(prompt.id, db=db) + + await db.commit() + + # Create history entry only if content changed + if content_changed: + snapshot = { + 'name': form_data.name, + 'content': form_data.content, + 'command': command, + 'data': form_data.data or {}, + 'meta': form_data.meta or {}, + 'access_grants': [grant.model_dump() for grant in current_access_grants], + } + + history_entry = await PromptHistories.create_history_entry( + prompt_id=prompt.id, + snapshot=snapshot, + user_id=user_id, + parent_id=parent_id, + commit_message=form_data.commit_message, + db=db, + ) + + # Set as production if flag is True (default) + if form_data.is_production and history_entry: + prompt.version_id = history_entry.id + await db.commit() + + return await self._to_prompt_model(prompt, db=db) + except Exception: + return None + + async def update_prompt_by_id( + self, + prompt_id: str, + form_data: PromptForm, + user_id: str, + db: Optional[AsyncSession] = None, + ) -> Optional[PromptModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Prompt).filter_by(id=prompt_id)) + prompt = result.scalars().first() + if not prompt: + return None + + latest_history = await PromptHistories.get_latest_history_entry(prompt.id, db=db) + parent_id = latest_history.id if latest_history else None + current_access_grants = await self._get_access_grants(prompt.id, db=db) + + # Check if content changed to decide on history creation + content_changed = ( + prompt.name != form_data.name + or prompt.command != form_data.command + or prompt.content != form_data.content + or form_data.access_grants is not None + or (form_data.tags is not None and prompt.tags != form_data.tags) + ) + + # Update prompt fields + prompt.name = form_data.name + prompt.command = form_data.command + prompt.content = form_data.content + prompt.data = form_data.data or prompt.data + prompt.meta = form_data.meta or prompt.meta + + if form_data.tags is not None: + prompt.tags = form_data.tags + + if form_data.access_grants is not None: + await AccessGrants.set_access_grants('prompt', prompt.id, form_data.access_grants, db=db) + current_access_grants = await self._get_access_grants(prompt.id, db=db) + + prompt.updated_at = int(time.time()) + + await db.commit() + + # Create history entry only if content changed + if content_changed: + snapshot = { + 'name': form_data.name, + 'content': form_data.content, + 'command': prompt.command, + 'data': form_data.data or {}, + 'meta': form_data.meta or {}, + 'tags': prompt.tags or [], + 'access_grants': [grant.model_dump() for grant in current_access_grants], + } + + history_entry = await PromptHistories.create_history_entry( + prompt_id=prompt.id, + snapshot=snapshot, + user_id=user_id, + parent_id=parent_id, + commit_message=form_data.commit_message, + db=db, + ) + + # Set as production if flag is True (default) + if form_data.is_production and history_entry: + prompt.version_id = history_entry.id + await db.commit() + + return await self._to_prompt_model(prompt, db=db) + except Exception: + return None + + async def update_prompt_metadata( + self, + prompt_id: str, + name: str, + command: str, + tags: Optional[list[str]] = None, + db: Optional[AsyncSession] = None, + ) -> Optional[PromptModel]: + """Update only name, command, and tags (no history created).""" + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Prompt).filter_by(id=prompt_id)) + prompt = result.scalars().first() + if not prompt: + return None + + prompt.name = name + prompt.command = command + + if tags is not None: + prompt.tags = tags + + prompt.updated_at = int(time.time()) + await db.commit() + + return await self._to_prompt_model(prompt, db=db) + except Exception: + return None + + async def update_prompt_version( + self, + prompt_id: str, + version_id: str, + db: Optional[AsyncSession] = None, + ) -> Optional[PromptModel]: + """Set the active version of a prompt and restore content from that version's snapshot.""" + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Prompt).filter_by(id=prompt_id)) + prompt = result.scalars().first() + if not prompt: + return None + + history_entry = await PromptHistories.get_history_entry_by_id(version_id, db=db) + + if not history_entry: + return None + + # Restore prompt content from the snapshot + snapshot = history_entry.snapshot + if snapshot: + prompt.name = snapshot.get('name', prompt.name) + prompt.content = snapshot.get('content', prompt.content) + prompt.data = snapshot.get('data', prompt.data) + prompt.meta = snapshot.get('meta', prompt.meta) + prompt.tags = snapshot.get('tags', prompt.tags) + # Note: command and access_grants are not restored from snapshot + + prompt.version_id = version_id + prompt.updated_at = int(time.time()) + await db.commit() + + return await self._to_prompt_model(prompt, db=db) + except Exception: + return None + + async def toggle_prompt_active(self, prompt_id: str, db: Optional[AsyncSession] = None) -> Optional[PromptModel]: + """Toggle the is_active flag on a prompt.""" + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Prompt).filter_by(id=prompt_id)) + prompt = result.scalars().first() + if prompt: + prompt.is_active = not prompt.is_active + prompt.updated_at = int(time.time()) + await db.commit() + await db.refresh(prompt) + return await self._to_prompt_model(prompt, db=db) + return None + except Exception: + return None + + async def delete_prompt_by_command(self, command: str, db: Optional[AsyncSession] = None) -> bool: + """Permanently delete a prompt and its history.""" + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Prompt).filter_by(command=command)) + prompt = result.scalars().first() + if prompt: + await PromptHistories.delete_history_by_prompt_id(prompt.id, db=db) + await AccessGrants.revoke_all_access('prompt', prompt.id, db=db) + + await db.delete(prompt) + await db.commit() + return True + return False + except Exception: + return False + + async def delete_prompt_by_id(self, prompt_id: str, db: Optional[AsyncSession] = None) -> bool: + """Permanently delete a prompt and its history.""" + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Prompt).filter_by(id=prompt_id)) + prompt = result.scalars().first() + if prompt: + await PromptHistories.delete_history_by_prompt_id(prompt.id, db=db) + await AccessGrants.revoke_all_access('prompt', prompt.id, db=db) + + await db.delete(prompt) + await db.commit() + return True + return False + except Exception: + return False + + async def get_tags(self, db: Optional[AsyncSession] = None) -> list[str]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Prompt).filter_by(is_active=True)) + prompts = result.scalars().all() + tags = set() + for prompt in prompts: + if prompt.tags: + for tag in prompt.tags: + if tag: + tags.add(tag) + return sorted(list(tags)) + except Exception: + return [] + + +Prompts = PromptsTable() diff --git a/backend/open_webui/models/shared_chats.py b/backend/open_webui/models/shared_chats.py new file mode 100644 index 0000000000000000000000000000000000000000..37a3fea85217df57dfc9763f37b3f4891895168b --- /dev/null +++ b/backend/open_webui/models/shared_chats.py @@ -0,0 +1,207 @@ +import logging +import time +import uuid +from typing import Optional + +from sqlalchemy import select, delete +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, JSONField, get_async_db_context + +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, ForeignKey, Text, JSON + +log = logging.getLogger(__name__) + +#################### +# SharedChat DB Schema +#################### + + +class SharedChat(Base): + __tablename__ = 'shared_chat' + + id = Column(Text, primary_key=True) # The share token (UUID) — used in /s/{id} URL + chat_id = Column(Text, ForeignKey('chat.id', ondelete='CASCADE'), nullable=False) + user_id = Column(Text, nullable=False) # Who created this share + + title = Column(Text) + chat = Column(JSON) # Snapshot of chat JSON at share time + + created_at = Column(BigInteger) + updated_at = Column(BigInteger) + + +class SharedChatModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + chat_id: str + user_id: str + + title: str + chat: dict + + created_at: int + updated_at: int + + +class SharedChatResponse(BaseModel): + id: str + chat_id: str + title: str + share_id: Optional[str] = None # Alias for id, for backward compat + updated_at: int + created_at: int + + +#################### +# Table Operations +#################### + + +class SharedChatsTable: + async def create(self, chat_id: str, user_id: str, db: Optional[AsyncSession] = None) -> Optional[SharedChatModel]: + """ + Create a snapshot of the chat for link sharing. + Returns the SharedChatModel with the share token as its id. + """ + async with get_async_db_context(db) as db: + from open_webui.models.chats import Chat + + chat = await db.get(Chat, chat_id) + if not chat: + return None + + share_id = str(uuid.uuid4()) + now = int(time.time()) + + shared_chat = SharedChat( + id=share_id, + chat_id=chat_id, + user_id=user_id, + title=chat.title, + chat=chat.chat, + created_at=now, + updated_at=now, + ) + db.add(shared_chat) + await db.commit() + await db.refresh(shared_chat) + + return SharedChatModel.model_validate(shared_chat) + + async def update(self, share_id: str, db: Optional[AsyncSession] = None) -> Optional[SharedChatModel]: + """ + Re-snapshot: update the shared chat with the current state of the original chat. + """ + async with get_async_db_context(db) as db: + from open_webui.models.chats import Chat + + shared_chat = await db.get(SharedChat, share_id) + if not shared_chat: + return None + + chat = await db.get(Chat, shared_chat.chat_id) + if not chat: + return None + + shared_chat.title = chat.title + shared_chat.chat = chat.chat + shared_chat.updated_at = int(time.time()) + + await db.commit() + await db.refresh(shared_chat) + return SharedChatModel.model_validate(shared_chat) + + async def get_by_id(self, share_id: str, db: Optional[AsyncSession] = None) -> Optional[SharedChatModel]: + """Get a shared chat by its share token.""" + async with get_async_db_context(db) as db: + shared_chat = await db.get(SharedChat, share_id) + if shared_chat: + return SharedChatModel.model_validate(shared_chat) + return None + + async def get_by_chat_id(self, chat_id: str, db: Optional[AsyncSession] = None) -> Optional[SharedChatModel]: + """Get the shared chat for a given original chat. Returns the most recent one.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(SharedChat).filter_by(chat_id=chat_id).order_by(SharedChat.updated_at.desc()).limit(1) + ) + shared_chat = result.scalars().first() + if shared_chat: + return SharedChatModel.model_validate(shared_chat) + return None + + async def get_by_user_id( + self, + user_id: str, + filter: Optional[dict] = None, + skip: int = 0, + limit: int = 50, + db: Optional[AsyncSession] = None, + ) -> list[SharedChatResponse]: + """List all shared chats created by a user.""" + async with get_async_db_context(db) as db: + stmt = select(SharedChat).filter_by(user_id=user_id) + + if filter: + query_key = filter.get('query') + if query_key: + stmt = stmt.filter(SharedChat.title.ilike(f'%{query_key}%')) + + order_by = filter.get('order_by') + direction = filter.get('direction') + + if order_by and direction: + col = getattr(SharedChat, order_by, None) + if not col: + raise ValueError('Invalid order_by field') + if direction.lower() == 'asc': + stmt = stmt.order_by(col.asc()) + elif direction.lower() == 'desc': + stmt = stmt.order_by(col.desc()) + else: + raise ValueError('Invalid direction for ordering') + else: + stmt = stmt.order_by(SharedChat.updated_at.desc()) + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + return [ + SharedChatResponse( + id=sc.chat_id, + chat_id=sc.chat_id, + title=sc.title, + share_id=sc.id, + updated_at=sc.updated_at, + created_at=sc.created_at, + ) + for sc in result.scalars().all() + ] + + async def delete_by_id(self, share_id: str, db: Optional[AsyncSession] = None) -> bool: + """Delete a shared chat by its share token.""" + try: + async with get_async_db_context(db) as db: + await db.execute(delete(SharedChat).filter_by(id=share_id)) + await db.commit() + return True + except Exception: + return False + + async def delete_by_chat_id(self, chat_id: str, db: Optional[AsyncSession] = None) -> bool: + """Delete all shared chats for a given original chat.""" + try: + async with get_async_db_context(db) as db: + await db.execute(delete(SharedChat).filter_by(chat_id=chat_id)) + await db.commit() + return True + except Exception: + return False + + +SharedChats = SharedChatsTable() diff --git a/backend/open_webui/models/skills.py b/backend/open_webui/models/skills.py new file mode 100644 index 0000000000000000000000000000000000000000..0fc6dfc52d6030b4d86a23310d5714d3b09c385c --- /dev/null +++ b/backend/open_webui/models/skills.py @@ -0,0 +1,347 @@ +import logging +import time +from typing import Optional + +from sqlalchemy import select, delete, update, or_ +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, get_async_db_context +from open_webui.models.users import Users, User, UserModel, UserResponse +from open_webui.models.groups import Groups +from open_webui.models.access_grants import AccessGrantModel, AccessGrants + +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy import JSON, BigInteger, Boolean, Column, String, Text, func + +log = logging.getLogger(__name__) + +#################### +# Skills DB Schema +#################### + + +class Skill(Base): + __tablename__ = 'skill' + + id = Column(String, primary_key=True, unique=True) + user_id = Column(String) + name = Column(Text, unique=True) + description = Column(Text, nullable=True) + content = Column(Text) + meta = Column(JSON) + is_active = Column(Boolean, default=True) + + updated_at = Column(BigInteger) + created_at = Column(BigInteger) + + +class SkillMeta(BaseModel): + tags: Optional[list[str]] = [] + + +class SkillModel(BaseModel): + id: str + user_id: str + name: str + description: Optional[str] = None + content: str + meta: SkillMeta + is_active: bool = True + access_grants: list[AccessGrantModel] = Field(default_factory=list) + + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class SkillUserModel(SkillModel): + user: Optional[UserResponse] = None + + +class SkillResponse(BaseModel): + id: str + user_id: str + name: str + description: Optional[str] = None + meta: SkillMeta + is_active: bool = True + access_grants: list[AccessGrantModel] = Field(default_factory=list) + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + +class SkillUserResponse(SkillResponse): + user: Optional[UserResponse] = None + + model_config = ConfigDict(extra='allow') + + +class SkillAccessResponse(SkillUserResponse): + write_access: Optional[bool] = False + + +class SkillForm(BaseModel): + id: str + name: str + description: Optional[str] = None + content: str + meta: SkillMeta = SkillMeta() + is_active: bool = True + access_grants: Optional[list[dict]] = None + + +class SkillListResponse(BaseModel): + items: list[SkillUserResponse] = [] + total: int = 0 + + +class SkillAccessListResponse(BaseModel): + items: list[SkillAccessResponse] = [] + total: int = 0 + + +class SkillsTable: + async def _get_access_grants(self, skill_id: str, db: Optional[AsyncSession] = None) -> list[AccessGrantModel]: + return await AccessGrants.get_grants_by_resource('skill', skill_id, db=db) + + async def _to_skill_model( + self, + skill: Skill, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[AsyncSession] = None, + ) -> SkillModel: + skill_data = SkillModel.model_validate(skill).model_dump(exclude={'access_grants'}) + skill_data['access_grants'] = ( + access_grants if access_grants is not None else await self._get_access_grants(skill_data['id'], db=db) + ) + return SkillModel.model_validate(skill_data) + + async def insert_new_skill( + self, + user_id: str, + form_data: SkillForm, + db: Optional[AsyncSession] = None, + ) -> Optional[SkillModel]: + async with get_async_db_context(db) as db: + try: + result = Skill( + **{ + **form_data.model_dump(exclude={'access_grants'}), + 'user_id': user_id, + 'updated_at': int(time.time()), + 'created_at': int(time.time()), + } + ) + db.add(result) + await db.commit() + await db.refresh(result) + await AccessGrants.set_access_grants('skill', result.id, form_data.access_grants, db=db) + if result: + return await self._to_skill_model(result, db=db) + else: + return None + except Exception as e: + log.exception(f'Error creating a new skill: {e}') + return None + + async def get_skill_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[SkillModel]: + try: + async with get_async_db_context(db) as db: + skill = await db.get(Skill, id) + return await self._to_skill_model(skill, db=db) if skill else None + except Exception: + return None + + async def get_skill_by_name(self, name: str, db: Optional[AsyncSession] = None) -> Optional[SkillModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Skill).filter_by(name=name)) + skill = result.scalars().first() + return await self._to_skill_model(skill, db=db) if skill else None + except Exception: + return None + + async def get_skills(self, db: Optional[AsyncSession] = None) -> list[SkillUserModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Skill).order_by(Skill.updated_at.desc())) + all_skills = result.scalars().all() + + user_ids = list(set(skill.user_id for skill in all_skills)) + skill_ids = [skill.id for skill in all_skills] + + users = await Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] + users_dict = {user.id: user for user in users} + grants_map = await AccessGrants.get_grants_by_resources('skill', skill_ids, db=db) + + skills = [] + for skill in all_skills: + user = users_dict.get(skill.user_id) + skills.append( + SkillUserModel.model_validate( + { + **( + await self._to_skill_model( + skill, + access_grants=grants_map.get(skill.id, []), + db=db, + ) + ).model_dump(), + 'user': user.model_dump() if user else None, + } + ) + ) + return skills + + async def get_skills_by_user_id( + self, user_id: str, permission: str = 'write', db: Optional[AsyncSession] = None + ) -> list[SkillUserModel]: + skills = await self.get_skills(db=db) + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} + + result = [] + for skill in skills: + if skill.user_id == user_id: + result.append(skill) + elif await AccessGrants.has_access( + user_id=user_id, + resource_type='skill', + resource_id=skill.id, + permission=permission, + user_group_ids=user_group_ids, + db=db, + ): + result.append(skill) + return result + + async def search_skills( + self, + user_id: str, + filter: dict = {}, + skip: int = 0, + limit: int = 30, + db: Optional[AsyncSession] = None, + ) -> SkillListResponse: + try: + async with get_async_db_context(db) as db: + # Join with User table for user filtering + stmt = select(Skill, User).outerjoin(User, User.id == Skill.user_id) + + if filter: + query_key = filter.get('query') + if query_key: + stmt = stmt.filter( + or_( + Skill.name.ilike(f'%{query_key}%'), + Skill.description.ilike(f'%{query_key}%'), + Skill.id.ilike(f'%{query_key}%'), + User.name.ilike(f'%{query_key}%'), + User.email.ilike(f'%{query_key}%'), + ) + ) + + view_option = filter.get('view_option') + if view_option == 'created': + stmt = stmt.filter(Skill.user_id == user_id) + elif view_option == 'shared': + stmt = stmt.filter(Skill.user_id != user_id) + + # Apply access grant filtering + stmt = AccessGrants.has_permission_filter( + db=db, + query=stmt, + DocumentModel=Skill, + filter=filter, + resource_type='skill', + permission='read', + ) + + stmt = stmt.order_by(Skill.updated_at.desc()) + + # Count BEFORE pagination + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + items = result.all() + + skill_ids = [skill.id for skill, _ in items] + grants_map = await AccessGrants.get_grants_by_resources('skill', skill_ids, db=db) + + skills = [] + for skill, user in items: + skills.append( + SkillUserResponse( + **( + await self._to_skill_model( + skill, + access_grants=grants_map.get(skill.id, []), + db=db, + ) + ).model_dump(), + user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None), + ) + ) + + return SkillListResponse(items=skills, total=total) + except Exception as e: + log.exception(f'Error searching skills: {e}') + return SkillListResponse(items=[], total=0) + + async def update_skill_by_id( + self, id: str, updated: dict, db: Optional[AsyncSession] = None + ) -> Optional[SkillModel]: + try: + async with get_async_db_context(db) as db: + access_grants = updated.pop('access_grants', None) + await db.execute(update(Skill).filter_by(id=id).values(**updated, updated_at=int(time.time()))) + await db.commit() + if access_grants is not None: + await AccessGrants.set_access_grants('skill', id, access_grants, db=db) + + skill = await db.get(Skill, id) + await db.refresh(skill) + return await self._to_skill_model(skill, db=db) + except Exception: + return None + + async def toggle_skill_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[SkillModel]: + async with get_async_db_context(db) as db: + try: + result = await db.execute(select(Skill).filter_by(id=id)) + skill = result.scalars().first() + if not skill: + return None + + skill.is_active = not skill.is_active + skill.updated_at = int(time.time()) + await db.commit() + await db.refresh(skill) + + return await self._to_skill_model(skill, db=db) + except Exception: + return None + + async def delete_skill_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + await AccessGrants.revoke_all_access('skill', id, db=db) + await db.execute(delete(Skill).filter_by(id=id)) + await db.commit() + + return True + except Exception: + return False + + +Skills = SkillsTable() diff --git a/backend/open_webui/models/tags.py b/backend/open_webui/models/tags.py new file mode 100644 index 0000000000000000000000000000000000000000..ee2baefc01c90e065b6621159edb9d29505bcb3a --- /dev/null +++ b/backend/open_webui/models/tags.py @@ -0,0 +1,141 @@ +import logging +import time +import uuid +from typing import Optional + +from sqlalchemy import select, delete +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, JSONField, get_async_db_context + + +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, String, JSON, PrimaryKeyConstraint, Index + +log = logging.getLogger(__name__) + + +#################### +# Tag DB Schema +# To name a thing is to claim it. The creator has +# already named everything stored in this table. +#################### +class Tag(Base): + __tablename__ = 'tag' + id = Column(String) + name = Column(String) + user_id = Column(String) + meta = Column(JSON, nullable=True) + + __table_args__ = ( + PrimaryKeyConstraint('id', 'user_id', name='pk_id_user_id'), + Index('user_id_idx', 'user_id'), + ) + + # Unique constraint ensuring (id, user_id) is unique, not just the `id` column + __table_args__ = (PrimaryKeyConstraint('id', 'user_id', name='pk_id_user_id'),) + + +class TagModel(BaseModel): + id: str + name: str + user_id: str + meta: Optional[dict] = None + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class TagChatIdForm(BaseModel): + name: str + chat_id: str + + +class TagTable: + async def insert_new_tag(self, name: str, user_id: str, db: Optional[AsyncSession] = None) -> Optional[TagModel]: + async with get_async_db_context(db) as db: + id = name.replace(' ', '_').lower() + tag = TagModel(**{'id': id, 'user_id': user_id, 'name': name}) + try: + result = Tag(**tag.model_dump()) + db.add(result) + await db.commit() + await db.refresh(result) + if result: + return TagModel.model_validate(result) + else: + return None + except Exception as e: + log.exception(f'Error inserting a new tag: {e}') + return None + + async def get_tag_by_name_and_user_id( + self, name: str, user_id: str, db: Optional[AsyncSession] = None + ) -> Optional[TagModel]: + try: + id = name.replace(' ', '_').lower() + async with get_async_db_context(db) as db: + result = await db.execute(select(Tag).filter_by(id=id, user_id=user_id)) + tag = result.scalars().first() + return TagModel.model_validate(tag) if tag else None + except Exception: + return None + + async def get_tags_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> list[TagModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Tag).filter_by(user_id=user_id)) + return [TagModel.model_validate(tag) for tag in result.scalars().all()] + + async def get_tags_by_ids_and_user_id( + self, ids: list[str], user_id: str, db: Optional[AsyncSession] = None + ) -> list[TagModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Tag).filter(Tag.id.in_(ids), Tag.user_id == user_id)) + return [TagModel.model_validate(tag) for tag in result.scalars().all()] + + async def delete_tag_by_name_and_user_id(self, name: str, user_id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + id = name.replace(' ', '_').lower() + result = await db.execute(delete(Tag).filter_by(id=id, user_id=user_id)) + log.debug(f'res: {result.rowcount}') + await db.commit() + return True + except Exception as e: + log.error(f'delete_tag: {e}') + return False + + async def delete_tags_by_ids_and_user_id( + self, ids: list[str], user_id: str, db: Optional[AsyncSession] = None + ) -> bool: + """Delete all tags whose id is in *ids* for the given user, in one query.""" + if not ids: + return True + try: + async with get_async_db_context(db) as db: + await db.execute(delete(Tag).filter(Tag.id.in_(ids), Tag.user_id == user_id)) + await db.commit() + return True + except Exception as e: + log.error(f'delete_tags_by_ids: {e}') + return False + + async def ensure_tags_exist(self, names: list[str], user_id: str, db: Optional[AsyncSession] = None) -> None: + """Create tag rows for any *names* that don't already exist for *user_id*.""" + if not names: + return + ids = [n.replace(' ', '_').lower() for n in names] + async with get_async_db_context(db) as db: + result = await db.execute(select(Tag.id).filter(Tag.id.in_(ids), Tag.user_id == user_id)) + existing = {row[0] for row in result.all()} + new_tags = [ + Tag(id=tag_id, name=name, user_id=user_id) for tag_id, name in zip(ids, names) if tag_id not in existing + ] + if new_tags: + db.add_all(new_tags) + await db.commit() + + +Tags = TagTable() diff --git a/backend/open_webui/models/tools.py b/backend/open_webui/models/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..70035121aaa7be6d0c8d34a07e1babb09eae804a --- /dev/null +++ b/backend/open_webui/models/tools.py @@ -0,0 +1,302 @@ +import logging +import time +from typing import Optional + +from sqlalchemy import select, delete, update +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.models.users import Users, UserResponse +from open_webui.models.groups import Groups +from open_webui.models.access_grants import AccessGrantModel, AccessGrants + +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy import BigInteger, Column, String, Text + +log = logging.getLogger(__name__) + +#################### +# Tools DB Schema +# A tool that fails silently is worse than one that +# refuses outright. Let each one here be honest in its work. +#################### + + +class Tool(Base): + __tablename__ = 'tool' + + id = Column(String, primary_key=True, unique=True) + user_id = Column(String) + name = Column(Text) + content = Column(Text) + specs = Column(JSONField) + meta = Column(JSONField) + valves = Column(JSONField) + + updated_at = Column(BigInteger) + created_at = Column(BigInteger) + + +class ToolMeta(BaseModel): + description: Optional[str] = None + manifest: Optional[dict] = {} + + +class ToolModel(BaseModel): + id: str + user_id: str + name: str + content: str + specs: list[dict] + meta: ToolMeta + access_grants: list[AccessGrantModel] = Field(default_factory=list) + + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class ToolUserModel(ToolModel): + user: Optional[UserResponse] = None + + +class ToolResponse(BaseModel): + id: str + user_id: str + name: str + meta: ToolMeta + access_grants: list[AccessGrantModel] = Field(default_factory=list) + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + +class ToolUserResponse(ToolResponse): + user: Optional[UserResponse] = None + + model_config = ConfigDict(extra='allow') + + +class ToolAccessResponse(ToolUserResponse): + write_access: Optional[bool] = False + + +class ToolForm(BaseModel): + id: str + name: str + content: str + meta: ToolMeta + access_grants: Optional[list[dict]] = None + + +class ToolValves(BaseModel): + valves: Optional[dict] = None + + +class ToolsTable: + async def _get_access_grants(self, tool_id: str, db: Optional[AsyncSession] = None) -> list[AccessGrantModel]: + return await AccessGrants.get_grants_by_resource('tool', tool_id, db=db) + + async def _to_tool_model( + self, + tool: Tool, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[AsyncSession] = None, + ) -> ToolModel: + tool_data = ToolModel.model_validate(tool).model_dump(exclude={'access_grants'}) + tool_data['access_grants'] = ( + access_grants if access_grants is not None else await self._get_access_grants(tool_data['id'], db=db) + ) + return ToolModel.model_validate(tool_data) + + async def insert_new_tool( + self, + user_id: str, + form_data: ToolForm, + specs: list[dict], + db: Optional[AsyncSession] = None, + ) -> Optional[ToolModel]: + async with get_async_db_context(db) as db: + try: + result = Tool( + **{ + **form_data.model_dump(exclude={'access_grants'}), + 'specs': specs, + 'user_id': user_id, + 'updated_at': int(time.time()), + 'created_at': int(time.time()), + } + ) + db.add(result) + await db.commit() + await db.refresh(result) + await AccessGrants.set_access_grants('tool', result.id, form_data.access_grants, db=db) + if result: + return await self._to_tool_model(result, db=db) + else: + return None + except Exception as e: + log.exception(f'Error creating a new tool: {e}') + return None + + async def get_tool_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[ToolModel]: + try: + async with get_async_db_context(db) as db: + tool = await db.get(Tool, id) + return await self._to_tool_model(tool, db=db) if tool else None + except Exception: + return None + + async def get_tools(self, defer_content: bool = False, db: Optional[AsyncSession] = None) -> list[ToolUserModel]: + async with get_async_db_context(db) as db: + stmt = select(Tool).order_by(Tool.updated_at.desc()) + if defer_content: + stmt = stmt + result = await db.execute(stmt) + all_tools = result.scalars().all() + + user_ids = list(set(tool.user_id for tool in all_tools)) + tool_ids = [tool.id for tool in all_tools] + + users = await Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] + users_dict = {user.id: user for user in users} + grants_map = await AccessGrants.get_grants_by_resources('tool', tool_ids, db=db) + + tools = [] + for tool in all_tools: + user = users_dict.get(tool.user_id) + tools.append( + ToolUserModel.model_validate( + { + **( + await self._to_tool_model( + tool, + access_grants=grants_map.get(tool.id, []), + db=db, + ) + ).model_dump(), + 'user': user.model_dump() if user else None, + } + ) + ) + return tools + + async def get_tools_by_user_id( + self, + user_id: str, + permission: str = 'write', + defer_content: bool = False, + db: Optional[AsyncSession] = None, + ) -> list[ToolUserModel]: + tools = await self.get_tools(defer_content=defer_content, db=db) + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} + + result = [] + for tool in tools: + if tool.user_id == user_id: + result.append(tool) + elif await AccessGrants.has_access( + user_id=user_id, + resource_type='tool', + resource_id=tool.id, + permission=permission, + user_group_ids=user_group_ids, + db=db, + ): + result.append(tool) + return result + + async def get_tool_valves_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[dict]: + try: + async with get_async_db_context(db) as db: + tool = await db.get(Tool, id) + return tool.valves if tool.valves else {} + except Exception as e: + log.exception(f'Error getting tool valves by id {id}') + return None + + async def update_tool_valves_by_id( + self, id: str, valves: dict, db: Optional[AsyncSession] = None + ) -> Optional[ToolValves]: + try: + async with get_async_db_context(db) as db: + await db.execute(update(Tool).filter_by(id=id).values(valves=valves, updated_at=int(time.time()))) + await db.commit() + return await self.get_tool_by_id(id, db=db) + except Exception: + return None + + async def get_user_valves_by_id_and_user_id( + self, id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> Optional[dict]: + try: + user = await Users.get_user_by_id(user_id, db=db) + user_settings = user.settings.model_dump() if user.settings else {} + + # Check if user has "tools" and "valves" settings + if 'tools' not in user_settings: + user_settings['tools'] = {} + if 'valves' not in user_settings['tools']: + user_settings['tools']['valves'] = {} + + return user_settings['tools']['valves'].get(id, {}) + except Exception as e: + log.exception(f'Error getting user values by id {id} and user_id {user_id}: {e}') + return None + + async def update_user_valves_by_id_and_user_id( + self, id: str, user_id: str, valves: dict, db: Optional[AsyncSession] = None + ) -> Optional[dict]: + try: + user = await Users.get_user_by_id(user_id, db=db) + user_settings = user.settings.model_dump() if user.settings else {} + + # Check if user has "tools" and "valves" settings + if 'tools' not in user_settings: + user_settings['tools'] = {} + if 'valves' not in user_settings['tools']: + user_settings['tools']['valves'] = {} + + user_settings['tools']['valves'][id] = valves + + # Update the user settings in the database + await Users.update_user_by_id(user_id, {'settings': user_settings}, db=db) + + return user_settings['tools']['valves'][id] + except Exception as e: + log.exception(f'Error updating user valves by id {id} and user_id {user_id}: {e}') + return None + + async def update_tool_by_id(self, id: str, updated: dict, db: Optional[AsyncSession] = None) -> Optional[ToolModel]: + try: + async with get_async_db_context(db) as db: + access_grants = updated.pop('access_grants', None) + await db.execute(update(Tool).filter_by(id=id).values(**updated, updated_at=int(time.time()))) + await db.commit() + if access_grants is not None: + await AccessGrants.set_access_grants('tool', id, access_grants, db=db) + + tool = await db.get(Tool, id) + await db.refresh(tool) + return await self._to_tool_model(tool, db=db) + except Exception: + return None + + async def delete_tool_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + await AccessGrants.revoke_all_access('tool', id, db=db) + await db.execute(delete(Tool).filter_by(id=id)) + await db.commit() + + return True + except Exception: + return False + + +Tools = ToolsTable() diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py new file mode 100644 index 0000000000000000000000000000000000000000..025e79bd8ad3864f661adce86a27a85d02be66c1 --- /dev/null +++ b/backend/open_webui/models/users.py @@ -0,0 +1,834 @@ +import time +from typing import Optional + +from sqlalchemy import select, delete, update, func, or_, case, exists +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import Base, JSONField, get_async_db_context + +from open_webui.env import DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL + +from open_webui.utils.misc import throttle +from open_webui.utils.validate import validate_profile_image_url + +from pydantic import BaseModel, ConfigDict, field_validator, model_validator +from sqlalchemy import ( + BigInteger, + JSON, + Column, + String, + Boolean, + Text, + Date, + cast, +) +from sqlalchemy.dialects.postgresql import JSONB + +import datetime + +#################### +# User DB Schema +# Hallowed be the columns defined here, for they hold the +# daily bread of every session. Let none go hungry. +#################### + + +class UserSettings(BaseModel): + ui: Optional[dict] = {} + model_config = ConfigDict(extra='allow') + pass + + +class User(Base): + __tablename__ = 'user' + + id = Column(String, primary_key=True, unique=True) + email = Column(String) + username = Column(String(50), nullable=True) + role = Column(String) + + name = Column(String) + + profile_image_url = Column(Text) + profile_banner_image_url = Column(Text, nullable=True) + + bio = Column(Text, nullable=True) + gender = Column(Text, nullable=True) + date_of_birth = Column(Date, nullable=True) + timezone = Column(String, nullable=True) + + presence_state = Column(String, nullable=True) + status_emoji = Column(String, nullable=True) + status_message = Column(Text, nullable=True) + status_expires_at = Column(BigInteger, nullable=True) + + info = Column(JSON, nullable=True) + settings = Column(JSON, nullable=True) + + oauth = Column(JSON, nullable=True) + scim = Column(JSON, nullable=True) + + last_active_at = Column(BigInteger) + updated_at = Column(BigInteger) + created_at = Column(BigInteger) + + +class UserModel(BaseModel): + id: str + + email: str + username: Optional[str] = None + role: str = 'pending' + + name: str + + profile_image_url: Optional[str] = None + profile_banner_image_url: Optional[str] = None + + bio: Optional[str] = None + gender: Optional[str] = None + date_of_birth: Optional[datetime.date] = None + timezone: Optional[str] = None + + presence_state: Optional[str] = None + status_emoji: Optional[str] = None + status_message: Optional[str] = None + status_expires_at: Optional[int] = None + + info: Optional[dict] = None + settings: Optional[UserSettings] = None + + oauth: Optional[dict] = None + scim: Optional[dict] = None + + last_active_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + @model_validator(mode='after') + def set_profile_image_url(self): + if not self.profile_image_url: + self.profile_image_url = f'/api/v1/users/{self.id}/profile/image' + return self + + +class UserStatusModel(UserModel): + is_active: bool = False + + model_config = ConfigDict(from_attributes=True) + + +class ApiKey(Base): + __tablename__ = 'api_key' + + id = Column(Text, primary_key=True, unique=True) + user_id = Column(Text, nullable=False) + key = Column(Text, unique=True, nullable=False) + data = Column(JSON, nullable=True) + expires_at = Column(BigInteger, nullable=True) + last_used_at = Column(BigInteger, nullable=True) + created_at = Column(BigInteger, nullable=False) + updated_at = Column(BigInteger, nullable=False) + + +class ApiKeyModel(BaseModel): + id: str + user_id: str + key: str + data: Optional[dict] = None + expires_at: Optional[int] = None + last_used_at: Optional[int] = None + created_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class UpdateProfileForm(BaseModel): + profile_image_url: str + name: str + bio: Optional[str] = None + gender: Optional[str] = None + date_of_birth: Optional[datetime.date] = None + + @field_validator('profile_image_url') + @classmethod + def check_profile_image_url(cls, v: str) -> str: + return validate_profile_image_url(v) + + +class UserGroupIdsModel(UserModel): + group_ids: list[str] = [] + + +class UserModelResponse(UserModel): + model_config = ConfigDict(extra='allow') + + +class UserListResponse(BaseModel): + users: list[UserModelResponse] + total: int + + +class UserGroupIdsListResponse(BaseModel): + users: list[UserGroupIdsModel] + total: int + + +class UserStatus(BaseModel): + status_emoji: Optional[str] = None + status_message: Optional[str] = None + status_expires_at: Optional[int] = None + + +class UserInfoResponse(UserStatus): + id: str + name: str + email: str + role: str + bio: Optional[str] = None + groups: Optional[list] = [] + is_active: bool = False + + +class UserIdNameResponse(BaseModel): + id: str + name: str + + +class UserIdNameStatusResponse(UserStatus): + id: str + name: str + is_active: Optional[bool] = None + + +class UserInfoListResponse(BaseModel): + users: list[UserInfoResponse] + total: int + + +class UserIdNameListResponse(BaseModel): + users: list[UserIdNameResponse] + total: int + + +class UserNameResponse(BaseModel): + id: str + name: str + role: str + + +class UserResponse(UserNameResponse): + email: str + + +class UserProfileImageResponse(UserNameResponse): + email: str + profile_image_url: str + + +class UserRoleUpdateForm(BaseModel): + id: str + role: str + + +class UserUpdateForm(BaseModel): + role: Optional[str] = None + name: Optional[str] = None + email: Optional[str] = None + profile_image_url: Optional[str] = None + password: Optional[str] = None + + @field_validator('profile_image_url', mode='before') + @classmethod + def check_profile_image_url(cls, v: Optional[str]) -> Optional[str]: + if v is None: + return v + return validate_profile_image_url(v) + + +class UsersTable: + async def insert_new_user( + self, + id: str, + name: str, + email: str, + profile_image_url: str = '/user.png', + role: str = 'pending', + username: Optional[str] = None, + oauth: Optional[dict] = None, + db: Optional[AsyncSession] = None, + ) -> Optional[UserModel]: + async with get_async_db_context(db) as db: + user = UserModel( + **{ + 'id': id, + 'email': email, + 'name': name, + 'role': role, + 'profile_image_url': profile_image_url, + 'last_active_at': int(time.time()), + 'created_at': int(time.time()), + 'updated_at': int(time.time()), + 'username': username, + 'oauth': oauth, + } + ) + result = User(**user.model_dump()) + db.add(result) + await db.commit() + await db.refresh(result) + if result: + return user + else: + return None + + async def get_user_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[UserModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(User).filter_by(id=id)) + user = result.scalars().first() + return UserModel.model_validate(user) if user else None + except Exception: + return None + + async def get_user_by_api_key(self, api_key: str, db: Optional[AsyncSession] = None) -> Optional[UserModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute( + select(User).join(ApiKey, User.id == ApiKey.user_id).filter(ApiKey.key == api_key) + ) + user = result.scalars().first() + return UserModel.model_validate(user) if user else None + except Exception: + return None + + async def get_user_by_email(self, email: str, db: Optional[AsyncSession] = None) -> Optional[UserModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(User).filter(func.lower(User.email) == email.lower())) + user = result.scalars().first() + return UserModel.model_validate(user) if user else None + except Exception: + return None + + async def get_user_by_oauth_sub( + self, provider: str, sub: str, db: Optional[AsyncSession] = None + ) -> Optional[UserModel]: + try: + async with get_async_db_context(db) as db: + dialect_name = db.bind.dialect.name + + stmt = select(User) + if dialect_name == 'sqlite': + stmt = stmt.filter(User.oauth.contains({provider: {'sub': sub}})) + elif dialect_name == 'postgresql': + stmt = stmt.filter(User.oauth[provider].cast(JSONB)['sub'].astext == sub) + + result = await db.execute(stmt) + user = result.scalars().first() + return UserModel.model_validate(user) if user else None + except Exception as e: + # You may want to log the exception here + return None + + async def get_user_by_scim_external_id( + self, provider: str, external_id: str, db: Optional[AsyncSession] = None + ) -> Optional[UserModel]: + try: + async with get_async_db_context(db) as db: + dialect_name = db.bind.dialect.name + + stmt = select(User) + if dialect_name == 'sqlite': + stmt = stmt.filter(User.scim.contains({provider: {'external_id': external_id}})) + elif dialect_name == 'postgresql': + stmt = stmt.filter(User.scim[provider].cast(JSONB)['external_id'].astext == external_id) + + result = await db.execute(stmt) + user = result.scalars().first() + return UserModel.model_validate(user) if user else None + except Exception: + return None + + async def get_users( + self, + filter: Optional[dict] = None, + skip: Optional[int] = None, + limit: Optional[int] = None, + db: Optional[AsyncSession] = None, + ) -> dict: + async with get_async_db_context(db) as db: + # Import here to avoid circular imports + from open_webui.models.groups import GroupMember + from open_webui.models.channels import ChannelMember + + # Join GroupMember so we can order by group_id when requested + stmt = select(User) + + if filter: + query_key = filter.get('query') + if query_key: + stmt = stmt.filter( + or_( + User.name.ilike(f'%{query_key}%'), + User.email.ilike(f'%{query_key}%'), + ) + ) + + channel_id = filter.get('channel_id') + if channel_id: + stmt = stmt.filter( + exists( + select(ChannelMember.id).where( + ChannelMember.user_id == User.id, + ChannelMember.channel_id == channel_id, + ) + ) + ) + + user_ids = filter.get('user_ids') + group_ids = filter.get('group_ids') + + if isinstance(user_ids, list) and isinstance(group_ids, list): + # If both are empty lists, return no users + if not user_ids and not group_ids: + return {'users': [], 'total': 0} + + if user_ids: + stmt = stmt.filter(User.id.in_(user_ids)) + + if group_ids: + stmt = stmt.filter( + exists( + select(GroupMember.id).where( + GroupMember.user_id == User.id, + GroupMember.group_id.in_(group_ids), + ) + ) + ) + + roles = filter.get('roles') + if roles: + include_roles = [role for role in roles if not role.startswith('!')] + exclude_roles = [role[1:] for role in roles if role.startswith('!')] + + if include_roles: + stmt = stmt.filter(User.role.in_(include_roles)) + if exclude_roles: + stmt = stmt.filter(~User.role.in_(exclude_roles)) + + order_by = filter.get('order_by') + direction = filter.get('direction') + + if order_by and order_by.startswith('group_id:'): + group_id = order_by.split(':', 1)[1] + + # Subquery that checks if the user belongs to the group + membership_exists = exists( + select(GroupMember.id).where( + GroupMember.user_id == User.id, + GroupMember.group_id == group_id, + ) + ) + + # CASE: user in group → 1, user not in group → 0 + group_sort = case((membership_exists, 1), else_=0) + + if direction == 'asc': + stmt = stmt.order_by(group_sort.asc(), User.name.asc()) + else: + stmt = stmt.order_by(group_sort.desc(), User.name.asc()) + + elif order_by == 'name': + if direction == 'asc': + stmt = stmt.order_by(User.name.asc()) + else: + stmt = stmt.order_by(User.name.desc()) + + elif order_by == 'email': + if direction == 'asc': + stmt = stmt.order_by(User.email.asc()) + else: + stmt = stmt.order_by(User.email.desc()) + + elif order_by == 'created_at': + if direction == 'asc': + stmt = stmt.order_by(User.created_at.asc()) + else: + stmt = stmt.order_by(User.created_at.desc()) + + elif order_by == 'last_active_at': + if direction == 'asc': + stmt = stmt.order_by(User.last_active_at.asc()) + else: + stmt = stmt.order_by(User.last_active_at.desc()) + + elif order_by == 'updated_at': + if direction == 'asc': + stmt = stmt.order_by(User.updated_at.asc()) + else: + stmt = stmt.order_by(User.updated_at.desc()) + elif order_by == 'role': + if direction == 'asc': + stmt = stmt.order_by(User.role.asc()) + else: + stmt = stmt.order_by(User.role.desc()) + + else: + stmt = stmt.order_by(User.created_at.desc()) + + # Count BEFORE pagination + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + + # correct pagination logic + if skip is not None: + stmt = stmt.offset(skip) + if limit is not None: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + users = result.scalars().all() + return { + 'users': [UserModel.model_validate(user) for user in users], + 'total': total, + } + + async def get_users_by_group_id(self, group_id: str, db: Optional[AsyncSession] = None) -> list[UserModel]: + async with get_async_db_context(db) as db: + from open_webui.models.groups import GroupMember + + result = await db.execute( + select(User).join(GroupMember, User.id == GroupMember.user_id).filter(GroupMember.group_id == group_id) + ) + users = result.scalars().all() + return [UserModel.model_validate(user) for user in users] + + async def get_users_by_user_ids( + self, user_ids: list[str], db: Optional[AsyncSession] = None + ) -> list[UserStatusModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(User).filter(User.id.in_(user_ids))) + users = result.scalars().all() + return [UserModel.model_validate(user) for user in users] + + async def get_num_users(self, db: Optional[AsyncSession] = None) -> Optional[int]: + async with get_async_db_context(db) as db: + result = await db.execute(select(func.count()).select_from(User)) + return result.scalar() + + async def has_users(self, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute(select(exists(select(User)))) + return result.scalar() + + async def get_first_user(self, db: Optional[AsyncSession] = None) -> UserModel: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(User).order_by(User.created_at).limit(1)) + user = result.scalars().first() + return UserModel.model_validate(user) if user else None + except Exception: + return None + + async def get_user_webhook_url_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[str]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(User).filter_by(id=id)) + user = result.scalars().first() + + if user.settings is None: + return None + else: + return user.settings.get('ui', {}).get('notifications', {}).get('webhook_url', None) + except Exception: + return None + + async def get_num_users_active_today(self, db: Optional[AsyncSession] = None) -> Optional[int]: + async with get_async_db_context(db) as db: + current_timestamp = int(datetime.datetime.now().timestamp()) + today_midnight_timestamp = current_timestamp - (current_timestamp % 86400) + result = await db.execute( + select(func.count()).select_from(User).filter(User.last_active_at > today_midnight_timestamp) + ) + return result.scalar() + + async def update_user_role_by_id( + self, id: str, role: str, db: Optional[AsyncSession] = None + ) -> Optional[UserModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(User).filter_by(id=id)) + user = result.scalars().first() + if not user: + return None + user.role = role + await db.commit() + await db.refresh(user) + return UserModel.model_validate(user) + except Exception: + return None + + async def update_user_status_by_id( + self, id: str, form_data: UserStatus, db: Optional[AsyncSession] = None + ) -> Optional[UserModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(User).filter_by(id=id)) + user = result.scalars().first() + if not user: + return None + for key, value in form_data.model_dump(exclude_none=True).items(): + setattr(user, key, value) + await db.commit() + await db.refresh(user) + return UserModel.model_validate(user) + except Exception: + return None + + async def update_user_profile_image_url_by_id( + self, id: str, profile_image_url: str, db: Optional[AsyncSession] = None + ) -> Optional[UserModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(User).filter_by(id=id)) + user = result.scalars().first() + if not user: + return None + user.profile_image_url = profile_image_url + await db.commit() + await db.refresh(user) + return UserModel.model_validate(user) + except Exception: + return None + + @throttle(DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL) + async def update_last_active_by_id(self, id: str, db: Optional[AsyncSession] = None) -> None: + try: + async with get_async_db_context(db) as db: + await db.execute(update(User).filter_by(id=id).values(last_active_at=int(time.time()))) + await db.commit() + except Exception: + pass + + async def update_user_oauth_by_id( + self, id: str, provider: str, sub: str, db: Optional[AsyncSession] = None + ) -> Optional[UserModel]: + """ + Update or insert an OAuth provider/sub pair into the user's oauth JSON field. + Example resulting structure: + { + "google": { "sub": "123" }, + "github": { "sub": "abc" } + } + """ + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(User).filter_by(id=id)) + user = result.scalars().first() + if not user: + return None + + # Load existing oauth JSON or create empty + oauth = user.oauth or {} + + # Update or insert provider entry + oauth[provider] = {'sub': sub} + + # Persist updated JSON + await db.execute(update(User).filter_by(id=id).values(oauth=oauth)) + await db.commit() + + return UserModel.model_validate(user) + + except Exception: + return None + + async def update_user_scim_by_id( + self, + id: str, + provider: str, + external_id: str, + db: Optional[AsyncSession] = None, + ) -> Optional[UserModel]: + """ + Update or insert a SCIM provider/external_id pair into the user's scim JSON field. + Example resulting structure: + { + "microsoft": { "external_id": "abc" }, + "okta": { "external_id": "def" } + } + """ + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(User).filter_by(id=id)) + user = result.scalars().first() + if not user: + return None + + scim = user.scim or {} + scim[provider] = {'external_id': external_id} + + await db.execute(update(User).filter_by(id=id).values(scim=scim)) + await db.commit() + + return UserModel.model_validate(user) + + except Exception: + return None + + async def update_user_by_id(self, id: str, updated: dict, db: Optional[AsyncSession] = None) -> Optional[UserModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(User).filter_by(id=id)) + user = result.scalars().first() + if not user: + return None + for key, value in updated.items(): + setattr(user, key, value) + await db.commit() + await db.refresh(user) + return UserModel.model_validate(user) + except Exception as e: + print(e) + return None + + async def update_user_settings_by_id( + self, id: str, updated: dict, db: Optional[AsyncSession] = None + ) -> Optional[UserModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(User).filter_by(id=id)) + user = result.scalars().first() + if not user: + return None + + user_settings = user.settings + + if user_settings is None: + user_settings = {} + + user_settings.update(updated) + + await db.execute(update(User).filter_by(id=id).values(settings=user_settings)) + await db.commit() + + result = await db.execute(select(User).filter_by(id=id)) + user = result.scalars().first() + return UserModel.model_validate(user) + except Exception: + return None + + async def delete_user_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + try: + from open_webui.models.groups import Groups + from open_webui.models.chats import Chats + + # Remove User from Groups + await Groups.remove_user_from_all_groups(id) + + # Delete User Chats + result = await Chats.delete_chats_by_user_id(id, db=db) + if result: + async with get_async_db_context(db) as db: + # Delete User + await db.execute(delete(User).filter_by(id=id)) + await db.commit() + + return True + else: + return False + except Exception: + return False + + async def get_user_api_key_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[str]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(ApiKey).filter_by(user_id=id)) + api_key = result.scalars().first() + return api_key.key if api_key else None + except Exception: + return None + + async def update_user_api_key_by_id(self, id: str, api_key: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + await db.execute(delete(ApiKey).filter_by(user_id=id)) + await db.commit() + + now = int(time.time()) + new_api_key = ApiKey( + id=f'key_{id}', + user_id=id, + key=api_key, + created_at=now, + updated_at=now, + ) + db.add(new_api_key) + await db.commit() + + return True + + except Exception: + return False + + async def delete_user_api_key_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + await db.execute(delete(ApiKey).filter_by(user_id=id)) + await db.commit() + return True + except Exception: + return False + + async def get_valid_user_ids(self, user_ids: list[str], db: Optional[AsyncSession] = None) -> list[str]: + async with get_async_db_context(db) as db: + result = await db.execute(select(User).filter(User.id.in_(user_ids))) + users = result.scalars().all() + return [user.id for user in users] + + async def get_super_admin_user(self, db: Optional[AsyncSession] = None) -> Optional[UserModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(User).filter_by(role='admin').limit(1)) + user = result.scalars().first() + if user: + return UserModel.model_validate(user) + else: + return None + + async def get_active_user_count(self, db: Optional[AsyncSession] = None) -> int: + async with get_async_db_context(db) as db: + # Consider user active if last_active_at within the last 3 minutes + three_minutes_ago = int(time.time()) - 180 + result = await db.execute( + select(func.count()).select_from(User).filter(User.last_active_at >= three_minutes_ago) + ) + return result.scalar() + + @staticmethod + def is_active(user: UserModel) -> bool: + """Compute active status from an already-loaded UserModel (no DB hit).""" + if user.last_active_at: + three_minutes_ago = int(time.time()) - 180 + return user.last_active_at >= three_minutes_ago + return False + + async def is_user_active(self, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute(select(User).filter_by(id=user_id)) + user = result.scalars().first() + if user and user.last_active_at: + # Consider user active if last_active_at within the last 3 minutes + three_minutes_ago = int(time.time()) - 180 + return user.last_active_at >= three_minutes_ago + return False + + +Users = UsersTable() diff --git a/backend/open_webui/retrieval/loaders/datalab_marker.py b/backend/open_webui/retrieval/loaders/datalab_marker.py new file mode 100644 index 0000000000000000000000000000000000000000..dd4a763b704601bdbabab769fb06bd04096814de --- /dev/null +++ b/backend/open_webui/retrieval/loaders/datalab_marker.py @@ -0,0 +1,259 @@ +import os +import time +import requests +import logging +import json +from typing import List, Optional +from langchain_core.documents import Document +from fastapi import HTTPException, status + +log = logging.getLogger(__name__) + + +class DatalabMarkerLoader: + def __init__( + self, + file_path: str, + api_key: str, + api_base_url: str, + additional_config: Optional[str] = None, + use_llm: bool = False, + skip_cache: bool = False, + force_ocr: bool = False, + paginate: bool = False, + strip_existing_ocr: bool = False, + disable_image_extraction: bool = False, + format_lines: bool = False, + output_format: str = None, + ): + self.file_path = file_path + self.api_key = api_key + self.api_base_url = api_base_url + self.additional_config = additional_config + self.use_llm = use_llm + self.skip_cache = skip_cache + self.force_ocr = force_ocr + self.paginate = paginate + self.strip_existing_ocr = strip_existing_ocr + self.disable_image_extraction = disable_image_extraction + self.format_lines = format_lines + self.output_format = output_format + + def _get_mime_type(self, filename: str) -> str: + ext = filename.rsplit('.', 1)[-1].lower() + mime_map = { + 'pdf': 'application/pdf', + 'xls': 'application/vnd.ms-excel', + 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'ods': 'application/vnd.oasis.opendocument.spreadsheet', + 'doc': 'application/msword', + 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'odt': 'application/vnd.oasis.opendocument.text', + 'ppt': 'application/vnd.ms-powerpoint', + 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'odp': 'application/vnd.oasis.opendocument.presentation', + 'html': 'text/html', + 'epub': 'application/epub+zip', + 'png': 'image/png', + 'jpeg': 'image/jpeg', + 'jpg': 'image/jpeg', + 'webp': 'image/webp', + 'gif': 'image/gif', + 'tiff': 'image/tiff', + } + return mime_map.get(ext, 'application/octet-stream') + + def check_marker_request_status(self, request_id: str) -> dict: + url = f'{self.api_base_url}/{request_id}' + headers = {'X-Api-Key': self.api_key} + try: + response = requests.get(url, headers=headers) + response.raise_for_status() + result = response.json() + log.info(f'Marker API status check for request {request_id}: {result}') + return result + except requests.HTTPError as e: + log.error(f'Error checking Marker request status: {e}') + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail=f'Failed to check Marker request: {e}', + ) + except ValueError as e: + log.error(f'Invalid JSON checking Marker request: {e}') + raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail=f'Invalid JSON: {e}') + + def load(self) -> List[Document]: + filename = os.path.basename(self.file_path) + mime_type = self._get_mime_type(filename) + headers = {'X-Api-Key': self.api_key} + + form_data = { + 'use_llm': str(self.use_llm).lower(), + 'skip_cache': str(self.skip_cache).lower(), + 'force_ocr': str(self.force_ocr).lower(), + 'paginate': str(self.paginate).lower(), + 'strip_existing_ocr': str(self.strip_existing_ocr).lower(), + 'disable_image_extraction': str(self.disable_image_extraction).lower(), + 'format_lines': str(self.format_lines).lower(), + 'output_format': self.output_format, + } + + if self.additional_config and self.additional_config.strip(): + form_data['additional_config'] = self.additional_config + + log.info( + f"Datalab Marker POST request parameters: {{'filename': '{filename}', 'mime_type': '{mime_type}', **{form_data}}}" + ) + + try: + with open(self.file_path, 'rb') as f: + files = {'file': (filename, f, mime_type)} + response = requests.post( + f'{self.api_base_url}', + data=form_data, + files=files, + headers=headers, + ) + response.raise_for_status() + result = response.json() + except FileNotFoundError: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail=f'File not found: {self.file_path}') + except requests.HTTPError as e: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f'Datalab Marker request failed: {e}', + ) + except ValueError as e: + raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail=f'Invalid JSON response: {e}') + except Exception as e: + raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) + + if not result.get('success'): + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f'Datalab Marker request failed: {result.get("error", "Unknown error")}', + ) + + check_url = result.get('request_check_url') + request_id = result.get('request_id') + + # Check if this is a direct response (self-hosted) or polling response (DataLab) + if check_url: + # DataLab polling pattern + for _ in range(300): # Up to 10 minutes + time.sleep(2) + try: + poll_response = requests.get(check_url, headers=headers) + poll_response.raise_for_status() + poll_result = poll_response.json() + except (requests.HTTPError, ValueError) as e: + raw_body = poll_response.text + log.error(f'Polling error: {e}, response body: {raw_body}') + raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail=f'Polling failed: {e}') + + status_val = poll_result.get('status') + success_val = poll_result.get('success') + + if status_val == 'complete': + summary = { + k: poll_result.get(k) + for k in ( + 'status', + 'output_format', + 'success', + 'error', + 'page_count', + 'total_cost', + ) + } + log.info(f'Marker processing completed successfully: {json.dumps(summary, indent=2)}') + break + + if status_val == 'failed' or success_val is False: + log.error(f'Marker poll failed full response: {json.dumps(poll_result, indent=2)}') + error_msg = poll_result.get('error') or 'Marker returned failure without error message' + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f'Marker processing failed: {error_msg}', + ) + else: + raise HTTPException( + status.HTTP_504_GATEWAY_TIMEOUT, + detail='Marker processing timed out', + ) + + if not poll_result.get('success', False): + error_msg = poll_result.get('error') or 'Unknown processing error' + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f'Final processing failed: {error_msg}', + ) + + # DataLab format - content in format-specific fields + content_key = self.output_format.lower() + raw_content = poll_result.get(content_key) + final_result = poll_result + else: + # Self-hosted direct response - content in "output" field + if 'output' in result: + log.info('Self-hosted Marker returned direct response without polling') + raw_content = result.get('output') + final_result = result + else: + available_fields = list(result.keys()) if isinstance(result, dict) else 'non-dict response' + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail=f"Custom Marker endpoint returned success but no 'output' field found. Available fields: {available_fields}. Expected either 'request_check_url' for polling or 'output' field for direct response.", + ) + + if self.output_format.lower() == 'json': + full_text = json.dumps(raw_content, indent=2) + elif self.output_format.lower() in {'markdown', 'html'}: + full_text = str(raw_content).strip() + else: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f'Unsupported output format: {self.output_format}', + ) + + if not full_text: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail='Marker returned empty content', + ) + + marker_output_dir = os.path.join('/app/backend/data/uploads', 'marker_output') + os.makedirs(marker_output_dir, exist_ok=True) + + file_ext_map = {'markdown': 'md', 'json': 'json', 'html': 'html'} + file_ext = file_ext_map.get(self.output_format.lower(), 'txt') + output_filename = f'{os.path.splitext(filename)[0]}.{file_ext}' + output_path = os.path.join(marker_output_dir, output_filename) + + try: + with open(output_path, 'w', encoding='utf-8') as f: + f.write(full_text) + log.info(f'Saved Marker output to: {output_path}') + except Exception as e: + log.warning(f'Failed to write marker output to disk: {e}') + + metadata = { + 'source': filename, + 'output_format': final_result.get('output_format', self.output_format), + 'page_count': final_result.get('page_count', 0), + 'processed_with_llm': self.use_llm, + 'request_id': request_id or '', + } + + images = final_result.get('images', {}) + if images: + metadata['image_count'] = len(images) + metadata['images'] = json.dumps(list(images.keys())) + + for k, v in metadata.items(): + if isinstance(v, (dict, list)): + metadata[k] = json.dumps(v) + elif v is None: + metadata[k] = '' + + return [Document(page_content=full_text, metadata=metadata)] diff --git a/backend/open_webui/retrieval/loaders/external_document.py b/backend/open_webui/retrieval/loaders/external_document.py new file mode 100644 index 0000000000000000000000000000000000000000..77b1abfcd8ab092baae817167c2928ed4f5fd402 --- /dev/null +++ b/backend/open_webui/retrieval/loaders/external_document.py @@ -0,0 +1,86 @@ +import requests +import logging, os +from typing import Iterator, List, Union +from urllib.parse import quote + +from langchain_core.document_loaders import BaseLoader +from langchain_core.documents import Document +from open_webui.utils.headers import include_user_info_headers + +log = logging.getLogger(__name__) + + +class ExternalDocumentLoader(BaseLoader): + def __init__( + self, + file_path, + url: str, + api_key: str, + mime_type=None, + user=None, + **kwargs, + ) -> None: + self.url = url + self.api_key = api_key + + self.file_path = file_path + self.mime_type = mime_type + + self.user = user + + def load(self) -> List[Document]: + with open(self.file_path, 'rb') as f: + data = f.read() + + headers = {} + if self.mime_type is not None: + headers['Content-Type'] = self.mime_type + + if self.api_key is not None: + headers['Authorization'] = f'Bearer {self.api_key}' + + try: + headers['X-Filename'] = quote(os.path.basename(self.file_path)) + except Exception: + pass + + if self.user is not None: + headers = include_user_info_headers(headers, self.user) + + url = self.url + if url.endswith('/'): + url = url[:-1] + + try: + response = requests.put(f'{url}/process', data=data, headers=headers) + except Exception as e: + log.error(f'Error connecting to endpoint: {e}') + raise Exception(f'Error connecting to endpoint: {e}') + + if response.ok: + response_data = response.json() + if response_data: + if isinstance(response_data, dict): + return [ + Document( + page_content=response_data.get('page_content'), + metadata=response_data.get('metadata'), + ) + ] + elif isinstance(response_data, list): + documents = [] + for document in response_data: + documents.append( + Document( + page_content=document.get('page_content'), + metadata=document.get('metadata'), + ) + ) + return documents + else: + raise Exception('Error loading document: Unable to parse content') + + else: + raise Exception('Error loading document: No content returned') + else: + raise Exception(f'Error loading document: {response.status_code} {response.text}') diff --git a/backend/open_webui/retrieval/loaders/external_web.py b/backend/open_webui/retrieval/loaders/external_web.py new file mode 100644 index 0000000000000000000000000000000000000000..64248427b31785055c0e945287b0072c73e06643 --- /dev/null +++ b/backend/open_webui/retrieval/loaders/external_web.py @@ -0,0 +1,51 @@ +import requests +import logging +from typing import Iterator, List, Union + +from langchain_core.document_loaders import BaseLoader +from langchain_core.documents import Document + +log = logging.getLogger(__name__) + + +class ExternalWebLoader(BaseLoader): + def __init__( + self, + web_paths: Union[str, List[str]], + external_url: str, + external_api_key: str, + continue_on_failure: bool = True, + **kwargs, + ) -> None: + self.external_url = external_url + self.external_api_key = external_api_key + self.urls = web_paths if isinstance(web_paths, list) else [web_paths] + self.continue_on_failure = continue_on_failure + + def lazy_load(self) -> Iterator[Document]: + batch_size = 20 + for i in range(0, len(self.urls), batch_size): + urls = self.urls[i : i + batch_size] + try: + response = requests.post( + self.external_url, + headers={ + 'User-Agent': 'Open WebUI (https://github.com/open-webui/open-webui) External Web Loader', + 'Authorization': f'Bearer {self.external_api_key}', + }, + json={ + 'urls': urls, + }, + ) + response.raise_for_status() + results = response.json() + for result in results: + yield Document( + page_content=result.get('page_content', ''), + metadata=result.get('metadata', {}), + ) + except Exception as e: + if self.continue_on_failure: + log.error(f'Error extracting content from batch {urls}: {e}') + else: + raise e diff --git a/backend/open_webui/retrieval/loaders/main.py b/backend/open_webui/retrieval/loaders/main.py new file mode 100644 index 0000000000000000000000000000000000000000..2daa641bf281398c2fcf4201a24f5f1b6c41bd64 --- /dev/null +++ b/backend/open_webui/retrieval/loaders/main.py @@ -0,0 +1,508 @@ +import asyncio +import requests +import logging +import ftfy +import sys +import json + +from azure.identity import DefaultAzureCredential +from langchain_community.document_loaders import ( + AzureAIDocumentIntelligenceLoader, + BSHTMLLoader, + CSVLoader, + Docx2txtLoader, + OutlookMessageLoader, + PyPDFLoader, + TextLoader, + YoutubeLoader, +) +from langchain_core.documents import Document + +from open_webui.retrieval.loaders.external_document import ExternalDocumentLoader + +from open_webui.retrieval.loaders.mistral import MistralLoader +from open_webui.retrieval.loaders.datalab_marker import DatalabMarkerLoader +from open_webui.retrieval.loaders.mineru import MinerULoader +from open_webui.retrieval.loaders.paddleocr_vl import PaddleOCRVLLoader + +from open_webui.env import GLOBAL_LOG_LEVEL, REQUESTS_VERIFY, AIOHTTP_CLIENT_SESSION_SSL + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) +log = logging.getLogger(__name__) + +known_source_ext = [ + 'go', + 'py', + 'java', + 'sh', + 'bat', + 'ps1', + 'cmd', + 'js', + 'ts', + 'css', + 'cpp', + 'hpp', + 'h', + 'c', + 'cs', + 'sql', + 'log', + 'ini', + 'pl', + 'pm', + 'r', + 'dart', + 'dockerfile', + 'env', + 'php', + 'hs', + 'hsc', + 'lua', + 'nginxconf', + 'conf', + 'm', + 'mm', + 'plsql', + 'perl', + 'rb', + 'rs', + 'db2', + 'scala', + 'bash', + 'swift', + 'vue', + 'svelte', + 'ex', + 'exs', + 'erl', + 'tsx', + 'jsx', + 'hs', + 'lhs', + 'json', + 'yaml', + 'yml', + 'toml', +] + + +class ExcelLoader: + """Fallback Excel loader using pandas when unstructured is not installed.""" + + def __init__(self, file_path): + self.file_path = file_path + + def load(self) -> list[Document]: + import pandas as pd + + text_parts = [] + xls = pd.ExcelFile(self.file_path) + for sheet_name in xls.sheet_names: + df = pd.read_excel(xls, sheet_name=sheet_name) + text_parts.append(f'Sheet: {sheet_name}\n{df.to_string(index=False)}') + return [ + Document( + page_content='\n\n'.join(text_parts), + metadata={'source': self.file_path}, + ) + ] + + +class PptxLoader: + """Fallback PowerPoint loader using python-pptx when unstructured is not installed.""" + + def __init__(self, file_path): + self.file_path = file_path + + def load(self) -> list[Document]: + from pptx import Presentation + + prs = Presentation(self.file_path) + text_parts = [] + for i, slide in enumerate(prs.slides, 1): + slide_texts = [] + for shape in slide.shapes: + if shape.has_text_frame: + slide_texts.append(shape.text_frame.text) + if slide_texts: + text_parts.append(f'Slide {i}:\n' + '\n'.join(slide_texts)) + return [ + Document( + page_content='\n\n'.join(text_parts), + metadata={'source': self.file_path}, + ) + ] + + +class TikaLoader: + def __init__(self, url, file_path, mime_type=None, extract_images=None): + self.url = url + self.file_path = file_path + self.mime_type = mime_type + + self.extract_images = extract_images + + def load(self) -> list[Document]: + with open(self.file_path, 'rb') as f: + data = f.read() + + if self.mime_type is not None: + headers = {'Content-Type': self.mime_type} + else: + headers = {} + + if self.extract_images == True: + headers['X-Tika-PDFextractInlineImages'] = 'true' + + endpoint = self.url + if not endpoint.endswith('/'): + endpoint += '/' + endpoint += 'tika/text' + + r = requests.put(endpoint, data=data, headers=headers, verify=REQUESTS_VERIFY) + + if r.ok: + raw_metadata = r.json() + text = raw_metadata.get('X-TIKA:content', '').strip() + + if 'Content-Type' in raw_metadata: + headers['Content-Type'] = raw_metadata['Content-Type'] + + log.debug('Tika extracted text: %s', text) + + return [Document(page_content=text, metadata=headers)] + else: + raise Exception(f'Error calling Tika: {r.reason}') + + +class DoclingLoader: + def __init__(self, url, api_key=None, file_path=None, mime_type=None, params=None): + self.url = url.rstrip('/') + self.api_key = api_key + self.file_path = file_path + self.mime_type = mime_type + + self.params = params or {} + + def load(self) -> list[Document]: + with open(self.file_path, 'rb') as f: + headers = {} + if self.api_key: + headers['X-Api-Key'] = f'{self.api_key}' + + r = requests.post( + f'{self.url}/v1/convert/file', + files={ + 'files': ( + self.file_path, + f, + self.mime_type or 'application/octet-stream', + ) + }, + data={ + 'image_export_mode': 'placeholder', + **self.params, + }, + headers=headers, + verify=AIOHTTP_CLIENT_SESSION_SSL, + ) + if r.ok: + result = r.json() + document_data = result.get('document', {}) + text = document_data.get('md_content', '') + + metadata = {'Content-Type': self.mime_type} if self.mime_type else {} + + log.debug('Docling extracted text: %s', text) + return [Document(page_content=text, metadata=metadata)] + else: + error_msg = f'Error calling Docling API: {r.reason}' + if r.text: + try: + error_data = r.json() + if 'detail' in error_data: + error_msg += f' - {error_data["detail"]}' + except Exception: + error_msg += f' - {r.text}' + raise Exception(f'Error calling Docling: {error_msg}') + + +class Loader: + def __init__(self, engine: str = '', **kwargs): + self.engine = engine + self.user = kwargs.get('user', None) + self.kwargs = kwargs + + def load(self, filename: str, file_content_type: str, file_path: str) -> list[Document]: + loader = self._get_loader(filename, file_content_type, file_path) + docs = loader.load() + + return [Document(page_content=ftfy.fix_text(doc.page_content), metadata=doc.metadata) for doc in docs] + + async def aload(self, filename: str, file_content_type: str, file_path: str) -> list[Document]: + """ + Async wrapper around `load`. + + Document loaders dispatched by `_get_loader` (PyMuPDF, Unstructured, + python-docx, Tika, etc.) are uniformly synchronous and CPU/IO-bound. + Calling `load` directly from an async handler would block the event + loop for the entire parse — minutes for large PDFs. This offloads + the work to a worker thread so the loop stays responsive. + """ + return await asyncio.to_thread(self.load, filename, file_content_type, file_path) + + def _is_text_file(self, file_ext: str, file_content_type: str) -> bool: + return file_ext in known_source_ext or ( + file_content_type + and file_content_type.find('text/') >= 0 + # Avoid text/html files being detected as text + and not file_content_type.find('html') >= 0 + ) + + def _get_loader(self, filename: str, file_content_type: str, file_path: str): + file_ext = filename.split('.')[-1].lower() + + if ( + self.engine == 'external' + and self.kwargs.get('EXTERNAL_DOCUMENT_LOADER_URL') + and self.kwargs.get('EXTERNAL_DOCUMENT_LOADER_API_KEY') + ): + loader = ExternalDocumentLoader( + file_path=file_path, + url=self.kwargs.get('EXTERNAL_DOCUMENT_LOADER_URL'), + api_key=self.kwargs.get('EXTERNAL_DOCUMENT_LOADER_API_KEY'), + mime_type=file_content_type, + user=self.user, + ) + elif self.engine == 'tika' and self.kwargs.get('TIKA_SERVER_URL'): + if self._is_text_file(file_ext, file_content_type): + loader = TextLoader(file_path, autodetect_encoding=True) + else: + loader = TikaLoader( + url=self.kwargs.get('TIKA_SERVER_URL'), + file_path=file_path, + extract_images=self.kwargs.get('PDF_EXTRACT_IMAGES'), + ) + elif ( + self.engine == 'datalab_marker' + and self.kwargs.get('DATALAB_MARKER_API_KEY') + and file_ext + in [ + 'pdf', + 'xls', + 'xlsx', + 'ods', + 'doc', + 'docx', + 'odt', + 'ppt', + 'pptx', + 'odp', + 'html', + 'epub', + 'png', + 'jpeg', + 'jpg', + 'webp', + 'gif', + 'tiff', + ] + ): + api_base_url = self.kwargs.get('DATALAB_MARKER_API_BASE_URL', '') + if not api_base_url or api_base_url.strip() == '': + api_base_url = 'https://www.datalab.to/api/v1/marker' # https://github.com/open-webui/open-webui/pull/16867#issuecomment-3218424349 + + loader = DatalabMarkerLoader( + file_path=file_path, + api_key=self.kwargs['DATALAB_MARKER_API_KEY'], + api_base_url=api_base_url, + additional_config=self.kwargs.get('DATALAB_MARKER_ADDITIONAL_CONFIG'), + use_llm=self.kwargs.get('DATALAB_MARKER_USE_LLM', False), + skip_cache=self.kwargs.get('DATALAB_MARKER_SKIP_CACHE', False), + force_ocr=self.kwargs.get('DATALAB_MARKER_FORCE_OCR', False), + paginate=self.kwargs.get('DATALAB_MARKER_PAGINATE', False), + strip_existing_ocr=self.kwargs.get('DATALAB_MARKER_STRIP_EXISTING_OCR', False), + disable_image_extraction=self.kwargs.get('DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION', False), + format_lines=self.kwargs.get('DATALAB_MARKER_FORMAT_LINES', False), + output_format=self.kwargs.get('DATALAB_MARKER_OUTPUT_FORMAT', 'markdown'), + ) + elif self.engine == 'docling' and self.kwargs.get('DOCLING_SERVER_URL'): + if self._is_text_file(file_ext, file_content_type): + loader = TextLoader(file_path, autodetect_encoding=True) + else: + # Build params for DoclingLoader + params = self.kwargs.get('DOCLING_PARAMS', {}) + if not isinstance(params, dict): + try: + params = json.loads(params) + except json.JSONDecodeError: + log.error('Invalid DOCLING_PARAMS format, expected JSON object') + params = {} + + loader = DoclingLoader( + url=self.kwargs.get('DOCLING_SERVER_URL'), + api_key=self.kwargs.get('DOCLING_API_KEY', None), + file_path=file_path, + mime_type=file_content_type, + params=params, + ) + elif ( + self.engine == 'document_intelligence' + and self.kwargs.get('DOCUMENT_INTELLIGENCE_ENDPOINT') != '' + and ( + file_ext in ['pdf', 'docx', 'ppt', 'pptx'] + or file_content_type + in [ + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + ] + ) + ): + if self.kwargs.get('DOCUMENT_INTELLIGENCE_KEY') != '': + loader = AzureAIDocumentIntelligenceLoader( + file_path=file_path, + api_endpoint=self.kwargs.get('DOCUMENT_INTELLIGENCE_ENDPOINT'), + api_key=self.kwargs.get('DOCUMENT_INTELLIGENCE_KEY'), + api_model=self.kwargs.get('DOCUMENT_INTELLIGENCE_MODEL'), + ) + else: + loader = AzureAIDocumentIntelligenceLoader( + file_path=file_path, + api_endpoint=self.kwargs.get('DOCUMENT_INTELLIGENCE_ENDPOINT'), + azure_credential=DefaultAzureCredential(), + api_model=self.kwargs.get('DOCUMENT_INTELLIGENCE_MODEL'), + ) + elif self.engine == 'mineru' and file_ext in ['pdf']: # MinerU currently only supports PDF + mineru_timeout = self.kwargs.get('MINERU_API_TIMEOUT', 300) + if mineru_timeout: + try: + mineru_timeout = int(mineru_timeout) + except ValueError: + mineru_timeout = 300 + + loader = MinerULoader( + file_path=file_path, + api_mode=self.kwargs.get('MINERU_API_MODE', 'local'), + api_url=self.kwargs.get('MINERU_API_URL', 'http://localhost:8000'), + api_key=self.kwargs.get('MINERU_API_KEY', ''), + params=self.kwargs.get('MINERU_PARAMS', {}), + timeout=mineru_timeout, + ) + elif ( + self.engine == 'mistral_ocr' + and self.kwargs.get('MISTRAL_OCR_API_KEY') != '' + and file_ext in ['pdf'] # Mistral OCR currently only supports PDF and images + ): + loader = MistralLoader( + base_url=self.kwargs.get('MISTRAL_OCR_API_BASE_URL'), + api_key=self.kwargs.get('MISTRAL_OCR_API_KEY'), + file_path=file_path, + ) + elif self.engine == 'paddleocr_vl' and self.kwargs.get('PADDLEOCR_VL_TOKEN') != '': + loader = PaddleOCRVLLoader( + api_url=self.kwargs.get('PADDLEOCR_VL_BASE_URL'), + token=self.kwargs.get('PADDLEOCR_VL_TOKEN'), + file_path=file_path, + ) + else: + if file_ext == 'pdf': + loader = PyPDFLoader( + file_path, + extract_images=self.kwargs.get('PDF_EXTRACT_IMAGES'), + mode=self.kwargs.get('PDF_LOADER_MODE', 'page'), + ) + elif file_ext == 'csv': + loader = CSVLoader(file_path, autodetect_encoding=True) + elif file_ext == 'rst': + try: + from langchain_community.document_loaders import UnstructuredRSTLoader + + loader = UnstructuredRSTLoader(file_path, mode='elements') + except ImportError: + log.warning( + "The 'unstructured' package is not installed. " + 'Falling back to plain text loading for .rst file. ' + 'Install it with: pip install unstructured' + ) + loader = TextLoader(file_path, autodetect_encoding=True) + elif file_ext == 'xml': + try: + from langchain_community.document_loaders import UnstructuredXMLLoader + + loader = UnstructuredXMLLoader(file_path) + except ImportError: + log.warning( + "The 'unstructured' package is not installed. " + 'Falling back to plain text loading for .xml file. ' + 'Install it with: pip install unstructured' + ) + loader = TextLoader(file_path, autodetect_encoding=True) + elif file_ext in ['htm', 'html']: + loader = BSHTMLLoader(file_path, open_encoding='unicode_escape') + elif file_ext == 'md': + loader = TextLoader(file_path, autodetect_encoding=True) + elif file_content_type == 'application/epub+zip': + try: + from langchain_community.document_loaders import UnstructuredEPubLoader + + loader = UnstructuredEPubLoader(file_path) + except ImportError: + raise ValueError( + "Processing .epub files requires the 'unstructured' package. " + 'Install it with: pip install unstructured' + ) + elif ( + file_content_type == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + or file_ext == 'docx' + ): + loader = Docx2txtLoader(file_path) + elif file_content_type in [ + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ] or file_ext in ['xls', 'xlsx']: + try: + from langchain_community.document_loaders import UnstructuredExcelLoader + + loader = UnstructuredExcelLoader(file_path) + except ImportError: + log.warning( + "The 'unstructured' package is not installed. " + 'Falling back to pandas for Excel file loading. ' + 'Install unstructured for better results: pip install unstructured' + ) + loader = ExcelLoader(file_path) + elif file_content_type in [ + 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + ] or file_ext in ['ppt', 'pptx']: + try: + from langchain_community.document_loaders import UnstructuredPowerPointLoader + + loader = UnstructuredPowerPointLoader(file_path) + except ImportError: + log.warning( + "The 'unstructured' package is not installed. " + 'Falling back to python-pptx for PowerPoint file loading. ' + 'Install unstructured for better results: pip install unstructured' + ) + loader = PptxLoader(file_path) + elif file_ext == 'msg': + loader = OutlookMessageLoader(file_path) + elif file_ext == 'odt': + try: + from langchain_community.document_loaders import UnstructuredODTLoader + + loader = UnstructuredODTLoader(file_path) + except ImportError: + raise ValueError( + "Processing .odt files requires the 'unstructured' package. " + 'Install it with: pip install unstructured' + ) + elif self._is_text_file(file_ext, file_content_type): + loader = TextLoader(file_path, autodetect_encoding=True) + else: + loader = TextLoader(file_path, autodetect_encoding=True) + + return loader diff --git a/backend/open_webui/retrieval/loaders/mineru.py b/backend/open_webui/retrieval/loaders/mineru.py new file mode 100644 index 0000000000000000000000000000000000000000..1f0848a61370a8ebe03af5d702fe31187267ce6c --- /dev/null +++ b/backend/open_webui/retrieval/loaders/mineru.py @@ -0,0 +1,506 @@ +import os +import time +import requests +import logging +import tempfile +import zipfile +from typing import List, Optional +from langchain_core.documents import Document +from fastapi import HTTPException, status + +log = logging.getLogger(__name__) + + +class MinerULoader: + """ + MinerU document parser loader supporting both Cloud API and Local API modes. + + Cloud API: Uses MinerU managed service with async task-based processing + Local API: Uses self-hosted MinerU API with synchronous processing + """ + + def __init__( + self, + file_path: str, + api_mode: str = 'local', + api_url: str = 'http://localhost:8000', + api_key: str = '', + params: dict = None, + timeout: Optional[int] = 300, + ): + self.file_path = file_path + self.api_mode = api_mode.lower() + self.api_url = api_url.rstrip('/') + self.api_key = api_key + self.timeout = timeout + + # Parse params dict with defaults + self.params = params or {} + self.enable_ocr = params.get('enable_ocr', False) + self.enable_formula = params.get('enable_formula', True) + self.enable_table = params.get('enable_table', True) + self.language = params.get('language', 'en') + self.model_version = params.get('model_version', 'pipeline') + + self.page_ranges = self.params.pop('page_ranges', '') + + # Validate API mode + if self.api_mode not in ['local', 'cloud']: + raise ValueError(f"Invalid API mode: {self.api_mode}. Must be 'local' or 'cloud'") + + # Validate Cloud API requirements + if self.api_mode == 'cloud' and not self.api_key: + raise ValueError('API key is required for Cloud API mode') + + def load(self) -> List[Document]: + """ + Main entry point for loading and parsing the document. + Routes to Cloud or Local API based on api_mode. + """ + try: + if self.api_mode == 'cloud': + return self._load_cloud_api() + else: + return self._load_local_api() + except Exception as e: + log.error(f'Error loading document with MinerU: {e}') + raise + + def _load_local_api(self) -> List[Document]: + """ + Load document using Local API (synchronous). + Posts file to /file_parse endpoint and gets immediate response. + """ + log.info(f'Using MinerU Local API at {self.api_url}') + + filename = os.path.basename(self.file_path) + + # Build form data for Local API + form_data = { + **self.params, + 'return_md': 'true', + } + + # Page ranges (Local API uses start_page_id and end_page_id) + if self.page_ranges: + # For simplicity, if page_ranges is specified, log a warning + # Full page range parsing would require parsing the string + log.warning( + f"Page ranges '{self.page_ranges}' specified but Local API uses different format. " + 'Consider using start_page_id/end_page_id parameters if needed.' + ) + + try: + with open(self.file_path, 'rb') as f: + files = {'files': (filename, f, 'application/octet-stream')} + + log.info(f'Sending file to MinerU Local API: {filename}') + log.debug(f'Local API parameters: {form_data}') + + response = requests.post( + f'{self.api_url}/file_parse', + data=form_data, + files=files, + timeout=self.timeout, + ) + response.raise_for_status() + + except FileNotFoundError: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail=f'File not found: {self.file_path}') + except requests.Timeout: + raise HTTPException( + status.HTTP_504_GATEWAY_TIMEOUT, + detail='MinerU Local API request timed out', + ) + except requests.HTTPError as e: + error_detail = f'MinerU Local API request failed: {e}' + if e.response is not None: + try: + error_data = e.response.json() + error_detail += f' - {error_data}' + except Exception: + error_detail += f' - {e.response.text}' + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=error_detail) + except Exception as e: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f'Error calling MinerU Local API: {str(e)}', + ) + + # Parse response + try: + result = response.json() + except ValueError as e: + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail=f'Invalid JSON response from MinerU Local API: {e}', + ) + + # Extract markdown content from response + if 'results' not in result: + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail="MinerU Local API response missing 'results' field", + ) + + results = result['results'] + if not results: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail='MinerU returned empty results', + ) + + # Get the first (and typically only) result + file_result = list(results.values())[0] + markdown_content = file_result.get('md_content', '') + + if not markdown_content: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail='MinerU returned empty markdown content', + ) + + log.info(f'Successfully parsed document with MinerU Local API: {filename}') + + # Create metadata + metadata = { + 'source': filename, + 'api_mode': 'local', + 'backend': result.get('backend', 'unknown'), + 'version': result.get('version', 'unknown'), + } + + return [Document(page_content=markdown_content, metadata=metadata)] + + def _load_cloud_api(self) -> List[Document]: + """ + Load document using Cloud API (asynchronous). + Uses batch upload endpoint to avoid need for public file URLs. + """ + log.info(f'Using MinerU Cloud API at {self.api_url}') + + filename = os.path.basename(self.file_path) + + # Step 1: Request presigned upload URL + batch_id, upload_url = self._request_upload_url(filename) + + # Step 2: Upload file to presigned URL + self._upload_to_presigned_url(upload_url) + + # Step 3: Poll for results + result = self._poll_batch_status(batch_id, filename) + + # Step 4: Download and extract markdown from ZIP + markdown_content = self._download_and_extract_zip(result['full_zip_url'], filename) + + log.info(f'Successfully parsed document with MinerU Cloud API: {filename}') + + # Create metadata + metadata = { + 'source': filename, + 'api_mode': 'cloud', + 'batch_id': batch_id, + } + + return [Document(page_content=markdown_content, metadata=metadata)] + + def _request_upload_url(self, filename: str) -> tuple: + """ + Request presigned upload URL from Cloud API. + Returns (batch_id, upload_url). + """ + headers = { + 'Authorization': f'Bearer {self.api_key}', + 'Content-Type': 'application/json', + } + + # Build request body + request_body = { + **self.params, + 'files': [ + { + 'name': filename, + 'is_ocr': self.enable_ocr, + } + ], + } + + # Add page ranges if specified + if self.page_ranges: + request_body['files'][0]['page_ranges'] = self.page_ranges + + log.info(f'Requesting upload URL for: {filename}') + log.debug(f'Cloud API request body: {request_body}') + + try: + response = requests.post( + f'{self.api_url}/file-urls/batch', + headers=headers, + json=request_body, + timeout=30, + ) + response.raise_for_status() + except requests.HTTPError as e: + error_detail = f'Failed to request upload URL: {e}' + if e.response is not None: + try: + error_data = e.response.json() + error_detail += f' - {error_data.get("msg", error_data)}' + except Exception: + error_detail += f' - {e.response.text}' + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=error_detail) + except Exception as e: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f'Error requesting upload URL: {str(e)}', + ) + + try: + result = response.json() + except ValueError as e: + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail=f'Invalid JSON response: {e}', + ) + + # Check for API error response + if result.get('code') != 0: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f'MinerU Cloud API error: {result.get("msg", "Unknown error")}', + ) + + data = result.get('data', {}) + batch_id = data.get('batch_id') + file_urls = data.get('file_urls', []) + + if not batch_id or not file_urls: + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail='MinerU Cloud API response missing batch_id or file_urls', + ) + + upload_url = file_urls[0] + log.info(f'Received upload URL for batch: {batch_id}') + + return batch_id, upload_url + + def _upload_to_presigned_url(self, upload_url: str) -> None: + """ + Upload file to presigned URL (no authentication needed). + """ + log.info(f'Uploading file to presigned URL') + + try: + with open(self.file_path, 'rb') as f: + response = requests.put( + upload_url, + data=f, + timeout=self.timeout, + ) + response.raise_for_status() + except FileNotFoundError: + raise HTTPException(status.HTTP_404_NOT_FOUND, detail=f'File not found: {self.file_path}') + except requests.Timeout: + raise HTTPException( + status.HTTP_504_GATEWAY_TIMEOUT, + detail='File upload to presigned URL timed out', + ) + except requests.HTTPError as e: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f'Failed to upload file to presigned URL: {e}', + ) + except Exception as e: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f'Error uploading file: {str(e)}', + ) + + log.info('File uploaded successfully') + + def _poll_batch_status(self, batch_id: str, filename: str) -> dict: + """ + Poll batch status until completion. + Returns the result dict for the file. + """ + headers = { + 'Authorization': f'Bearer {self.api_key}', + } + + max_iterations = 300 # 10 minutes max (2 seconds per iteration) + poll_interval = 2 # seconds + + log.info(f'Polling batch status: {batch_id}') + + for iteration in range(max_iterations): + try: + response = requests.get( + f'{self.api_url}/extract-results/batch/{batch_id}', + headers=headers, + timeout=30, + ) + response.raise_for_status() + except requests.HTTPError as e: + error_detail = f'Failed to poll batch status: {e}' + if e.response is not None: + try: + error_data = e.response.json() + error_detail += f' - {error_data.get("msg", error_data)}' + except Exception: + error_detail += f' - {e.response.text}' + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=error_detail) + except Exception as e: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f'Error polling batch status: {str(e)}', + ) + + try: + result = response.json() + except ValueError as e: + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail=f'Invalid JSON response while polling: {e}', + ) + + # Check for API error response + if result.get('code') != 0: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f'MinerU Cloud API error: {result.get("msg", "Unknown error")}', + ) + + data = result.get('data', {}) + extract_result = data.get('extract_result', []) + + # Find our file in the batch results + file_result = None + for item in extract_result: + if item.get('file_name') == filename: + file_result = item + break + + if not file_result: + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail=f'File {filename} not found in batch results', + ) + + state = file_result.get('state') + + if state == 'done': + log.info(f'Processing complete for {filename}') + return file_result + elif state == 'failed': + error_msg = file_result.get('err_msg', 'Unknown error') + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f'MinerU processing failed: {error_msg}', + ) + elif state in ['waiting-file', 'pending', 'running', 'converting']: + # Still processing + if iteration % 10 == 0: # Log every 20 seconds + log.info(f'Processing status: {state} (iteration {iteration + 1}/{max_iterations})') + time.sleep(poll_interval) + else: + log.warning(f'Unknown state: {state}') + time.sleep(poll_interval) + + # Timeout + raise HTTPException( + status.HTTP_504_GATEWAY_TIMEOUT, + detail='MinerU processing timed out after 10 minutes', + ) + + def _download_and_extract_zip(self, zip_url: str, filename: str) -> str: + """ + Download ZIP file from CDN and extract markdown content. + Returns the markdown content as a string. + """ + log.info(f'Downloading results from: {zip_url}') + + try: + response = requests.get(zip_url, timeout=60) + response.raise_for_status() + except requests.HTTPError as e: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail=f'Failed to download results ZIP: {e}', + ) + except Exception as e: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f'Error downloading results: {str(e)}', + ) + + # Save ZIP to temporary file and extract + try: + with tempfile.NamedTemporaryFile(delete=False, suffix='.zip') as tmp_zip: + tmp_zip.write(response.content) + tmp_zip_path = tmp_zip.name + + with tempfile.TemporaryDirectory() as tmp_dir: + # Extract ZIP + with zipfile.ZipFile(tmp_zip_path, 'r') as zip_ref: + zip_ref.extractall(tmp_dir) + + # Find markdown file - search recursively for any .md file + markdown_content = None + found_md_path = None + + # First, list all files in the ZIP for debugging + all_files = [] + for root, dirs, files in os.walk(tmp_dir): + for file in files: + full_path = os.path.join(root, file) + all_files.append(full_path) + # Look for any .md file + if file.endswith('.md'): + found_md_path = full_path + log.info(f'Found markdown file at: {full_path}') + try: + with open(full_path, 'r', encoding='utf-8') as f: + markdown_content = f.read() + if markdown_content: # Use the first non-empty markdown file + break + except Exception as e: + log.warning(f'Failed to read {full_path}: {e}') + if markdown_content: + break + + if markdown_content is None: + log.error(f'Available files in ZIP: {all_files}') + # Try to provide more helpful error message + md_files = [f for f in all_files if f.endswith('.md')] + if md_files: + error_msg = f"Found .md files but couldn't read them: {md_files}" + else: + error_msg = f'No .md files found in ZIP. Available files: {all_files}' + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail=error_msg, + ) + + # Clean up temporary ZIP file + os.unlink(tmp_zip_path) + + except zipfile.BadZipFile as e: + raise HTTPException( + status.HTTP_502_BAD_GATEWAY, + detail=f'Invalid ZIP file received: {e}', + ) + except Exception as e: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f'Error extracting ZIP: {str(e)}', + ) + + if not markdown_content: + raise HTTPException( + status.HTTP_400_BAD_REQUEST, + detail='Extracted markdown content is empty', + ) + + log.info(f'Successfully extracted markdown content ({len(markdown_content)} characters)') + return markdown_content diff --git a/backend/open_webui/retrieval/loaders/mistral.py b/backend/open_webui/retrieval/loaders/mistral.py new file mode 100644 index 0000000000000000000000000000000000000000..b3d274ee7c35c9d119bdc1e72bb8a232249329d8 --- /dev/null +++ b/backend/open_webui/retrieval/loaders/mistral.py @@ -0,0 +1,721 @@ +import requests +import aiohttp +import asyncio +import logging +import os +import sys +import time +from typing import List, Dict, Any +from contextlib import asynccontextmanager + +from langchain_core.documents import Document +from open_webui.env import GLOBAL_LOG_LEVEL, AIOHTTP_CLIENT_SESSION_SSL + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) +log = logging.getLogger(__name__) + + +class MistralLoader: + """ + Enhanced Mistral OCR loader with both sync and async support. + Loads documents by processing them through the Mistral OCR API. + + Performance Optimizations: + - Differentiated timeouts for different operations + - Intelligent retry logic with exponential backoff + - Memory-efficient file streaming for large files + - Connection pooling and keepalive optimization + - Semaphore-based concurrency control for batch processing + - Enhanced error handling with retryable error classification + """ + + def __init__( + self, + base_url: str, + api_key: str, + file_path: str, + timeout: int = 300, # 5 minutes default + max_retries: int = 3, + enable_debug_logging: bool = False, + ): + """ + Initializes the loader with enhanced features. + + Args: + api_key: Your Mistral API key. + file_path: The local path to the PDF file to process. + timeout: Request timeout in seconds. + max_retries: Maximum number of retry attempts. + enable_debug_logging: Enable detailed debug logs. + """ + if not api_key: + raise ValueError('API key cannot be empty.') + if not os.path.exists(file_path): + raise FileNotFoundError(f'File not found at {file_path}') + + self.base_url = base_url.rstrip('/') if base_url else 'https://api.mistral.ai/v1' + self.api_key = api_key + self.file_path = file_path + self.timeout = timeout + self.max_retries = max_retries + self.debug = enable_debug_logging + + # PERFORMANCE OPTIMIZATION: Differentiated 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 + self.url_timeout = 30 # URL requests should be fast - fail quickly if API is slow + 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 + # 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 + self.headers = { + 'Authorization': f'Bearer {self.api_key}', + 'User-Agent': 'OpenWebUI-MistralLoader/2.0', # Helps API provider track usage + } + + def _debug_log(self, message: str, *args) -> None: + """ + PERFORMANCE OPTIMIZATION: Conditional debug logging for performance. + + Only processes debug messages when debug mode is enabled, avoiding + string formatting overhead in production environments. + """ + if self.debug: + log.debug(message, *args) + + def _handle_response(self, response: requests.Response) -> Dict[str, Any]: + """Checks response status and returns JSON content.""" + try: + response.raise_for_status() # Raises HTTPError for bad responses (4xx or 5xx) + # Handle potential empty responses for certain successful requests (e.g., DELETE) + if response.status_code == 204 or not response.content: + return {} # Return empty dict if no content + return response.json() + except requests.exceptions.HTTPError as http_err: + log.error(f'HTTP error occurred: {http_err} - Response: {response.text}') + raise + except requests.exceptions.RequestException as req_err: + log.error(f'Request exception occurred: {req_err}') + raise + except ValueError as json_err: # Includes JSONDecodeError + log.error(f'JSON decode error: {json_err} - Response: {response.text}') + raise # Re-raise after logging + + async def _handle_response_async(self, response: aiohttp.ClientResponse) -> Dict[str, Any]: + """Async version of response handling with better error info.""" + try: + response.raise_for_status() + + # Check content type + content_type = response.headers.get('content-type', '') + if 'application/json' not in content_type: + if response.status == 204: + return {} + text = await response.text() + raise ValueError(f'Unexpected content type: {content_type}, body: {text[:200]}...') + + return await response.json() + + except aiohttp.ClientResponseError as e: + error_text = await response.text() if response else 'No response' + log.error(f'HTTP {e.status}: {e.message} - Response: {error_text[:500]}') + raise + except aiohttp.ClientError as e: + log.error(f'Client error: {e}') + raise + except Exception as e: + log.error(f'Unexpected error processing response: {e}') + raise + + def _is_retryable_error(self, error: Exception) -> bool: + """ + ENHANCEMENT: Intelligent error classification for retry logic. + + Determines if an error is retryable based on its type and status code. + This prevents wasting time retrying errors that will never succeed + (like authentication errors) while ensuring transient errors are retried. + + Retryable errors: + - Network connection errors (temporary network issues) + - Timeouts (server might be temporarily overloaded) + - Server errors (5xx status codes - server-side issues) + - Rate limiting (429 status - temporary throttling) + + Non-retryable errors: + - Authentication errors (401, 403 - won't fix with retry) + - Bad request errors (400 - malformed request) + - Not found errors (404 - resource doesn't exist) + """ + if isinstance(error, requests.exceptions.ConnectionError): + return True # Network issues are usually temporary + if isinstance(error, requests.exceptions.Timeout): + return True # Timeouts might resolve on retry + if isinstance(error, requests.exceptions.HTTPError): + # Only retry on server errors (5xx) or rate limits (429) + if hasattr(error, 'response') and error.response is not None: + status_code = error.response.status_code + return status_code >= 500 or status_code == 429 + return False + if isinstance(error, (aiohttp.ClientConnectionError, aiohttp.ServerTimeoutError)): + return True # Async network/timeout errors are retryable + if isinstance(error, aiohttp.ClientResponseError): + return error.status >= 500 or error.status == 429 + return False # All other errors are non-retryable + + def _retry_request_sync(self, request_func, *args, **kwargs): + """ + ENHANCEMENT: Synchronous retry logic with intelligent error classification. + + Uses exponential backoff with jitter to avoid thundering herd problems. + The wait time increases exponentially but is capped at 30 seconds to + prevent excessive delays. Only retries errors that are likely to succeed + on subsequent attempts. + """ + for attempt in range(self.max_retries): + try: + return request_func(*args, **kwargs) + except Exception as e: + if attempt == self.max_retries - 1 or not self._is_retryable_error(e): + raise + + # PERFORMANCE OPTIMIZATION: Exponential backoff with cap + # Prevents overwhelming the server while ensuring reasonable retry delays + wait_time = min((2**attempt) + 0.5, 30) # Cap at 30 seconds + log.warning( + f'Retryable error (attempt {attempt + 1}/{self.max_retries}): {e}. Retrying in {wait_time}s...' + ) + time.sleep(wait_time) + + async def _retry_request_async(self, request_func, *args, **kwargs): + """ + ENHANCEMENT: Async retry logic with intelligent error classification. + + Async version of retry logic that doesn't block the event loop during + wait periods. Uses the same exponential backoff strategy as sync version. + """ + for attempt in range(self.max_retries): + try: + return await request_func(*args, **kwargs) + except Exception as e: + if attempt == self.max_retries - 1 or not self._is_retryable_error(e): + raise + + # PERFORMANCE OPTIMIZATION: Non-blocking exponential backoff + wait_time = min((2**attempt) + 0.5, 30) # Cap at 30 seconds + log.warning( + f'Retryable error (attempt {attempt + 1}/{self.max_retries}): {e}. Retrying in {wait_time}s...' + ) + await asyncio.sleep(wait_time) # Non-blocking wait + + def _upload_file(self) -> str: + """ + PERFORMANCE OPTIMIZATION: Enhanced file upload with streaming consideration. + + Uploads the file to Mistral for OCR processing (sync version). + Uses context manager for file handling to ensure proper resource cleanup. + Although streaming is not enabled for this endpoint, the file is opened + in a context manager to minimize memory usage duration. + """ + log.info('Uploading file to Mistral API') + url = f'{self.base_url}/files' + + def upload_request(): + # MEMORY OPTIMIZATION: Use context manager to minimize file handle lifetime + # 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')} + data = {'purpose': 'ocr'} + + # NOTE: stream=False is required for this endpoint + # The Mistral API doesn't support chunked uploads for this endpoint + response = requests.post( + url, + headers=self.headers, + files=files, + data=data, + timeout=self.upload_timeout, # Use specialized upload timeout + stream=False, # Keep as False for this endpoint + ) + + return self._handle_response(response) + + try: + response_data = self._retry_request_sync(upload_request) + file_id = response_data.get('id') + if not file_id: + raise ValueError('File ID not found in upload response.') + log.info(f'File uploaded successfully. File ID: {file_id}') + return file_id + except Exception as e: + log.error(f'Failed to upload file: {e}') + raise + + async def _upload_file_async(self, session: aiohttp.ClientSession) -> str: + """Async file upload with streaming for better memory efficiency.""" + url = f'{self.base_url}/files' + + async def upload_request(): + # Create multipart writer for streaming upload + writer = aiohttp.MultipartWriter('form-data') + + # Add purpose field + purpose_part = writer.append('ocr') + purpose_part.set_content_disposition('form-data', name='purpose') + + # Add file part with streaming + file_part = writer.append_payload( + aiohttp.streams.FilePayload( + self.file_path, + filename=self.file_name, + content_type='application/pdf', + ) + ) + file_part.set_content_disposition('form-data', name='file', filename=self.file_name) + + self._debug_log(f'Uploading file: {self.file_name} ({self.file_size:,} bytes)') + + async with session.post( + url, + data=writer, + headers=self.headers, + timeout=aiohttp.ClientTimeout(total=self.upload_timeout), + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + return await self._handle_response_async(response) + + response_data = await self._retry_request_async(upload_request) + + file_id = response_data.get('id') + if not file_id: + raise ValueError('File ID not found in upload response.') + + log.info(f'File uploaded successfully. File ID: {file_id}') + return file_id + + def _get_signed_url(self, file_id: str) -> str: + """Retrieves a temporary signed URL for the uploaded file (sync version).""" + log.info(f'Getting signed URL for file ID: {file_id}') + url = f'{self.base_url}/files/{file_id}/url' + params = {'expiry': 1} + signed_url_headers = {**self.headers, 'Accept': 'application/json'} + + def url_request(): + response = requests.get(url, headers=signed_url_headers, params=params, timeout=self.url_timeout) + return self._handle_response(response) + + try: + response_data = self._retry_request_sync(url_request) + signed_url = response_data.get('url') + if not signed_url: + raise ValueError('Signed URL not found in response.') + log.info('Signed URL received.') + return signed_url + except Exception as e: + log.error(f'Failed to get signed URL: {e}') + raise + + async def _get_signed_url_async(self, session: aiohttp.ClientSession, file_id: str) -> str: + """Async signed URL retrieval.""" + url = f'{self.base_url}/files/{file_id}/url' + params = {'expiry': 1} + + headers = {**self.headers, 'Accept': 'application/json'} + + async def url_request(): + self._debug_log(f'Getting signed URL for file ID: {file_id}') + async with session.get( + url, + headers=headers, + params=params, + timeout=aiohttp.ClientTimeout(total=self.url_timeout), + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + return await self._handle_response_async(response) + + response_data = await self._retry_request_async(url_request) + + signed_url = response_data.get('url') + if not signed_url: + raise ValueError('Signed URL not found in response.') + + self._debug_log('Signed URL received successfully') + return signed_url + + def _process_ocr(self, signed_url: str) -> Dict[str, Any]: + """Sends the signed URL to the OCR endpoint for processing (sync version).""" + log.info('Processing OCR via Mistral API') + url = f'{self.base_url}/ocr' + ocr_headers = { + **self.headers, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + payload = { + 'model': 'mistral-ocr-latest', + 'document': { + 'type': 'document_url', + 'document_url': signed_url, + }, + 'include_image_base64': False, + } + + def ocr_request(): + response = requests.post(url, headers=ocr_headers, json=payload, timeout=self.ocr_timeout) + return self._handle_response(response) + + try: + ocr_response = self._retry_request_sync(ocr_request) + log.info('OCR processing done.') + self._debug_log('OCR response: %s', ocr_response) + return ocr_response + except Exception as e: + log.error(f'Failed during OCR processing: {e}') + raise + + async def _process_ocr_async(self, session: aiohttp.ClientSession, signed_url: str) -> Dict[str, Any]: + """Async OCR processing with timing metrics.""" + url = f'{self.base_url}/ocr' + + headers = { + **self.headers, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + + payload = { + 'model': 'mistral-ocr-latest', + 'document': { + 'type': 'document_url', + 'document_url': signed_url, + }, + 'include_image_base64': False, + } + + async def ocr_request(): + log.info('Starting OCR processing via Mistral API') + start_time = time.time() + + async with session.post( + url, + json=payload, + headers=headers, + timeout=aiohttp.ClientTimeout(total=self.ocr_timeout), + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + ocr_response = await self._handle_response_async(response) + + processing_time = time.time() - start_time + log.info(f'OCR processing completed in {processing_time:.2f}s') + + return ocr_response + + return await self._retry_request_async(ocr_request) + + def _delete_file(self, file_id: str) -> None: + """Deletes the file from Mistral storage (sync version).""" + log.info(f'Deleting uploaded file ID: {file_id}') + url = f'{self.base_url}/files/{file_id}' + + try: + response = requests.delete(url, headers=self.headers, timeout=self.cleanup_timeout) + delete_response = self._handle_response(response) + log.info(f'File deleted successfully: {delete_response}') + except Exception as e: + # Log error but don't necessarily halt execution if deletion fails + log.error(f'Failed to delete file ID {file_id}: {e}') + + async def _delete_file_async(self, session: aiohttp.ClientSession, file_id: str) -> None: + """Async file deletion with error tolerance.""" + try: + + async def delete_request(): + self._debug_log(f'Deleting file ID: {file_id}') + async with session.delete( + url=f'{self.base_url}/files/{file_id}', + headers=self.headers, + timeout=aiohttp.ClientTimeout(total=self.cleanup_timeout), + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + return await self._handle_response_async(response) + + await self._retry_request_async(delete_request) + self._debug_log(f'File {file_id} deleted successfully') + + except Exception as e: + # Don't fail the entire process if cleanup fails + log.warning(f'Failed to delete file ID {file_id}: {e}') + + @asynccontextmanager + async def _get_session(self): + """Context manager for HTTP session with optimized settings.""" + connector = aiohttp.TCPConnector( + limit=20, # Increased total connection limit for better throughput + limit_per_host=10, # Increased per-host limit for API endpoints + ttl_dns_cache=600, # Longer DNS cache TTL (10 minutes) + use_dns_cache=True, + keepalive_timeout=60, # Increased keepalive for connection reuse + enable_cleanup_closed=True, + force_close=False, # Allow connection reuse + resolver=aiohttp.AsyncResolver(), # Use async DNS resolver + ) + + timeout = aiohttp.ClientTimeout( + total=self.timeout, + connect=30, # Connection timeout + sock_read=60, # Socket read timeout + ) + + async with aiohttp.ClientSession( + connector=connector, + timeout=timeout, + headers={'User-Agent': 'OpenWebUI-MistralLoader/2.0'}, + raise_for_status=False, # We handle status codes manually + trust_env=True, + ) as session: + yield session + + def _process_results(self, ocr_response: Dict[str, Any]) -> List[Document]: + """Process OCR results into Document objects with enhanced metadata and memory efficiency.""" + pages_data = ocr_response.get('pages') + if not pages_data: + log.warning('No pages found in OCR response.') + return [ + Document( + page_content='No text content found', + metadata={'error': 'no_pages', 'file_name': self.file_name}, + ) + ] + + documents = [] + total_pages = len(pages_data) + skipped_pages = 0 + + # Process pages in a memory-efficient way + for page_data in pages_data: + page_content = page_data.get('markdown') + page_index = page_data.get('index') # API uses 0-based index + + if page_content is None or page_index is None: + skipped_pages += 1 + self._debug_log( + f"Skipping page due to missing 'markdown' or 'index'. Data keys: {list(page_data.keys())}" + ) + continue + + # Clean up content efficiently with early exit for empty content + if isinstance(page_content, str): + cleaned_content = page_content.strip() + else: + cleaned_content = str(page_content).strip() + + if not cleaned_content: + skipped_pages += 1 + self._debug_log(f'Skipping empty page {page_index}') + continue + + # Create document with optimized metadata + documents.append( + Document( + page_content=cleaned_content, + metadata={ + 'page': page_index, # 0-based index from API + 'page_label': page_index + 1, # 1-based label for convenience + 'total_pages': total_pages, + 'file_name': self.file_name, + 'file_size': self.file_size, + 'processing_engine': 'mistral-ocr', + 'content_length': len(cleaned_content), + }, + ) + ) + + if skipped_pages > 0: + log.info(f'Processed {len(documents)} pages, skipped {skipped_pages} empty/invalid pages') + + if not documents: + # Case where pages existed but none had valid markdown/index + log.warning('OCR response contained pages, but none had valid content/index.') + return [ + Document( + page_content='No valid text content found in document', + metadata={ + 'error': 'no_valid_pages', + 'total_pages': total_pages, + 'file_name': self.file_name, + }, + ) + ] + + return documents + + def load(self) -> List[Document]: + """ + Executes the full OCR workflow: upload, get URL, process OCR, delete file. + Synchronous version for backward compatibility. + + Returns: + A list of Document objects, one for each page processed. + """ + file_id = None + start_time = time.time() + + try: + # 1. Upload file + file_id = self._upload_file() + + # 2. Get Signed URL + signed_url = self._get_signed_url(file_id) + + # 3. Process OCR + ocr_response = self._process_ocr(signed_url) + + # 4. Process results + documents = self._process_results(ocr_response) + + total_time = time.time() - start_time + log.info(f'Sync OCR workflow completed in {total_time:.2f}s, produced {len(documents)} documents') + + return documents + + except Exception as e: + total_time = time.time() - start_time + log.error(f'An error occurred during the loading process after {total_time:.2f}s: {e}') + # Return an error document on failure + return [ + Document( + page_content=f'Error during processing: {e}', + metadata={ + 'error': 'processing_failed', + 'file_name': self.file_name, + }, + ) + ] + finally: + # 5. Delete file (attempt even if prior steps failed after upload) + if file_id: + try: + self._delete_file(file_id) + except Exception as del_e: + # Log deletion error, but don't overwrite original error if one occurred + log.error(f'Cleanup error: Could not delete file ID {file_id}. Reason: {del_e}') + + async def load_async(self) -> List[Document]: + """ + Asynchronous OCR workflow execution with optimized performance. + + Returns: + A list of Document objects, one for each page processed. + """ + file_id = None + start_time = time.time() + + try: + async with self._get_session() as session: + # 1. Upload file with streaming + file_id = await self._upload_file_async(session) + + # 2. Get signed URL + signed_url = await self._get_signed_url_async(session, file_id) + + # 3. Process OCR + ocr_response = await self._process_ocr_async(session, signed_url) + + # 4. Process results + documents = self._process_results(ocr_response) + + total_time = time.time() - start_time + log.info(f'Async OCR workflow completed in {total_time:.2f}s, produced {len(documents)} documents') + + return documents + + except Exception as e: + total_time = time.time() - start_time + log.error(f'Async OCR workflow failed after {total_time:.2f}s: {e}') + return [ + Document( + page_content=f'Error during OCR processing: {e}', + metadata={ + 'error': 'processing_failed', + 'file_name': self.file_name, + }, + ) + ] + finally: + # 5. Cleanup - always attempt file deletion + if file_id: + try: + async with self._get_session() as session: + await self._delete_file_async(session, file_id) + except Exception as cleanup_error: + log.error(f'Cleanup failed for file ID {file_id}: {cleanup_error}') + + @staticmethod + async def load_multiple_async( + loaders: List['MistralLoader'], + max_concurrent: int = 5, # Limit concurrent requests + ) -> List[List[Document]]: + """ + Process multiple files concurrently with controlled concurrency. + + Args: + loaders: List of MistralLoader instances + max_concurrent: Maximum number of concurrent requests + + Returns: + List of document lists, one for each loader + """ + if not loaders: + return [] + + log.info(f'Starting concurrent processing of {len(loaders)} files with max {max_concurrent} concurrent') + start_time = time.time() + + # Use semaphore to control concurrency + semaphore = asyncio.Semaphore(max_concurrent) + + async def process_with_semaphore(loader: 'MistralLoader') -> List[Document]: + async with semaphore: + return await loader.load_async() + + # Process all files with controlled concurrency + tasks = [process_with_semaphore(loader) for loader in loaders] + results = await asyncio.gather(*tasks, return_exceptions=True) + + # Handle any exceptions in results + processed_results = [] + for i, result in enumerate(results): + if isinstance(result, Exception): + log.error(f'File {i} failed: {result}') + processed_results.append( + [ + Document( + page_content=f'Error processing file: {result}', + metadata={ + 'error': 'batch_processing_failed', + 'file_index': i, + }, + ) + ] + ) + else: + processed_results.append(result) + + # MONITORING: Log comprehensive batch processing statistics + total_time = time.time() - start_time + total_docs = sum(len(docs) for docs in processed_results) + success_count = sum(1 for result in results if not isinstance(result, Exception)) + failure_count = len(results) - success_count + + log.info( + f'Batch processing completed in {total_time:.2f}s: ' + f'{success_count} files succeeded, {failure_count} files failed, ' + f'produced {total_docs} total documents' + ) + + return processed_results diff --git a/backend/open_webui/retrieval/loaders/paddleocr_vl.py b/backend/open_webui/retrieval/loaders/paddleocr_vl.py new file mode 100644 index 0000000000000000000000000000000000000000..b89369b2a415f3c7b54b4ff1178d0e1c8a0a7c4b --- /dev/null +++ b/backend/open_webui/retrieval/loaders/paddleocr_vl.py @@ -0,0 +1,125 @@ +import base64 +import os +import requests +import logging +import sys +from typing import List + +from langchain_core.documents import Document +from open_webui.env import GLOBAL_LOG_LEVEL + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) +log = logging.getLogger(__name__) + + +class PaddleOCRVLLoader: + """Loader that uses PaddleOCR-vl API to extract text from PDF/images.""" + + def __init__( + self, + api_url: str, + token: str, + file_path: str, + ): + if not api_url or not token: + raise ValueError('PaddleOCR-vl API URL and Token are required.') + if not os.path.exists(file_path): + raise FileNotFoundError(f'File not found at {file_path}') + + self.api_url = api_url.rstrip('/') + self.token = token + self.file_path = file_path + self.file_name = os.path.basename(file_path) + + def load(self) -> List[Document]: + log.info(f'Processing with PaddleOCR-vl: {self.file_path}') + + try: + with open(self.file_path, 'rb') as file: + file_bytes = file.read() + file_data = base64.b64encode(file_bytes).decode('ascii') + except Exception as e: + log.error(f'Failed to read file {self.file_path}: {e}') + raise + + headers = {'Authorization': f'token {self.token}', 'Content-Type': 'application/json'} + + # Detect fileType based on file extension + ext = self.file_path.lower().split('.')[-1] + image_extensions = ['png', 'jpg', 'jpeg', 'bmp', 'tiff', 'webp'] + file_type = 1 if ext in image_extensions else 0 + + payload = { + 'file': file_data, + 'fileType': file_type, + 'useDocOrientationClassify': False, + 'useDocUnwarping': False, + 'useChartRecognition': False, + } + + try: + response = requests.post(f'{self.api_url}/layout-parsing', json=payload, headers=headers) + response.raise_for_status() + + result = response.json().get('result', {}) + layout_results = result.get('layoutParsingResults', []) + + documents = [] + total_pages = len(layout_results) + skipped_pages = 0 + + for i, res in enumerate(layout_results): + markdown_text = res.get('markdown', {}).get('text', '') + + if isinstance(markdown_text, str): + cleaned_content = markdown_text.strip() + else: + cleaned_content = str(markdown_text).strip() + + if not cleaned_content: + skipped_pages += 1 + continue + + documents.append( + Document( + page_content=cleaned_content, + metadata={ + 'page': i, + 'page_label': i + 1, + 'total_pages': total_pages, + 'file_name': self.file_name, + 'processing_engine': 'paddleocr-vl', + }, + ) + ) + + if skipped_pages > 0: + log.info(f'PaddleOCR-vl: Processed {len(documents)} pages, skipped {skipped_pages} empty pages.') + + if not documents: + log.warning('No valid text content found by PaddleOCR-vl.') + return [ + Document( + page_content='No valid text content found in document', + metadata={ + 'error': 'no_valid_pages', + 'file_name': self.file_name, + 'processing_engine': 'paddleocr-vl', + }, + ) + ] + + return documents + + except Exception as e: + log.error(f'Error calling PaddleOCR-vl: {e}') + return [ + Document( + page_content=f'Error during OCR processing: {e}', + metadata={ + 'error': 'processing_failed', + 'file_name': self.file_name, + 'processing_engine': 'paddleocr-vl', + }, + ) + ] diff --git a/backend/open_webui/retrieval/loaders/tavily.py b/backend/open_webui/retrieval/loaders/tavily.py new file mode 100644 index 0000000000000000000000000000000000000000..742ac499cf715cabd999f1916993aa3338fa8b53 --- /dev/null +++ b/backend/open_webui/retrieval/loaders/tavily.py @@ -0,0 +1,91 @@ +import requests +import logging +from typing import Iterator, List, Literal, Union + +from langchain_core.document_loaders import BaseLoader +from langchain_core.documents import Document + +log = logging.getLogger(__name__) + + +class TavilyLoader(BaseLoader): + """Extract web page content from URLs using Tavily Extract API. + + This is a LangChain document loader that uses Tavily's Extract API to + retrieve content from web pages and return it as Document objects. + + Args: + urls: URL or list of URLs to extract content from. + api_key: The Tavily API key. + extract_depth: Depth of extraction, either "basic" or "advanced". + continue_on_failure: Whether to continue if extraction of a URL fails. + """ + + def __init__( + self, + urls: Union[str, List[str]], + api_key: str, + extract_depth: Literal['basic', 'advanced'] = 'basic', + continue_on_failure: bool = True, + ) -> None: + """Initialize Tavily Extract client. + + Args: + urls: URL or list of URLs to extract content from. + api_key: The Tavily API key. + include_images: Whether to include images in the extraction. + extract_depth: Depth of extraction, either "basic" or "advanced". + advanced extraction retrieves more data, including tables and + embedded content, with higher success but may increase latency. + basic costs 1 credit per 5 successful URL extractions, + advanced costs 2 credits per 5 successful URL extractions. + continue_on_failure: Whether to continue if extraction of a URL fails. + """ + if not urls: + raise ValueError('At least one URL must be provided.') + + self.api_key = api_key + self.urls = urls if isinstance(urls, list) else [urls] + self.extract_depth = extract_depth + self.continue_on_failure = continue_on_failure + self.api_url = 'https://api.tavily.com/extract' + + def lazy_load(self) -> Iterator[Document]: + """Extract and yield documents from the URLs using Tavily Extract API.""" + batch_size = 20 + for i in range(0, len(self.urls), batch_size): + batch_urls = self.urls[i : i + batch_size] + try: + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.api_key}', + } + # Use string for single URL, array for multiple URLs + urls_param = batch_urls[0] if len(batch_urls) == 1 else batch_urls + payload = {'urls': urls_param, 'extract_depth': self.extract_depth} + # Make the API call + response = requests.post(self.api_url, headers=headers, json=payload) + response.raise_for_status() + response_data = response.json() + # Process successful results + for result in response_data.get('results', []): + url = result.get('url', '') + content = result.get('raw_content', '') + if not content: + log.warning(f'No content extracted from {url}') + continue + # Add URLs as metadata + metadata = {'source': url} + yield Document( + page_content=content, + metadata=metadata, + ) + for failed in response_data.get('failed_results', []): + url = failed.get('url', '') + error = failed.get('error', 'Unknown error') + log.error(f'Failed to extract content from {url}: {error}') + except Exception as e: + if self.continue_on_failure: + log.error(f'Error extracting content from batch {batch_urls}: {e}') + else: + raise e diff --git a/backend/open_webui/retrieval/loaders/youtube.py b/backend/open_webui/retrieval/loaders/youtube.py new file mode 100644 index 0000000000000000000000000000000000000000..34a1d207408bc7a248aedf88fe996a1a92b72e99 --- /dev/null +++ b/backend/open_webui/retrieval/loaders/youtube.py @@ -0,0 +1,156 @@ +import logging +from xml.etree.ElementTree import ParseError + +from typing import Any, Dict, Generator, List, Optional, Sequence, Union +from urllib.parse import parse_qs, urlparse +from langchain_core.documents import Document + +log = logging.getLogger(__name__) + +ALLOWED_SCHEMES = {'http', 'https'} +ALLOWED_NETLOCS = { + 'youtu.be', + 'm.youtube.com', + 'youtube.com', + 'www.youtube.com', + 'www.youtube-nocookie.com', + 'vid.plus', +} + + +def _parse_video_id(url: str) -> Optional[str]: + """Parse a YouTube URL and return the video ID if valid, otherwise None.""" + parsed_url = urlparse(url) + + if parsed_url.scheme not in ALLOWED_SCHEMES: + return None + + if parsed_url.netloc not in ALLOWED_NETLOCS: + return None + + path = parsed_url.path + + if path.endswith('/watch'): + query = parsed_url.query + parsed_query = parse_qs(query) + if 'v' in parsed_query: + ids = parsed_query['v'] + video_id = ids if isinstance(ids, str) else ids[0] + else: + return None + else: + path = parsed_url.path.lstrip('/') + video_id = path.split('/')[-1] + + if len(video_id) != 11: # Video IDs are 11 characters long + return None + + return video_id + + +class YoutubeLoader: + """Load `YouTube` video transcripts.""" + + def __init__( + self, + video_id: str, + language: Union[str, Sequence[str]] = 'en', + proxy_url: Optional[str] = None, + ): + """Initialize with YouTube video ID.""" + _video_id = _parse_video_id(video_id) + self.video_id = _video_id if _video_id is not None else video_id + self._metadata = {'source': video_id} + self.proxy_url = proxy_url + + # Ensure language is a list + if isinstance(language, str): + self.language = [language] + else: + self.language = list(language) + + # Add English as fallback if not already in the list + if 'en' not in self.language: + self.language.append('en') + + def load(self) -> List[Document]: + """Load YouTube transcripts into `Document` objects.""" + try: + from youtube_transcript_api import ( + NoTranscriptFound, + TranscriptsDisabled, + YouTubeTranscriptApi, + ) + from youtube_transcript_api.proxies import GenericProxyConfig + except ImportError: + raise ImportError( + 'Could not import "youtube_transcript_api" Python package. ' + 'Please install it with `pip install youtube-transcript-api`.' + ) + + if self.proxy_url: + youtube_proxies = GenericProxyConfig(http_url=self.proxy_url, https_url=self.proxy_url) + log.debug(f'Using proxy URL: {self.proxy_url[:14]}...') + else: + youtube_proxies = None + + transcript_api = YouTubeTranscriptApi(proxy_config=youtube_proxies) + try: + transcript_list = transcript_api.list(self.video_id) + except Exception as e: + log.exception('Loading YouTube transcript failed') + return [] + + # Try each language in order of priority + for lang in self.language: + try: + transcript = transcript_list.find_transcript([lang]) + if transcript.is_generated: + log.debug(f"Found generated transcript for language '{lang}'") + try: + transcript = transcript_list.find_manually_created_transcript([lang]) + log.debug(f"Found manual transcript for language '{lang}'") + except NoTranscriptFound: + log.debug(f"No manual transcript found for language '{lang}', using generated") + pass + + log.debug(f"Found transcript for language '{lang}'") + try: + transcript_pieces: List[Dict[str, Any]] = transcript.fetch() + except ParseError: + log.debug(f"Empty or invalid transcript for language '{lang}'") + continue + + if not transcript_pieces: + log.debug(f"Empty transcript for language '{lang}'") + continue + + transcript_text = ' '.join( + map( + lambda transcript_piece: ( + transcript_piece.text.strip(' ') if hasattr(transcript_piece, 'text') else '' + ), + transcript_pieces, + ) + ) + return [Document(page_content=transcript_text, metadata=self._metadata)] + except NoTranscriptFound: + log.debug(f"No transcript found for language '{lang}'") + continue + except Exception as e: + log.info(f"Error finding transcript for language '{lang}'") + raise e + + # If we get here, all languages failed + languages_tried = ', '.join(self.language) + log.warning( + f'No transcript found for any of the specified languages: {languages_tried}. Verify if the video has transcripts, add more languages if needed.' + ) + raise NoTranscriptFound(self.video_id, self.language, list(transcript_list)) + + async def aload(self) -> Generator[Document, None, None]: + """Asynchronously load YouTube transcripts into `Document` objects.""" + import asyncio + + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, self.load) diff --git a/backend/open_webui/retrieval/models/base_reranker.py b/backend/open_webui/retrieval/models/base_reranker.py new file mode 100644 index 0000000000000000000000000000000000000000..6be7a5649b8dde7e7eac54066e0a9311a997dbd4 --- /dev/null +++ b/backend/open_webui/retrieval/models/base_reranker.py @@ -0,0 +1,8 @@ +from abc import ABC, abstractmethod +from typing import Optional, List, Tuple + + +class BaseReranker(ABC): + @abstractmethod + def predict(self, sentences: List[Tuple[str, str]]) -> Optional[List[float]]: + pass diff --git a/backend/open_webui/retrieval/models/colbert.py b/backend/open_webui/retrieval/models/colbert.py new file mode 100644 index 0000000000000000000000000000000000000000..ceb41824e3e2f11c9aaca499d883777bd51d0d64 --- /dev/null +++ b/backend/open_webui/retrieval/models/colbert.py @@ -0,0 +1,75 @@ +import os +import logging +import torch +import numpy as np +from colbert.infra import ColBERTConfig +from colbert.modeling.checkpoint import Checkpoint + + +from open_webui.retrieval.models.base_reranker import BaseReranker + +log = logging.getLogger(__name__) + + +class ColBERT(BaseReranker): + def __init__(self, name, **kwargs) -> None: + log.info('ColBERT: Loading model', name) + self.device = 'cuda' if torch.cuda.is_available() else 'cpu' + + 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/py311_cpu/segmented_maxsim_cpp/segmented_maxsim_cpp.so: cannot open shared object file: No such file or directory + + lock_file = '/root/.cache/torch_extensions/py311_cpu/segmented_maxsim_cpp/lock' + if os.path.exists(lock_file): + os.remove(lock_file) + + self.ckpt = Checkpoint( + name, + colbert_config=ColBERTConfig(model_name=name), + ).to(self.device) + pass + + def calculate_similarity_scores(self, query_embeddings, document_embeddings): + query_embeddings = query_embeddings.to(self.device) + document_embeddings = document_embeddings.to(self.device) + + # Validate dimensions to ensure compatibility + if query_embeddings.dim() != 3: + raise ValueError(f'Expected query embeddings to have 3 dimensions, but got {query_embeddings.dim()}.') + if document_embeddings.dim() != 3: + raise ValueError(f'Expected document embeddings to have 3 dimensions, but got {document_embeddings.dim()}.') + if query_embeddings.size(0) not in [1, document_embeddings.size(0)]: + raise ValueError('There should be either one query or queries equal to the number of documents.') + + # Transpose the query embeddings to align for matrix multiplication + transposed_query_embeddings = query_embeddings.permute(0, 2, 1) + # Compute similarity scores using batch matrix multiplication + computed_scores = torch.matmul(document_embeddings, transposed_query_embeddings) + # Apply max pooling to extract the highest semantic similarity across each document's sequence + maximum_scores = torch.max(computed_scores, dim=1).values + + # Sum up the maximum scores across features to get the overall document relevance scores + final_scores = maximum_scores.sum(dim=1) + + normalized_scores = torch.softmax(final_scores, dim=0) + + return normalized_scores.detach().cpu().numpy().astype(np.float32) + + def predict(self, sentences, batch_size=32): + query = sentences[0][0] + docs = [i[1] for i in sentences] + + # Embedding the documents + embedded_docs = self.ckpt.docFromText(docs, bsize=batch_size)[0] + # Embedding the queries + embedded_queries = self.ckpt.queryFromText([query], bsize=batch_size) + embedded_query = embedded_queries[0] + + # Calculate retrieval scores for the query against all documents + scores = self.calculate_similarity_scores(embedded_query.unsqueeze(0), embedded_docs) + + return scores diff --git a/backend/open_webui/retrieval/models/external.py b/backend/open_webui/retrieval/models/external.py new file mode 100644 index 0000000000000000000000000000000000000000..f04583b965e678340ed8247fcaeb3223ad41617c --- /dev/null +++ b/backend/open_webui/retrieval/models/external.py @@ -0,0 +1,70 @@ +import logging +import requests +from typing import Optional, List, Tuple +from urllib.parse import quote + + +from open_webui.env import ENABLE_FORWARD_USER_INFO_HEADERS, REQUESTS_VERIFY +from open_webui.retrieval.models.base_reranker import BaseReranker +from open_webui.utils.headers import include_user_info_headers + +log = logging.getLogger(__name__) + + +class ExternalReranker(BaseReranker): + def __init__( + self, + api_key: str, + url: str = 'http://localhost:8080/v1/rerank', + model: str = 'reranker', + timeout: Optional[int] = None, + ): + self.api_key = api_key + self.url = url + self.model = model + self.timeout = timeout + + def predict(self, sentences: List[Tuple[str, str]], user=None) -> Optional[List[float]]: + query = sentences[0][0] + docs = [i[1] for i in sentences] + + payload = { + 'model': self.model, + 'query': query, + 'documents': docs, + 'top_n': len(docs), + } + + try: + log.info(f'ExternalReranker:predict:model {self.model}') + log.info(f'ExternalReranker:predict:query {query}') + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.api_key}', + } + + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) + + r = requests.post( + f'{self.url}', + headers=headers, + json=payload, + timeout=self.timeout, + verify=REQUESTS_VERIFY, + ) + + r.raise_for_status() + data = r.json() + + if 'results' in data: + sorted_results = sorted(data['results'], key=lambda x: x['index']) + return [result['relevance_score'] for result in sorted_results] + else: + log.error('No results found in external reranking response') + return None + + except Exception as e: + log.exception(f'Error in external reranking: {e}') + return None diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..14a64fed60a7400c4a528cd6498cad08f12f3ba3 --- /dev/null +++ b/backend/open_webui/retrieval/utils.py @@ -0,0 +1,1489 @@ +import logging +import os +from typing import Awaitable, Optional, Union + +import requests +import aiohttp +import asyncio +import hashlib +from concurrent.futures import ThreadPoolExecutor +import time +import re + +from urllib.parse import quote +from huggingface_hub import snapshot_download +from langchain_classic.retrievers import ( + ContextualCompressionRetriever, + EnsembleRetriever, +) +from langchain_community.retrievers import BM25Retriever +from langchain_core.documents import Document + +from open_webui.config import VECTOR_DB +from open_webui.retrieval.vector.async_client import ASYNC_VECTOR_DB_CLIENT +from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT + + +from open_webui.models.users import UserModel +from open_webui.models.files import Files +from open_webui.models.knowledge import Knowledges + +from open_webui.models.chats import Chats +from open_webui.models.notes import Notes +from open_webui.models.access_grants import AccessGrants +from open_webui.utils.access_control.files import has_access_to_file + +from open_webui.retrieval.vector.main import GetResult +from open_webui.utils.headers import include_user_info_headers +from open_webui.utils.misc import get_message_list + +from open_webui.retrieval.web.utils import get_web_loader +from open_webui.retrieval.loaders.youtube import YoutubeLoader + + +from open_webui.env import ( + AIOHTTP_CLIENT_TIMEOUT, + OFFLINE_MODE, + ENABLE_FORWARD_USER_INFO_HEADERS, + AIOHTTP_CLIENT_SESSION_SSL, +) +from open_webui.config import ( + RAG_EMBEDDING_QUERY_PREFIX, + RAG_EMBEDDING_CONTENT_PREFIX, + RAG_EMBEDDING_PREFIX_FIELD_NAME, +) + +log = logging.getLogger(__name__) + + +from typing import Any + +from langchain_core.callbacks import CallbackManagerForRetrieverRun +from langchain_core.retrievers import BaseRetriever + + +def is_youtube_url(url: str) -> bool: + youtube_regex = r'^(https?://)?(www\.)?(youtube\.com|youtu\.be)/.+$' + return re.match(youtube_regex, url) is not None + + +def get_loader(request, url: str): + if is_youtube_url(url): + return YoutubeLoader( + url, + language=request.app.state.config.YOUTUBE_LOADER_LANGUAGE, + proxy_url=request.app.state.config.YOUTUBE_LOADER_PROXY_URL, + ) + else: + return get_web_loader( + url, + verify_ssl=request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION, + requests_per_second=request.app.state.config.WEB_LOADER_CONCURRENT_REQUESTS, + trust_env=request.app.state.config.WEB_SEARCH_TRUST_ENV, + ) + + +def build_loader_from_config(request): + """Build a Loader instance with the admin's configured extraction engine settings.""" + from open_webui.retrieval.loaders.main import Loader + + config = request.app.state.config + return Loader( + engine=config.CONTENT_EXTRACTION_ENGINE, + DATALAB_MARKER_API_KEY=config.DATALAB_MARKER_API_KEY, + DATALAB_MARKER_API_BASE_URL=config.DATALAB_MARKER_API_BASE_URL, + DATALAB_MARKER_ADDITIONAL_CONFIG=config.DATALAB_MARKER_ADDITIONAL_CONFIG, + DATALAB_MARKER_SKIP_CACHE=config.DATALAB_MARKER_SKIP_CACHE, + DATALAB_MARKER_FORCE_OCR=config.DATALAB_MARKER_FORCE_OCR, + DATALAB_MARKER_PAGINATE=config.DATALAB_MARKER_PAGINATE, + DATALAB_MARKER_STRIP_EXISTING_OCR=config.DATALAB_MARKER_STRIP_EXISTING_OCR, + DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION=config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, + DATALAB_MARKER_FORMAT_LINES=config.DATALAB_MARKER_FORMAT_LINES, + DATALAB_MARKER_USE_LLM=config.DATALAB_MARKER_USE_LLM, + DATALAB_MARKER_OUTPUT_FORMAT=config.DATALAB_MARKER_OUTPUT_FORMAT, + EXTERNAL_DOCUMENT_LOADER_URL=config.EXTERNAL_DOCUMENT_LOADER_URL, + EXTERNAL_DOCUMENT_LOADER_API_KEY=config.EXTERNAL_DOCUMENT_LOADER_API_KEY, + TIKA_SERVER_URL=config.TIKA_SERVER_URL, + DOCLING_SERVER_URL=config.DOCLING_SERVER_URL, + DOCLING_API_KEY=config.DOCLING_API_KEY, + DOCLING_PARAMS=config.DOCLING_PARAMS, + PDF_EXTRACT_IMAGES=config.PDF_EXTRACT_IMAGES, + PDF_LOADER_MODE=config.PDF_LOADER_MODE, + DOCUMENT_INTELLIGENCE_ENDPOINT=config.DOCUMENT_INTELLIGENCE_ENDPOINT, + DOCUMENT_INTELLIGENCE_KEY=config.DOCUMENT_INTELLIGENCE_KEY, + DOCUMENT_INTELLIGENCE_MODEL=config.DOCUMENT_INTELLIGENCE_MODEL, + MISTRAL_OCR_API_BASE_URL=config.MISTRAL_OCR_API_BASE_URL, + MISTRAL_OCR_API_KEY=config.MISTRAL_OCR_API_KEY, + PADDLEOCR_VL_BASE_URL=config.PADDLEOCR_VL_BASE_URL, + PADDLEOCR_VL_TOKEN=config.PADDLEOCR_VL_TOKEN, + MINERU_API_MODE=config.MINERU_API_MODE, + MINERU_API_URL=config.MINERU_API_URL, + MINERU_API_KEY=config.MINERU_API_KEY, + MINERU_API_TIMEOUT=config.MINERU_API_TIMEOUT, + MINERU_PARAMS=config.MINERU_PARAMS, + ) + + +def _extract_text_from_binary_response(request, response: requests.Response, url: str) -> tuple[str, list]: + """Download response body to a temp file and extract text using the Loader pipeline.""" + import mimetypes + import tempfile + import urllib.parse + + content_type = response.headers.get('Content-Type', '').split(';')[0].strip() + + # Derive filename from URL path, falling back to Content-Disposition or mime guess + url_path = urllib.parse.urlparse(url).path + filename = os.path.basename(url_path) if url_path else '' + + if not filename or '.' not in filename: + # Try Content-Disposition header + cd = response.headers.get('Content-Disposition', '') + if 'filename=' in cd: + filename = cd.split('filename=')[-1].strip('"\'') + + if not filename or '.' not in filename: + ext = mimetypes.guess_extension(content_type) or '' + filename = f'download{ext}' + + suffix = '.' + filename.split('.')[-1].lower() if '.' in filename else '' + + with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp: + tmp.write(response.content) + tmp_path = tmp.name + + try: + loader = build_loader_from_config(request) + docs = loader.load(filename, content_type, tmp_path) + for doc in docs: + doc.metadata['source'] = url + content = ' '.join([doc.page_content for doc in docs]) + return content, docs + finally: + os.remove(tmp_path) + + +def _is_text_content_type(content_type: str) -> bool: + """Return True if the content type should be handled by the web loader.""" + ct = content_type.split(';')[0].strip().lower() + if ct.startswith('text/'): + return True + if any(t in ct for t in ['xml', 'json', 'javascript']): + return True + return not ct # empty / missing → assume HTML + + +def get_content_from_url(request, url: str) -> str: + from open_webui.retrieval.web.utils import validate_url + + # Validate URL before making any request (blocks private IPs, non-HTTP, filter list) + validate_url(url) + + # Streamed GET to check Content-Type without downloading the body. + try: + response = requests.get(url, stream=True, timeout=30) + response.raise_for_status() + content_type = response.headers.get('Content-Type', '') + except Exception: + content_type = '' + response = None + + # Text / HTML / unknown — use the configured web loader + if response is None or _is_text_content_type(content_type): + if response is not None: + response.close() + loader = get_loader(request, url) + docs = loader.load() + content = ' '.join([doc.page_content for doc in docs]) + return content, docs + + # Binary content (PDF, DOCX, XLSX, PPTX, etc.) — download and extract + try: + return _extract_text_from_binary_response(request, response, url) + finally: + response.close() + + +CHUNK_HASH_KEY = '_chunk_hash' + + +def _content_hash(text: str) -> str: + """SHA-256 hash of text, used as a stable chunk identifier for RRF dedup.""" + return hashlib.sha256(text.encode()).hexdigest() + + +class VectorSearchRetriever(BaseRetriever): + collection_name: Any + embedding_function: Any + top_k: int + + def _get_relevant_documents(self, query: str, *, run_manager: CallbackManagerForRetrieverRun) -> list[Document]: + """Get documents relevant to a query. + + Args: + query: String to find relevant documents for. + run_manager: The callback handler to use. + + Returns: + List of relevant documents. + """ + return [] + + async def _aget_relevant_documents( + self, + query: str, + *, + run_manager: CallbackManagerForRetrieverRun, + ) -> list[Document]: + embedding = await self.embedding_function(query, RAG_EMBEDDING_QUERY_PREFIX) + result = await ASYNC_VECTOR_DB_CLIENT.search( + collection_name=self.collection_name, + vectors=[embedding], + limit=self.top_k, + ) + + ids = result.ids[0] + metadatas = result.metadatas[0] + documents = result.documents[0] + + results = [] + for idx in range(len(ids)): + metadata = metadatas[idx] + metadata[CHUNK_HASH_KEY] = _content_hash(documents[idx]) + results.append( + Document( + metadata=metadata, + page_content=documents[idx], + ) + ) + return results + + +def query_doc(collection_name: str, query_embedding: list[float], k: int, user: UserModel = None): + try: + log.debug(f'query_doc:doc {collection_name}') + result = VECTOR_DB_CLIENT.search( + collection_name=collection_name, + vectors=[query_embedding], + limit=k, + ) + + if result: + log.info(f'query_doc:result {result.ids} {result.metadatas}') + + return result + except Exception as e: + log.exception(f'Error querying doc {collection_name} with limit {k}: {e}') + raise e + + +def get_doc(collection_name: str, user: UserModel = None): + try: + log.debug(f'get_doc:doc {collection_name}') + result = VECTOR_DB_CLIENT.get(collection_name=collection_name) + + if result: + log.info(f'query_doc:result {result.ids} {result.metadatas}') + + return result + except Exception as e: + log.exception(f'Error getting doc {collection_name}: {e}') + raise e + + +def get_enriched_texts(collection_result: GetResult) -> list[str]: + enriched_texts = [] + for idx, text in enumerate(collection_result.documents[0]): + metadata = collection_result.metadatas[0][idx] + metadata_parts = [text] + + # Add filename (repeat twice for extra weight in BM25 scoring) + if metadata.get('name'): + filename = metadata['name'] + filename_tokens = filename.replace('_', ' ').replace('-', ' ').replace('.', ' ') + metadata_parts.append(f'Filename: {filename} {filename_tokens} {filename_tokens}') + + # Add title if available + if metadata.get('title'): + metadata_parts.append(f'Title: {metadata["title"]}') + + # Add document section headings if available (from markdown splitter) + if metadata.get('headings') and isinstance(metadata['headings'], list): + headings = ' > '.join(str(h) for h in metadata['headings']) + metadata_parts.append(f'Section: {headings}') + + # Add source URL/path if available + if metadata.get('source'): + metadata_parts.append(f'Source: {metadata["source"]}') + + # Add snippet for web search results + if metadata.get('snippet'): + metadata_parts.append(f'Snippet: {metadata["snippet"]}') + + enriched_texts.append(' '.join(metadata_parts)) + + return enriched_texts + + +async def query_doc_with_hybrid_search( + collection_name: str, + collection_result: GetResult, + query: str, + embedding_function, + k: int, + reranking_function, + k_reranker: int, + r: float, + hybrid_bm25_weight: float, + enable_enriched_texts: bool = False, +) -> dict: + try: + # First check if collection_result has the required attributes + if ( + not collection_result + or not hasattr(collection_result, 'documents') + or not hasattr(collection_result, 'metadatas') + ): + log.warning(f'query_doc_with_hybrid_search:no_docs {collection_name}') + return {'documents': [], 'metadatas': [], 'distances': []} + + # Now safely check the documents content after confirming attributes exist + if ( + not collection_result.documents + or len(collection_result.documents) == 0 + or not collection_result.documents[0] + ): + log.warning(f'query_doc_with_hybrid_search:no_docs {collection_name}') + return {'documents': [], 'metadatas': [], 'distances': []} + + log.debug(f'query_doc_with_hybrid_search:doc {collection_name}') + + original_texts = collection_result.documents[0] + bm25_metadatas = [ + {**meta, CHUNK_HASH_KEY: _content_hash(original_texts[idx])} + for idx, meta in enumerate(collection_result.metadatas[0]) + ] + + bm25_texts = get_enriched_texts(collection_result) if enable_enriched_texts else original_texts + + bm25_retriever = BM25Retriever.from_texts( + texts=bm25_texts, + metadatas=bm25_metadatas, + ) + bm25_retriever.k = k + + vector_search_retriever = VectorSearchRetriever( + collection_name=collection_name, + embedding_function=embedding_function, + top_k=k, + ) + + # Use CHUNK_HASH_KEY for dedup so enriched BM25 texts don't defeat RRF + if hybrid_bm25_weight <= 0: + ensemble_retriever = EnsembleRetriever( + retrievers=[vector_search_retriever], + weights=[1.0], + id_key=CHUNK_HASH_KEY, + ) + elif hybrid_bm25_weight >= 1: + ensemble_retriever = EnsembleRetriever( + retrievers=[bm25_retriever], + weights=[1.0], + id_key=CHUNK_HASH_KEY, + ) + else: + ensemble_retriever = EnsembleRetriever( + retrievers=[bm25_retriever, vector_search_retriever], + weights=[hybrid_bm25_weight, 1.0 - hybrid_bm25_weight], + id_key=CHUNK_HASH_KEY, + ) + + compressor = RerankCompressor( + embedding_function=embedding_function, + top_n=k_reranker, + reranking_function=reranking_function, + r_score=r, + ) + + compression_retriever = ContextualCompressionRetriever( + base_compressor=compressor, base_retriever=ensemble_retriever + ) + + result = await compression_retriever.ainvoke(query) + + distances = [d.metadata.get('score') for d in result] + documents = [d.page_content for d in result] + metadatas = [d.metadata for d in result] + + # retrieve only min(k, k_reranker) items, sort and cut by distance if k < k_reranker + if k < k_reranker: + sorted_items = sorted(zip(distances, documents, metadatas), key=lambda x: x[0], reverse=True) + sorted_items = sorted_items[:k] + + if sorted_items: + distances, documents, metadatas = map(list, zip(*sorted_items)) + else: + distances, documents, metadatas = [], [], [] + + result = { + 'distances': [distances], + 'documents': [documents], + 'metadatas': [metadatas], + } + + log.info('query_doc_with_hybrid_search:result ' + f'{result["metadatas"]} {result["distances"]}') + return result + except Exception as e: + log.exception(f'Error querying doc {collection_name} with hybrid search: {e}') + raise e + + +def merge_get_results(get_results: list[dict]) -> dict: + # Initialize lists to store combined data + combined_documents = [] + combined_metadatas = [] + combined_ids = [] + + for data in get_results: + combined_documents.extend(data['documents'][0]) + combined_metadatas.extend(data['metadatas'][0]) + combined_ids.extend(data['ids'][0]) + + # Create the output dictionary + result = { + 'documents': [combined_documents], + 'metadatas': [combined_metadatas], + 'ids': [combined_ids], + } + + return result + + +def merge_and_sort_query_results(query_results: list[dict], k: int) -> dict: + # Initialize lists to store combined data + combined = dict() # To store documents with unique document hashes + + for data in query_results: + if ( + len(data.get('distances', [])) == 0 + or len(data.get('documents', [])) == 0 + or len(data.get('metadatas', [])) == 0 + ): + continue + + distances = data['distances'][0] + documents = data['documents'][0] + metadatas = data['metadatas'][0] + + for distance, document, metadata in zip(distances, documents, metadatas): + if isinstance(document, str): + doc_hash = hashlib.sha256(document.encode()).hexdigest() # Compute a hash for uniqueness + + if doc_hash not in combined.keys(): + combined[doc_hash] = (distance, document, metadata) + continue # if doc is new, no further comparison is needed + + # if doc is alredy in, but new distance is better, update + if distance > combined[doc_hash][0]: + combined[doc_hash] = (distance, document, metadata) + + combined = list(combined.values()) + # Sort the list based on distances + combined.sort(key=lambda x: x[0], reverse=True) + + # Slice to keep only the top k elements + sorted_distances, sorted_documents, sorted_metadatas = zip(*combined[:k]) if combined else ([], [], []) + + # Create and return the output dictionary + return { + 'distances': [list(sorted_distances)], + 'documents': [list(sorted_documents)], + 'metadatas': [list(sorted_metadatas)], + } + + +def get_all_items_from_collections(collection_names: list[str]) -> dict: + results = [] + + for collection_name in collection_names: + if collection_name: + try: + result = get_doc(collection_name=collection_name) + if result is not None: + results.append(result.model_dump()) + except Exception as e: + log.exception(f'Error when querying the collection: {e}') + else: + pass + + return merge_get_results(results) + + +async def query_collection( + request, + collection_names: list[str], + queries: list[str], + embedding_function, + k: int, +) -> dict: + # When request is provided, try hybrid search + reranking if enabled + if request and request.app.state.config.ENABLE_RAG_HYBRID_SEARCH: + try: + reranking_function = ( + (lambda query, documents: request.app.state.RERANKING_FUNCTION(query, documents)) + if request.app.state.RERANKING_FUNCTION + else None + ) + return await query_collection_with_hybrid_search( + collection_names=collection_names, + queries=queries, + embedding_function=embedding_function, + k=k, + reranking_function=reranking_function, + k_reranker=request.app.state.config.TOP_K_RERANKER, + r=request.app.state.config.RELEVANCE_THRESHOLD, + hybrid_bm25_weight=request.app.state.config.HYBRID_BM25_WEIGHT, + enable_enriched_texts=request.app.state.config.ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS, + ) + except Exception as e: + log.debug(f'Hybrid search failed, falling back to vector search: {e}') + + results = [] + error = False + + def process_query_collection(collection_name, query_embedding): + try: + if collection_name: + result = query_doc( + collection_name=collection_name, + k=k, + query_embedding=query_embedding, + ) + if result is not None: + return result.model_dump(), None + return None, None + except Exception as e: + log.exception(f'Error when querying the collection: {e}') + return None, e + + # Sanitize: filter out None/empty queries to prevent embedding crashes + # (e.g. when get_last_user_message returns None) + queries = [q for q in queries if q] + if not queries: + log.warning('query_collection: all queries were None or empty, returning empty results') + return {'distances': [[]], 'documents': [[]], 'metadatas': [[]]} + + # Generate all query embeddings (in one call) + query_embeddings = await embedding_function(queries, prefix=RAG_EMBEDDING_QUERY_PREFIX) + log.debug(f'query_collection: processing {len(queries)} queries across {len(collection_names)} collections') + + with ThreadPoolExecutor() as executor: + future_results = [] + for query_embedding in query_embeddings: + for collection_name in collection_names: + result = executor.submit(process_query_collection, collection_name, query_embedding) + future_results.append(result) + task_results = [future.result() for future in future_results] + + for result, err in task_results: + if err is not None: + error = True + elif result is not None: + results.append(result) + + if error and not results: + log.warning('All collection queries failed. No results returned.') + + return merge_and_sort_query_results(results, k=k) + + +async def query_collection_with_hybrid_search( + collection_names: list[str], + queries: list[str], + embedding_function, + k: int, + reranking_function, + k_reranker: int, + r: float, + hybrid_bm25_weight: float, + enable_enriched_texts: bool = False, +) -> dict: + results = [] + error = False + # 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)`. + log.debug( + 'query_collection_with_hybrid_search: prefetching %d collections', + len(collection_names), + ) + + async def _fetch_collection(name: str): + try: + return name, await ASYNC_VECTOR_DB_CLIENT.get(collection_name=name) + except Exception as e: + log.exception(f'Failed to fetch collection {name}: {e}') + return name, None + + collection_results = dict(await asyncio.gather(*(_fetch_collection(name) for name in collection_names))) + + log.info(f'Starting hybrid search for {len(queries)} queries in {len(collection_names)} collections...') + + async def process_query(collection_name, query): + try: + result = await query_doc_with_hybrid_search( + collection_name=collection_name, + collection_result=collection_results[collection_name], + query=query, + embedding_function=embedding_function, + k=k, + reranking_function=reranking_function, + k_reranker=k_reranker, + r=r, + hybrid_bm25_weight=hybrid_bm25_weight, + enable_enriched_texts=enable_enriched_texts, + ) + return result, None + except Exception as e: + log.exception(f'Error when querying the collection with hybrid_search: {e}') + return None, e + + # Prepare tasks for all collections and queries + # Avoid running any tasks for collections that failed to fetch data (have assigned None) + tasks = [ + (collection_name, query) + for collection_name in collection_names + if collection_results[collection_name] is not None + for query in queries + ] + + # Run all queries in parallel using asyncio.gather + task_results = await asyncio.gather(*[process_query(collection_name, query) for collection_name, query in tasks]) + + for result, err in task_results: + if err is not None: + error = True + elif result is not None: + results.append(result) + + if error and not results: + raise Exception('Hybrid search failed for all collections. Using Non-hybrid search as fallback.') + + return merge_and_sort_query_results(results, k=k) + + +def generate_openai_batch_embeddings( + model: str, + texts: list[str], + url: str = 'https://api.openai.com/v1', + key: str = '', + prefix: str = None, + user: UserModel = None, +) -> list[list[float]]: + log.debug(f'generate_openai_batch_embeddings:model {model} batch size: {len(texts)}') + json_data = {'input': texts, 'model': model} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {key}', + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) + + r = requests.post( + f'{url}/embeddings', + headers=headers, + json=json_data, + ) + r.raise_for_status() + data = r.json() + if 'data' in data: + return [elem['embedding'] for elem in data['data']] + else: + raise ValueError("Unexpected OpenAI embeddings response: missing 'data' key") + + +async def agenerate_openai_batch_embeddings( + model: str, + texts: list[str], + url: str = 'https://api.openai.com/v1', + key: str = '', + prefix: str = None, + user: UserModel = None, +) -> list[list[float]]: + log.debug(f'agenerate_openai_batch_embeddings:model {model} batch size: {len(texts)}') + form_data = {'input': texts, 'model': model} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + form_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {key}', + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) + + async with aiohttp.ClientSession( + trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + ) as session: + async with session.post( + f'{url}/embeddings', + headers=headers, + json=form_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + data = await r.json() + if 'data' in data: + return [item['embedding'] for item in data['data']] + else: + raise ValueError("Unexpected OpenAI embeddings response: missing 'data' key") + + +def generate_azure_openai_batch_embeddings( + model: str, + texts: list[str], + url: str, + key: str = '', + version: str = '', + prefix: str = None, + user: UserModel = None, +) -> list[list[float]]: + log.debug(f'generate_azure_openai_batch_embeddings:deployment {model} batch size: {len(texts)}') + json_data = {'input': texts} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix + + url = f'{url}/openai/deployments/{model}/embeddings?api-version={version}' + + for _ in range(5): + headers = { + 'Content-Type': 'application/json', + 'api-key': key, + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) + + r = requests.post( + url, + headers=headers, + json=json_data, + ) + if r.status_code == 429: + retry = float(r.headers.get('Retry-After', '1')) + time.sleep(retry) + continue + r.raise_for_status() + data = r.json() + if 'data' in data: + return [elem['embedding'] for elem in data['data']] + else: + raise ValueError("Unexpected Azure OpenAI embeddings response: missing 'data' key") + raise Exception('Azure OpenAI embedding request failed: max retries (429) exceeded') + + +async def agenerate_azure_openai_batch_embeddings( + model: str, + texts: list[str], + url: str, + key: str = '', + version: str = '', + prefix: str = None, + user: UserModel = None, +) -> list[list[float]]: + log.debug(f'agenerate_azure_openai_batch_embeddings:deployment {model} batch size: {len(texts)}') + form_data = {'input': texts} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + form_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix + + full_url = f'{url}/openai/deployments/{model}/embeddings?api-version={version}' + + headers = { + 'Content-Type': 'application/json', + 'api-key': key, + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) + + async with aiohttp.ClientSession( + trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + ) as session: + async with session.post( + full_url, + headers=headers, + json=form_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + data = await r.json() + if 'data' in data: + return [item['embedding'] for item in data['data']] + else: + raise ValueError("Unexpected Azure OpenAI embeddings response: missing 'data' key") + + +def generate_ollama_batch_embeddings( + model: str, + texts: list[str], + url: str, + key: str = '', + prefix: str = None, + user: UserModel = None, +) -> list[list[float]]: + log.debug(f'generate_ollama_batch_embeddings:model {model} batch size: {len(texts)}') + json_data = {'input': texts, 'model': model, 'truncate': True} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {key}', + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) + + r = requests.post( + f'{url}/api/embed', + headers=headers, + json=json_data, + ) + if r.status_code != 200: + error_detail = r.json().get('error', r.text) + raise Exception(f'Ollama embed error ({r.status_code}): {error_detail}') + data = r.json() + + if 'embeddings' in data: + return data['embeddings'] + else: + raise ValueError("Unexpected Ollama embeddings response: missing 'embeddings' key") + + +async def agenerate_ollama_batch_embeddings( + model: str, + texts: list[str], + url: str, + key: str = '', + prefix: str = None, + user: UserModel = None, +) -> list[list[float]]: + log.debug(f'agenerate_ollama_batch_embeddings:model {model} batch size: {len(texts)}') + form_data = {'input': texts, 'model': model, 'truncate': True} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + form_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {key}', + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) + + async with aiohttp.ClientSession( + trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + ) as session: + async with session.post( + f'{url}/api/embed', + headers=headers, + json=form_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + if r.status != 200: + error_data = await r.json() + error_detail = error_data.get('error', str(error_data)) + raise Exception(f'Ollama embed error ({r.status}): {error_detail}') + data = await r.json() + if 'embeddings' in data: + return data['embeddings'] + else: + raise ValueError("Unexpected Ollama embeddings response: missing 'embeddings' key") + + +def get_embedding_function( + embedding_engine, + embedding_model, + embedding_function, + url, + key, + embedding_batch_size, + azure_api_version=None, + enable_async=True, + concurrent_requests=0, +) -> Awaitable: + if embedding_engine == '': + # Sentence transformers: CPU-bound sync operation + async def async_embedding_function(query, prefix=None, user=None): + return await asyncio.to_thread( + ( + lambda query, prefix=None: embedding_function.encode( + query, + batch_size=int(embedding_batch_size), + **({'prompt': prefix} if prefix else {}), + ).tolist() + ), + query, + prefix, + ) + + return async_embedding_function + elif embedding_engine in ['ollama', 'openai', 'azure_openai']: + embedding_function = lambda query, prefix=None, user=None: generate_embeddings( + engine=embedding_engine, + model=embedding_model, + text=query, + prefix=prefix, + url=url, + key=key, + user=user, + azure_api_version=azure_api_version, + ) + + async def async_embedding_function(query, prefix=None, user=None): + if isinstance(query, list): + # Create batches + batches = [query[i : i + embedding_batch_size] for i in range(0, len(query), embedding_batch_size)] + + if enable_async: + log.debug(f'generate_multiple_async: Processing {len(batches)} batches in parallel') + # Use semaphore to limit concurrent embedding API requests + # 0 = unlimited (no semaphore) + if concurrent_requests: + semaphore = asyncio.Semaphore(concurrent_requests) + + async def generate_batch_with_semaphore(batch): + async with semaphore: + return await embedding_function(batch, prefix=prefix, user=user) + + tasks = [generate_batch_with_semaphore(batch) for batch in batches] + else: + tasks = [embedding_function(batch, prefix=prefix, user=user) for batch in batches] + batch_results = await asyncio.gather(*tasks) + else: + log.debug(f'generate_multiple_async: Processing {len(batches)} batches sequentially') + batch_results = [] + for batch in batches: + batch_results.append(await embedding_function(batch, prefix=prefix, user=user)) + + # Flatten results — raise if any batch failed + embeddings = [] + for i, batch_embeddings in enumerate(batch_results): + if batch_embeddings is None: + raise Exception(f'Embedding generation failed for batch {i + 1}/{len(batches)}') + embeddings.extend(batch_embeddings) + + log.debug( + f'generate_multiple_async: Generated {len(embeddings)} embeddings from {len(batches)} parallel batches' + ) + return embeddings + else: + return await embedding_function(query, prefix, user) + + return async_embedding_function + else: + raise ValueError(f'Unknown embedding engine: {embedding_engine}') + + +async def generate_embeddings( + engine: str, + model: str, + text: Union[str, list[str]], + prefix: Union[str, None] = None, + **kwargs, +): + url = kwargs.get('url', '') + key = kwargs.get('key', '') + user = kwargs.get('user') + + if prefix is not None and RAG_EMBEDDING_PREFIX_FIELD_NAME is None: + if isinstance(text, list): + text = [f'{prefix}{text_element}' for text_element in text] + else: + text = f'{prefix}{text}' + + if engine == 'ollama': + embeddings = await agenerate_ollama_batch_embeddings( + **{ + 'model': model, + 'texts': text if isinstance(text, list) else [text], + 'url': url, + 'key': key, + 'prefix': prefix, + 'user': user, + } + ) + if embeddings is None: + return None + return embeddings[0] if isinstance(text, str) else embeddings + elif engine == 'openai': + embeddings = await agenerate_openai_batch_embeddings( + model, text if isinstance(text, list) else [text], url, key, prefix, user + ) + if embeddings is None: + return None + return embeddings[0] if isinstance(text, str) else embeddings + elif engine == 'azure_openai': + azure_api_version = kwargs.get('azure_api_version', '') + embeddings = await agenerate_azure_openai_batch_embeddings( + model, + text if isinstance(text, list) else [text], + url, + key, + azure_api_version, + prefix, + user, + ) + if embeddings is None: + return None + return embeddings[0] if isinstance(text, str) else embeddings + + +def get_reranking_function(reranking_engine, reranking_model, reranking_function, reranking_batch_size=32): + if reranking_function is None: + return None + if reranking_engine == 'external': + return lambda query, documents, user=None: reranking_function.predict( + [(query, doc.page_content) for doc in documents], user=user + ) + else: + return lambda query, documents, user=None: reranking_function.predict( + [(query, doc.page_content) for doc in documents], batch_size=int(reranking_batch_size) + ) + + +async def filter_accessible_collections( + collection_names: set[str], + user: UserModel, + access_type: str = 'read', +) -> set[str]: + """ + Return only the collection names the user is allowed to access. + Admins bypass all checks. For non-admins the policy is: + + - file-* → validated via has_access_to_file + - user-memory-* → must match user's own memory collection + - web-search-* → ephemeral per-query collections, always allowed + - knowledge-bases → always denied (system meta-collection) + - everything else → if the name matches a knowledge base, validated + via Knowledges.check_access_by_user_id; if no + such KB exists, the name is treated as an + ephemeral/legacy collection and allowed + """ + if user.role == 'admin': + return collection_names + + validated = set() + for name in collection_names: + if name == 'knowledge-bases': + # System meta-collection — never exposed to non-admins. + continue + elif name.startswith('file-'): + file_id = name[len('file-') :] + if await has_access_to_file(file_id=file_id, access_type=access_type, user=user): + validated.add(name) + elif name.startswith('user-memory-'): + if name == f'user-memory-{user.id}': + validated.add(name) + elif name.startswith('web-search-'): + # Ephemeral collections created by process_web_search — safe + # to allow because they contain only transient web-search + # results scoped to the requesting user's session. + validated.add(name) + else: + # May be a knowledge-base ID or a legacy/ephemeral collection. + # If it IS a KB, enforce access control. If no such KB + # exists, treat it as a non-sensitive collection (e.g. legacy + # model knowledge, process_text SHA256 collections) and allow. + if await Knowledges.check_access_by_user_id(name, user.id, permission=access_type): + validated.add(name) + elif not await Knowledges.get_knowledge_by_id(name): + # Not a KB at all — legacy/ephemeral collection, allow + validated.add(name) + return validated + + +async def get_sources_from_items( + request, + items, + queries, + embedding_function, + k, + reranking_function, + k_reranker, + r, + hybrid_bm25_weight, + hybrid_search, + full_context=False, + user: Optional[UserModel] = None, +): + log.debug(f'items: {items} {queries} {embedding_function} {reranking_function} {full_context}') + + extracted_collections = [] + query_results = [] + + for item in items: + query_result = None + collection_names = [] + + if item.get('type') == 'text': + # Raw Text + # Used during temporary chat file uploads or web page & youtube attachements + + if item.get('context') == 'full': + if item.get('file'): + # if item has file data, use it + query_result = { + 'documents': [[item.get('file', {}).get('data', {}).get('content')]], + 'metadatas': [[item.get('file', {}).get('meta', {})]], + } + + if query_result is None: + # Fallback + if item.get('collection_name'): + # If item has a collection name, use it + collection_names.append(item.get('collection_name')) + elif item.get('file'): + # If item has file data, use it + query_result = { + 'documents': [[item.get('file', {}).get('data', {}).get('content')]], + 'metadatas': [[item.get('file', {}).get('meta', {})]], + } + else: + # Fallback to item content + query_result = { + 'documents': [[item.get('content')]], + 'metadatas': [[{'file_id': item.get('id'), 'name': item.get('name')}]], + } + + elif item.get('type') == 'note': + # Note Attached + note = await Notes.get_note_by_id(item.get('id')) + + if note and ( + user.role == 'admin' + or note.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='note', + resource_id=note.id, + permission='read', + ) + ): + # User has access to the note + query_result = { + 'documents': [[note.data.get('content', {}).get('md', '')]], + 'metadatas': [[{'file_id': note.id, 'name': note.title}]], + } + + elif item.get('type') == 'chat': + # Chat Attached + chat = await Chats.get_chat_by_id(item.get('id')) + + if chat and (user.role == 'admin' or chat.user_id == user.id): + messages_map = chat.chat.get('history', {}).get('messages', {}) + message_id = chat.chat.get('history', {}).get('currentId') + + if messages_map and message_id: + # Reconstruct the message list in order + message_list = get_message_list(messages_map, message_id) + message_history = '\n'.join( + [f'#### {m.get("role", "user").capitalize()}\n{m.get("content")}\n' for m in message_list] + ) + + # User has access to the chat + query_result = { + 'documents': [[message_history]], + 'metadatas': [[{'file_id': chat.id, 'name': chat.title}]], + } + + elif item.get('type') == 'url': + content, docs = get_content_from_url(request, item.get('url')) + if docs: + query_result = { + 'documents': [[content]], + 'metadatas': [[{'url': item.get('url'), 'name': item.get('url')}]], + } + elif item.get('type') == 'file': + if item.get('context') == 'full' or request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL: + if item.get('file', {}).get('data', {}).get('content', ''): + # Manual Full Mode Toggle + # Used from chat file modal, we can assume that the file content will be available from item.get("file").get("data", {}).get("content") + query_result = { + 'documents': [[item.get('file', {}).get('data', {}).get('content', '')]], + 'metadatas': [ + [ + { + 'file_id': item.get('id'), + 'name': item.get('name'), + **item.get('file').get('data', {}).get('metadata', {}), + } + ] + ], + } + elif item.get('id'): + file_object = await Files.get_file_by_id(item.get('id')) + if file_object and ( + user.role == 'admin' + or file_object.user_id == user.id + or await has_access_to_file(item.get('id'), 'read', user) + ): + query_result = { + 'documents': [[file_object.data.get('content', '')]], + 'metadatas': [ + [ + { + 'file_id': item.get('id'), + 'name': file_object.filename, + 'source': file_object.filename, + } + ] + ], + } + else: + # Fallback to collection names + if item.get('legacy'): + collection_names.append(f'{item["id"]}') + else: + collection_names.append(f'file-{item["id"]}') + + elif item.get('type') == 'collection': + # Manual Full Mode Toggle for Collection + knowledge_base = await Knowledges.get_knowledge_by_id(item.get('id')) + + if knowledge_base and ( + user.role == 'admin' + or knowledge_base.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge_base.id, + permission='read', + ) + ): + if item.get('context') == 'full' or request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL: + if knowledge_base and ( + user.role == 'admin' + or knowledge_base.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge_base.id, + permission='read', + ) + ): + files = await Knowledges.get_files_by_id(knowledge_base.id) + + documents = [] + metadatas = [] + for file in files: + documents.append(file.data.get('content', '')) + metadatas.append( + { + 'file_id': file.id, + 'name': file.filename, + 'source': file.filename, + } + ) + + query_result = { + 'documents': [documents], + 'metadatas': [metadatas], + } + else: + # Fallback to collection names + if item.get('legacy'): + collection_names = item.get('collection_names', []) + else: + collection_names.append(item['id']) + + elif item.get('docs'): + # BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL + query_result = { + 'documents': [[doc.get('content') for doc in item.get('docs')]], + 'metadatas': [[doc.get('metadata') for doc in item.get('docs')]], + } + elif item.get('collection_name'): + # Direct Collection Name + collection_names.append(item['collection_name']) + elif item.get('collection_names'): + # Collection Names List + collection_names.extend(item['collection_names']) + + # If query_result is None + # Fallback to collection names and vector search the collections + if query_result is None and collection_names: + collection_names = set(collection_names).difference(extracted_collections) + if not collection_names: + log.debug(f'skipping {item} as it has already been extracted') + continue + + # Filter out collections the user cannot read + if user: + collection_names = await filter_accessible_collections(collection_names, user) + if not collection_names: + log.debug(f'access denied for all collections in item {item}') + continue + + try: + if full_context: + # Sync helper makes blocking VECTOR_DB_CLIENT calls; + # offload so the async caller's event loop stays free. + query_result = await asyncio.to_thread(get_all_items_from_collections, collection_names) + else: + query_result = await query_collection( + request, + collection_names=collection_names, + queries=queries, + embedding_function=embedding_function, + k=k, + ) + except Exception as e: + log.exception(e) + + extracted_collections.extend(collection_names) + + if query_result: + if 'data' in item: + del item['data'] + query_results.append({**query_result, 'file': item}) + + sources = [] + for query_result in query_results: + try: + if 'documents' in query_result: + if 'metadatas' in query_result: + source = { + 'source': query_result['file'], + 'document': query_result['documents'][0], + 'metadata': query_result['metadatas'][0], + } + if 'distances' in query_result and query_result['distances']: + source['distances'] = query_result['distances'][0] + + sources.append(source) + except Exception as e: + log.exception(e) + return sources + + +def get_model_path(model: str, update_model: bool = False): + # Construct huggingface_hub kwargs with local_files_only to return the snapshot path + cache_dir = os.getenv('SENTENCE_TRANSFORMERS_HOME') + + local_files_only = not update_model + + if OFFLINE_MODE: + local_files_only = True + + snapshot_kwargs = { + 'cache_dir': cache_dir, + 'local_files_only': local_files_only, + } + + log.debug(f'model: {model}') + log.debug(f'snapshot_kwargs: {snapshot_kwargs}') + + # Inspiration from upstream sentence_transformers + if os.path.exists(model) or ('\\' in model or model.count('/') > 1) and local_files_only: + # If fully qualified path exists, return input, else set repo_id + return model + elif '/' not in model: + # Set valid repo_id for model short-name + model = 'sentence-transformers' + '/' + model + + snapshot_kwargs['repo_id'] = model + + # Attempt to query the huggingface_hub library to determine the local path and/or to update + try: + model_repo_path = snapshot_download(**snapshot_kwargs) + log.debug(f'model_repo_path: {model_repo_path}') + return model_repo_path + except Exception as e: + log.exception(f'Cannot determine model snapshot path: {e}') + if OFFLINE_MODE: + raise + return model + + +import operator +from typing import Optional, Sequence + +from langchain_core.callbacks import Callbacks +from langchain_core.documents import BaseDocumentCompressor, Document + + +class RerankCompressor(BaseDocumentCompressor): + embedding_function: Any + top_n: int + reranking_function: Any + r_score: float + + class Config: + extra = 'forbid' + arbitrary_types_allowed = True + + def compress_documents( + self, + documents: Sequence[Document], + query: str, + callbacks: Optional[Callbacks] = None, + ) -> Sequence[Document]: + """Compress retrieved documents given the query context. + + Args: + documents: The retrieved documents. + query: The query context. + callbacks: Optional callbacks to run during compression. + + Returns: + The compressed documents. + + """ + return [] + + async def acompress_documents( + self, + documents: Sequence[Document], + query: str, + callbacks: Optional[Callbacks] = None, + ) -> Sequence[Document]: + reranking = self.reranking_function is not None + + scores = None + if reranking: + scores = await asyncio.to_thread(self.reranking_function, query, documents) + else: + from sentence_transformers import util + + query_embedding = await self.embedding_function(query, RAG_EMBEDDING_QUERY_PREFIX) + document_embedding = await self.embedding_function( + [doc.page_content for doc in documents], RAG_EMBEDDING_CONTENT_PREFIX + ) + scores = util.cos_sim(query_embedding, document_embedding)[0] + + if scores is not None: + docs_with_scores = list( + zip( + documents, + scores.tolist() if not isinstance(scores, list) else scores, + ) + ) + if self.r_score: + docs_with_scores = [(d, s) for d, s in docs_with_scores if s >= self.r_score] + + result = sorted(docs_with_scores, key=operator.itemgetter(1), reverse=True) + final_results = [] + for doc, doc_score in result[: self.top_n]: + metadata = doc.metadata + metadata['score'] = doc_score + doc = Document( + page_content=doc.page_content, + metadata=metadata, + ) + final_results.append(doc) + return final_results + else: + log.warning('No valid scores found, check your reranking function. Returning original documents.') + return documents diff --git a/backend/open_webui/retrieval/vector/async_client.py b/backend/open_webui/retrieval/vector/async_client.py new file mode 100644 index 0000000000000000000000000000000000000000..0bea6696a9dbf7fa749d42e763722c244c7643a2 --- /dev/null +++ b/backend/open_webui/retrieval/vector/async_client.py @@ -0,0 +1,129 @@ +""" +Async facade over the synchronous VECTOR_DB_CLIENT. + +The vector DB backends bundled with Open WebUI (Chroma, pgvector, Qdrant, +Milvus, OpenSearch, Pinecone, Weaviate, …) all expose a uniformly +synchronous API. Each method performs blocking network or disk I/O — and +some, like `insert`/`upsert`, can run for several seconds. + +When such a sync method is awaited from an async route handler, it blocks +the event loop for its entire duration, freezing every other in-flight +HTTP request, websocket message and background task. + +This module wraps the sync client in an `AsyncVectorDBClient` that +transparently dispatches each call to a worker thread via +`asyncio.to_thread`. Async callers can `await ASYNC_VECTOR_DB_CLIENT.x(...)` +in place of `VECTOR_DB_CLIENT.x(...)` and the loop stays responsive. + +The original `VECTOR_DB_CLIENT` is unchanged, so callers already running +inside `run_in_threadpool` (e.g. `save_docs_to_vector_db`) are not +affected. + +Thread-safety expectations +-------------------------- +Every async caller now invokes `VECTOR_DB_CLIENT` from a worker thread +rather than the event-loop thread, and many can run concurrently. The +sync client (and its underlying backend driver) is therefore expected +to be safe for concurrent use across threads, which is the standard +contract for the bundled drivers (chroma, pgvector via SQLAlchemy +pool, qdrant-client, opensearch-py, …). This is *not* a new exposure +introduced by this facade — `save_docs_to_vector_db` already called +the sync client from `run_in_threadpool`, so concurrent threaded +access has always been a requirement of the codebase. Adding a global +serialization lock here would defeat the responsiveness this facade +exists to provide; any backend that genuinely cannot tolerate +concurrent access should grow its own internal serialization. + +API surface +----------- +Method signatures mirror `VectorDBBase` exactly. This is deliberate: +permissive `*args/**kwargs` forwarding hides typos at the call site +(an earlier revision of this file shipped that, and a `metadata=` +typo silently broke an entire endpoint until explicit signatures +surfaced it). Callers that need a backend-specific parameter not on +`VectorDBBase` should reach for the `.sync` escape hatch and wrap +their own `asyncio.to_thread`, e.g. :: + + await asyncio.to_thread( + ASYNC_VECTOR_DB_CLIENT.sync.some_backend_specific_op, + collection_name, special_kwarg=value, + ) +""" + +from __future__ import annotations + +import asyncio +from typing import Dict, List, Optional, Union + +from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) + + +class AsyncVectorDBClient: + """Awaitable mirror of `VectorDBBase` that off-loads each call to a thread. + + Method signatures mirror `VectorDBBase` exactly so static analysis + catches bad kwargs at the call site instead of letting them surface + deep inside the worker thread (where the resulting ``TypeError`` is + typically swallowed by surrounding ``try/except``). + """ + + def __init__(self, sync_client: VectorDBBase) -> None: + self._sync = sync_client + + @property + def sync(self) -> VectorDBBase: + """Escape hatch for code that must call the sync client directly + (e.g. already inside a worker thread).""" + return self._sync + + async def has_collection(self, collection_name: str) -> bool: + return await asyncio.to_thread(self._sync.has_collection, collection_name) + + async def delete_collection(self, collection_name: str) -> None: + return await asyncio.to_thread(self._sync.delete_collection, collection_name) + + async def insert(self, collection_name: str, items: List[VectorItem]) -> None: + return await asyncio.to_thread(self._sync.insert, collection_name, items) + + async def upsert(self, collection_name: str, items: List[VectorItem]) -> None: + return await asyncio.to_thread(self._sync.upsert, collection_name, items) + + async def search( + self, + collection_name: str, + vectors: List[List[Union[float, int]]], + filter: Optional[Dict] = None, + limit: int = 10, + ) -> Optional[SearchResult]: + return await asyncio.to_thread(self._sync.search, collection_name, vectors, filter, limit) + + async def query( + self, + collection_name: str, + filter: Dict, + limit: Optional[int] = None, + ) -> Optional[GetResult]: + return await asyncio.to_thread(self._sync.query, collection_name, filter, limit) + + async def get(self, collection_name: str) -> Optional[GetResult]: + return await asyncio.to_thread(self._sync.get, collection_name) + + async def delete( + self, + collection_name: str, + ids: Optional[List[str]] = None, + filter: Optional[Dict] = None, + ) -> None: + return await asyncio.to_thread(self._sync.delete, collection_name, ids, filter) + + async def reset(self) -> None: + return await asyncio.to_thread(self._sync.reset) + + +ASYNC_VECTOR_DB_CLIENT = AsyncVectorDBClient(VECTOR_DB_CLIENT) diff --git a/backend/open_webui/retrieval/vector/dbs/chroma.py b/backend/open_webui/retrieval/vector/dbs/chroma.py new file mode 100644 index 0000000000000000000000000000000000000000..4ace732b2dd0a1717ff5abcb36414fe33359ced1 --- /dev/null +++ b/backend/open_webui/retrieval/vector/dbs/chroma.py @@ -0,0 +1,189 @@ +import chromadb +import logging +from chromadb import Settings +from chromadb.utils.batch_utils import create_batches + +from typing import Optional + +from open_webui.retrieval.vector.main import ( + VectorDBBase, + VectorItem, + SearchResult, + GetResult, +) +from open_webui.retrieval.vector.utils import process_metadata + +from open_webui.config import ( + CHROMA_DATA_PATH, + CHROMA_HTTP_HOST, + CHROMA_HTTP_PORT, + CHROMA_HTTP_HEADERS, + CHROMA_HTTP_SSL, + CHROMA_TENANT, + CHROMA_DATABASE, + CHROMA_CLIENT_AUTH_PROVIDER, + CHROMA_CLIENT_AUTH_CREDENTIALS, +) + +log = logging.getLogger(__name__) + + +class ChromaClient(VectorDBBase): + def __init__(self): + settings_dict = { + 'allow_reset': True, + 'anonymized_telemetry': False, + } + if CHROMA_CLIENT_AUTH_PROVIDER is not None: + settings_dict['chroma_client_auth_provider'] = CHROMA_CLIENT_AUTH_PROVIDER + if CHROMA_CLIENT_AUTH_CREDENTIALS is not None: + settings_dict['chroma_client_auth_credentials'] = CHROMA_CLIENT_AUTH_CREDENTIALS + + if CHROMA_HTTP_HOST != '': + self.client = chromadb.HttpClient( + host=CHROMA_HTTP_HOST, + port=CHROMA_HTTP_PORT, + headers=CHROMA_HTTP_HEADERS, + ssl=CHROMA_HTTP_SSL, + tenant=CHROMA_TENANT, + database=CHROMA_DATABASE, + settings=Settings(**settings_dict), + ) + else: + self.client = chromadb.PersistentClient( + path=CHROMA_DATA_PATH, + settings=Settings(**settings_dict), + tenant=CHROMA_TENANT, + database=CHROMA_DATABASE, + ) + + def has_collection(self, collection_name: str) -> bool: + # Check if the collection exists based on the collection name. + collection_names = self.client.list_collections() + return collection_name in collection_names + + def delete_collection(self, collection_name: str): + # Delete the collection based on the collection name. + return self.client.delete_collection(name=collection_name) + + def search( + self, + collection_name: str, + vectors: list[list[float | int]], + filter: Optional[dict] = None, + limit: int = 10, + ) -> Optional[SearchResult]: + # Search for the nearest neighbor items based on the vectors and return 'limit' number of results. + try: + collection = self.client.get_collection(name=collection_name) + if collection: + result = collection.query( + query_embeddings=vectors, + n_results=limit, + where=filter, + ) + + # chromadb has cosine distance, 2 (worst) -> 0 (best). Re-odering to 0 -> 1 + # https://docs.trychroma.com/docs/collections/configure cosine equation + distances: list = result['distances'][0] + distances = [2 - dist for dist in distances] + distances = [[dist / 2 for dist in distances]] + + return SearchResult( + **{ + 'ids': result['ids'], + 'distances': distances, + 'documents': result['documents'], + 'metadatas': result['metadatas'], + } + ) + return None + except Exception as e: + return None + + def query(self, collection_name: str, filter: dict, limit: Optional[int] = None) -> Optional[GetResult]: + # Query the items from the collection based on the filter. + try: + collection = self.client.get_collection(name=collection_name) + if collection: + result = collection.get( + where=filter, + limit=limit, + ) + + return GetResult( + **{ + 'ids': [result['ids']], + 'documents': [result['documents']], + 'metadatas': [result['metadatas']], + } + ) + return None + except Exception: + return None + + def get(self, collection_name: str) -> Optional[GetResult]: + # Get all the items in the collection. + collection = self.client.get_collection(name=collection_name) + if collection: + result = collection.get() + return GetResult( + **{ + 'ids': [result['ids']], + 'documents': [result['documents']], + 'metadatas': [result['metadatas']], + } + ) + return None + + def insert(self, collection_name: str, items: list[VectorItem]): + # Insert the items into the collection, if the collection does not exist, it will be created. + collection = self.client.get_or_create_collection(name=collection_name, metadata={'hnsw:space': 'cosine'}) + + ids = [item['id'] for item in items] + documents = [item['text'] for item in items] + embeddings = [item['vector'] for item in items] + metadatas = [process_metadata(item['metadata']) for item in items] + + for batch in create_batches( + api=self.client, + documents=documents, + embeddings=embeddings, + ids=ids, + metadatas=metadatas, + ): + collection.add(*batch) + + def upsert(self, collection_name: str, items: list[VectorItem]): + # Update the items in the collection, if the items are not present, insert them. If the collection does not exist, it will be created. + collection = self.client.get_or_create_collection(name=collection_name, metadata={'hnsw:space': 'cosine'}) + + ids = [item['id'] for item in items] + documents = [item['text'] for item in items] + embeddings = [item['vector'] for item in items] + metadatas = [process_metadata(item['metadata']) for item in items] + + collection.upsert(ids=ids, documents=documents, embeddings=embeddings, metadatas=metadatas) + + def delete( + self, + collection_name: str, + ids: Optional[list[str]] = None, + filter: Optional[dict] = None, + ): + # Delete the items from the collection based on the ids. + try: + collection = self.client.get_collection(name=collection_name) + if collection: + if ids: + collection.delete(ids=ids) + elif filter: + collection.delete(where=filter) + except Exception as e: + # If collection doesn't exist, that's fine - nothing to delete + log.debug(f'Attempted to delete from non-existent collection {collection_name}. Ignoring.') + pass + + def reset(self): + # Resets the database. This will delete all collections and item entries. + return self.client.reset() diff --git a/backend/open_webui/retrieval/vector/dbs/elasticsearch.py b/backend/open_webui/retrieval/vector/dbs/elasticsearch.py new file mode 100644 index 0000000000000000000000000000000000000000..201a5e1706625573ffe7be007034bb3db332a785 --- /dev/null +++ b/backend/open_webui/retrieval/vector/dbs/elasticsearch.py @@ -0,0 +1,291 @@ +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + +from elasticsearch import Elasticsearch, BadRequestError +from typing import Optional +import ssl +from elasticsearch.helpers import bulk, scan + +from open_webui.retrieval.vector.utils import process_metadata +from open_webui.retrieval.vector.main import ( + VectorDBBase, + VectorItem, + SearchResult, + GetResult, +) +from open_webui.config import ( + ELASTICSEARCH_URL, + ELASTICSEARCH_CA_CERTS, + ELASTICSEARCH_API_KEY, + ELASTICSEARCH_USERNAME, + ELASTICSEARCH_PASSWORD, + ELASTICSEARCH_CLOUD_ID, + ELASTICSEARCH_INDEX_PREFIX, + SSL_ASSERT_FINGERPRINT, +) + + +class ElasticsearchClient(VectorDBBase): + """ + Important: + in order to reduce the number of indexes and since the embedding vector length is fixed, we avoid creating + an index for each file but store it as a text field, while seperating to different index + baesd on the embedding length. + """ + + def __init__(self): + self.index_prefix = ELASTICSEARCH_INDEX_PREFIX + self.client = Elasticsearch( + hosts=[ELASTICSEARCH_URL], + ca_certs=ELASTICSEARCH_CA_CERTS, + api_key=ELASTICSEARCH_API_KEY, + cloud_id=ELASTICSEARCH_CLOUD_ID, + basic_auth=( + (ELASTICSEARCH_USERNAME, ELASTICSEARCH_PASSWORD) + if ELASTICSEARCH_USERNAME and ELASTICSEARCH_PASSWORD + else None + ), + ssl_assert_fingerprint=SSL_ASSERT_FINGERPRINT, + ) + + # Status: works + def _get_index_name(self, dimension: int) -> str: + return f'{self.index_prefix}_d{str(dimension)}' + + # Status: works + def _scan_result_to_get_result(self, result) -> GetResult: + if not result: + return None + ids = [] + documents = [] + metadatas = [] + + for hit in result: + ids.append(hit['_id']) + documents.append(hit['_source'].get('text')) + metadatas.append(hit['_source'].get('metadata')) + + return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas]) + + # Status: works + def _result_to_get_result(self, result) -> GetResult: + if not result['hits']['hits']: + return None + ids = [] + documents = [] + metadatas = [] + + for hit in result['hits']['hits']: + ids.append(hit['_id']) + documents.append(hit['_source'].get('text')) + metadatas.append(hit['_source'].get('metadata')) + + return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas]) + + # Status: works + def _result_to_search_result(self, result) -> SearchResult: + ids = [] + distances = [] + documents = [] + metadatas = [] + + for hit in result['hits']['hits']: + ids.append(hit['_id']) + distances.append(hit['_score']) + documents.append(hit['_source'].get('text')) + metadatas.append(hit['_source'].get('metadata')) + + return SearchResult( + ids=[ids], + distances=[distances], + documents=[documents], + metadatas=[metadatas], + ) + + # Status: works + def _create_index(self, dimension: int): + body = { + 'mappings': { + 'dynamic_templates': [ + { + 'strings': { + 'match_mapping_type': 'string', + 'mapping': {'type': 'keyword'}, + } + } + ], + 'properties': { + 'collection': {'type': 'keyword'}, + 'id': {'type': 'keyword'}, + 'vector': { + 'type': 'dense_vector', + 'dims': dimension, # Adjust based on your vector dimensions + 'index': True, + 'similarity': 'cosine', + }, + 'text': {'type': 'text'}, + 'metadata': {'type': 'object'}, + }, + } + } + self.client.indices.create(index=self._get_index_name(dimension), body=body) + + # Status: works + + def _create_batches(self, items: list[VectorItem], batch_size=100): + for i in range(0, len(items), batch_size): + yield items[i : min(i + batch_size, len(items))] + + # Status: works + def has_collection(self, collection_name) -> bool: + query_body = {'query': {'bool': {'filter': []}}} + query_body['query']['bool']['filter'].append({'term': {'collection': collection_name}}) + + try: + result = self.client.count(index=f'{self.index_prefix}*', body=query_body) + + return result.body['count'] > 0 + except Exception as e: + return None + + def delete_collection(self, collection_name: str): + query = {'query': {'term': {'collection': collection_name}}} + self.client.delete_by_query(index=f'{self.index_prefix}*', body=query) + + # Status: works + def search( + self, + collection_name: str, + vectors: list[list[float]], + filter: Optional[dict] = None, + limit: int = 10, + ) -> Optional[SearchResult]: + query = { + 'size': limit, + '_source': ['text', 'metadata'], + 'query': { + 'script_score': { + 'query': {'bool': {'filter': [{'term': {'collection': collection_name}}]}}, + 'script': { + 'source': "cosineSimilarity(params.vector, 'vector') + 1.0", + 'params': {'vector': vectors[0]}, # Assuming single query vector + }, + } + }, + } + + result = self.client.search(index=self._get_index_name(len(vectors[0])), body=query) + + return self._result_to_search_result(result) + + # Status: only tested halfwat + def query(self, collection_name: str, filter: dict, limit: Optional[int] = None) -> Optional[GetResult]: + if not self.has_collection(collection_name): + return None + + query_body = { + 'query': {'bool': {'filter': []}}, + '_source': ['text', 'metadata'], + } + + for field, value in filter.items(): + query_body['query']['bool']['filter'].append({'term': {field: value}}) + query_body['query']['bool']['filter'].append({'term': {'collection': collection_name}}) + size = limit if limit else 10 + + try: + result = self.client.search( + index=f'{self.index_prefix}*', + body=query_body, + size=size, + ) + + return self._result_to_get_result(result) + + except Exception as e: + return None + + # Status: works + def _has_index(self, dimension: int): + return self.client.indices.exists(index=self._get_index_name(dimension=dimension)) + + def get_or_create_index(self, dimension: int): + if not self._has_index(dimension=dimension): + self._create_index(dimension=dimension) + + # Status: works + def get(self, collection_name: str) -> Optional[GetResult]: + # Get all the items in the collection. + query = { + 'query': {'bool': {'filter': [{'term': {'collection': collection_name}}]}}, + '_source': ['text', 'metadata'], + } + results = list(scan(self.client, index=f'{self.index_prefix}*', query=query)) + + return self._scan_result_to_get_result(results) + + # Status: works + def insert(self, collection_name: str, items: list[VectorItem]): + if not self._has_index(dimension=len(items[0]['vector'])): + self._create_index(dimension=len(items[0]['vector'])) + + for batch in self._create_batches(items): + actions = [ + { + '_index': self._get_index_name(dimension=len(items[0]['vector'])), + '_id': item['id'], + '_source': { + 'collection': collection_name, + 'vector': item['vector'], + 'text': item['text'], + 'metadata': process_metadata(item['metadata']), + }, + } + for item in batch + ] + bulk(self.client, actions) + + # Upsert documents using the update API with doc_as_upsert=True. + def upsert(self, collection_name: str, items: list[VectorItem]): + if not self._has_index(dimension=len(items[0]['vector'])): + self._create_index(dimension=len(items[0]['vector'])) + for batch in self._create_batches(items): + actions = [ + { + '_op_type': 'update', + '_index': self._get_index_name(dimension=len(item['vector'])), + '_id': item['id'], + 'doc': { + 'collection': collection_name, + 'vector': item['vector'], + 'text': item['text'], + 'metadata': process_metadata(item['metadata']), + }, + 'doc_as_upsert': True, + } + for item in batch + ] + bulk(self.client, actions) + + # Delete specific documents from a collection by filtering on both collection and document IDs. + def delete( + self, + collection_name: str, + ids: Optional[list[str]] = None, + filter: Optional[dict] = None, + ): + query = {'query': {'bool': {'filter': [{'term': {'collection': collection_name}}]}}} + # logic based on chromaDB + if ids: + query['query']['bool']['filter'].append({'terms': {'_id': ids}}) + elif filter: + for field, value in filter.items(): + query['query']['bool']['filter'].append({'term': {f'metadata.{field}': value}}) + + self.client.delete_by_query(index=f'{self.index_prefix}*', body=query) + + def reset(self): + indices = self.client.indices.get(index=f'{self.index_prefix}*') + for index in indices: + self.client.indices.delete(index=index) diff --git a/backend/open_webui/retrieval/vector/dbs/mariadb_vector.py b/backend/open_webui/retrieval/vector/dbs/mariadb_vector.py new file mode 100644 index 0000000000000000000000000000000000000000..1cb3563382979e9eb0809cabcbeb4fa13d4ad3fd --- /dev/null +++ b/backend/open_webui/retrieval/vector/dbs/mariadb_vector.py @@ -0,0 +1,583 @@ +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + +from __future__ import annotations + +import array +import json +import logging +import math +import re +import sys +from contextlib import contextmanager +from typing import Any, Dict, List, Optional, Tuple + +from sqlalchemy import create_engine +from sqlalchemy.pool import NullPool, QueuePool + +from open_webui.config import ( + MARIADB_VECTOR_DB_URL, + MARIADB_VECTOR_DISTANCE_STRATEGY, + MARIADB_VECTOR_INDEX_M, + MARIADB_VECTOR_INITIALIZE_MAX_VECTOR_LENGTH, + MARIADB_VECTOR_POOL_SIZE, + MARIADB_VECTOR_POOL_MAX_OVERFLOW, + MARIADB_VECTOR_POOL_TIMEOUT, + MARIADB_VECTOR_POOL_RECYCLE, +) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) +from open_webui.retrieval.vector.utils import process_metadata + +log = logging.getLogger(__name__) + +VECTOR_LENGTH = int(MARIADB_VECTOR_INITIALIZE_MAX_VECTOR_LENGTH) + + +def _embedding_to_f32_bytes(vec: List[float]) -> bytes: + """ + Convert a Python float vector into the binary payload expected by MariaDB VECTOR. + + MariaDB Vector expects the vector argument to be bound as a little-endian float32 + byte sequence. We use array('f') to avoid a numpy dependency and byteswap on + big-endian platforms for portability. + """ + a = array.array('f', [float(x) for x in vec]) # float32 + if sys.byteorder != 'little': + a.byteswap() + return a.tobytes() + + +def _safe_json(v: Any) -> Dict[str, Any]: + """ + Normalize a potentially JSON-like value into a Python dict. + + Accepts: + - dict: returned as-is + - str / bytes: parsed as JSON if possible + - None / other types: returns {} + """ + if v is None: + return {} + if isinstance(v, dict): + return v + if isinstance(v, (bytes, bytearray)): + try: + v = v.decode('utf-8') + except Exception: + return {} + if isinstance(v, str): + try: + j = json.loads(v) + return j if isinstance(j, dict) else {} + except Exception: + return {} + return {} + + +class MariaDBVectorClient(VectorDBBase): + """ + MariaDB + MariaDB Vector backend using DBAPI cursor parameter binding. + + IMPORTANT: + - Intended for: mariadb+mariadbconnector://... (official MariaDB driver). + - Uses qmark ("?") params and binds vectors as float32 bytes. + - Uses binary binding for BOTH inserts/updates and distance computations. + """ + + def __init__( + self, + db_url: Optional[str] = None, + vector_length: int = VECTOR_LENGTH, + distance_strategy: str = MARIADB_VECTOR_DISTANCE_STRATEGY, + index_m: int = MARIADB_VECTOR_INDEX_M, + ) -> None: + """ + Initialize a MariaDB Vector-backed VectorDBBase implementation. + + Validates URL scheme/driver requirements, ensures schema exists, and guards + against dimension mismatch with an existing VECTOR(n) column. + """ + self.db_url = (db_url or MARIADB_VECTOR_DB_URL).strip() + self.vector_length = int(vector_length) + self.distance_strategy = (distance_strategy or 'cosine').strip().lower() + self.index_m = int(index_m) + + if self.distance_strategy not in {'cosine', 'euclidean'}: + raise ValueError("distance_strategy must be 'cosine' or 'euclidean'") + + if not self.db_url.lower().startswith('mariadb+mariadbconnector://'): + raise ValueError( + 'MariaDBVectorClient requires mariadb+mariadbconnector:// (official MariaDB driver) ' + 'to ensure qmark paramstyle and correct VECTOR binding.' + ) + + if isinstance(MARIADB_VECTOR_POOL_SIZE, int): + if MARIADB_VECTOR_POOL_SIZE > 0: + self.engine = create_engine( + self.db_url, + pool_size=MARIADB_VECTOR_POOL_SIZE, + max_overflow=MARIADB_VECTOR_POOL_MAX_OVERFLOW, + pool_timeout=MARIADB_VECTOR_POOL_TIMEOUT, + pool_recycle=MARIADB_VECTOR_POOL_RECYCLE, + pool_pre_ping=True, + poolclass=QueuePool, + ) + else: + self.engine = create_engine(self.db_url, pool_pre_ping=True, poolclass=NullPool) + else: + self.engine = create_engine(self.db_url, pool_pre_ping=True) + self._init_schema() + self._check_vector_length() + + @contextmanager + def _connect(self): + """ + Yield a context-managed DBAPI connection (SQLAlchemy raw_connection()). + + Callers can use: + with self._connect() as conn: + with conn.cursor() as cur: + ... + """ + conn = self.engine.raw_connection() + try: + yield conn + finally: + try: + conn.close() + except Exception: + pass + + def _init_schema(self) -> None: + """ + Create the backing table and vector index if they do not exist. + + Uses a PK definition compatible with MariaDB Vector's VECTOR INDEX key-size constraints. + """ + with self._connect() as conn: + with conn.cursor() as cur: + try: + dist = self.distance_strategy + cur.execute(f""" + CREATE TABLE IF NOT EXISTS document_chunk ( + -- MariaDB Vector requires the table PRIMARY KEY used with a VECTOR INDEX to be <= 256 bytes. + -- VARCHAR has internal length/metadata overhead, so VARCHAR(255) can exceed the 256-byte limit. + -- We use VARCHAR(254) to stay safely under the limit, and force ASCII (1 byte/char) so the byte + -- size is predictable (avoid utf8mb4 where a "255 char" key could be up to 1020 bytes). + -- ascii_bin gives bytewise, case-sensitive comparisons for stable ID matching. + id VARCHAR(254) CHARACTER SET ascii COLLATE ascii_bin PRIMARY KEY, + embedding VECTOR({self.vector_length}) NOT NULL, + collection_name VARCHAR(255) NOT NULL, + text LONGTEXT NULL, + vmetadata JSON NULL, + VECTOR INDEX (embedding) M={self.index_m} DISTANCE={dist}, + INDEX idx_document_chunk_collection_name (collection_name) + ) ENGINE=InnoDB; + """) + conn.commit() + except Exception as e: + conn.rollback() + log.exception(f'Error during database initialization: {e}') + raise + + def _check_vector_length(self) -> None: + """ + Validate that the existing VECTOR column dimension matches this client's configured dimension. + + Dimension guard: if table already exists with + a different VECTOR(n), refuse to silently mismatch. + """ + with self._connect() as conn: + with conn.cursor() as cur: + cur.execute('SHOW CREATE TABLE document_chunk') + row = cur.fetchone() + if not row or len(row) < 2: + return + ddl = row[1] + m = re.search(r'vector\\((\\d+)\\)', ddl, flags=re.IGNORECASE) + if not m: + return + existing = int(m.group(1)) + if existing != int(self.vector_length): + raise Exception( + f'VECTOR_LENGTH {self.vector_length} does not match existing vector column dimension {existing}. ' + 'Cannot change vector size after initialization without migrating the data.' + ) + + def adjust_vector_length(self, vector: List[float]) -> List[float]: + """ + Pad or truncate a vector to match `self.vector_length`. + """ + n = len(vector) + if n < self.vector_length: + return vector + [0.0] * (self.vector_length - n) + if n > self.vector_length: + return vector[: self.vector_length] + return vector + + def _dist_fn(self) -> str: + """ + Return the MariaDB Vector distance function name for the configured strategy. + """ + return 'vec_distance_cosine' if self.distance_strategy == 'cosine' else 'vec_distance_euclidean' + + def _score_from_dist(self, dist: float) -> float: + """ + Convert a DB distance value into a normalized score in (0, 1]. + + - cosine: score ~= 1 - cosine_distance, clamped to [0, 1] + - euclidean: score = 1 / (1 + dist) + """ + if self.distance_strategy == 'cosine': + score = 1.0 - dist + if score < 0.0: + score = 0.0 + if score > 1.0: + score = 1.0 + return score + return 1.0 / (1.0 + max(0.0, dist)) + + def _build_filter_sql_qmark(self, expr: Any) -> Tuple[str, List[Any]]: + """ + Build a WHERE-clause fragment and qmark params from a minimal Mongo-like filter. + + Supported forms: + - {"field": "v"} + - {"field": {"$in": ["a","b"]}} + - {"$and": [ ... ]} + - {"$or": [ ... ]} + """ + if not expr or not isinstance(expr, dict): + return '', [] + + if '$and' in expr: + parts: List[str] = [] + params: List[Any] = [] + for e in expr.get('$and') or []: + s, p = self._build_filter_sql_qmark(e) + if s: + parts.append(s) + params.extend(p) + return ('(' + ' AND '.join(parts) + ')') if parts else '', params + + if '$or' in expr: + parts: List[str] = [] + params: List[Any] = [] + for e in expr.get('$or') or []: + s, p = self._build_filter_sql_qmark(e) + if s: + parts.append(s) + params.extend(p) + return ('(' + ' OR '.join(parts) + ')') if parts else '', params + + clauses: List[str] = [] + params: List[Any] = [] + for key, value in expr.items(): + if key.startswith('$'): + continue + json_expr = f"JSON_UNQUOTE(JSON_EXTRACT(vmetadata, '$.{key}'))" + if isinstance(value, dict) and '$in' in value: + vals = [str(v) for v in (value.get('$in') or [])] + if not vals: + clauses.append('0=1') + continue + ors = [] + for v in vals: + ors.append(f'{json_expr} = ?') + params.append(v) + clauses.append('(' + ' OR '.join(ors) + ')') + else: + clauses.append(f'{json_expr} = ?') + params.append(str(value)) + return ('(' + ' AND '.join(clauses) + ')') if clauses else '', params + + def insert(self, collection_name: str, items: List[VectorItem]) -> None: + """ + Insert items into the given collection (best-effort, ignores duplicates). + + Uses executemany() with binary VECTOR binding for high-throughput ingestion. + """ + if not items: + return + with self._connect() as conn: + with conn.cursor() as cur: + try: + sql = """ + INSERT IGNORE INTO document_chunk + (id, embedding, collection_name, text, vmetadata) + VALUES + (?, ?, ?, ?, ?) + """ + params: List[Tuple[Any, ...]] = [] + for item in items: + v = self.adjust_vector_length(item['vector']) + emb = _embedding_to_f32_bytes(v) + meta = process_metadata(item.get('metadata') or {}) + params.append( + ( + item['id'], + emb, + collection_name, + item.get('text'), + json.dumps(meta), + ) + ) + cur.executemany(sql, params) + conn.commit() + except Exception as e: + conn.rollback() + log.exception(f'Error during insert: {e}') + raise + + def upsert(self, collection_name: str, items: List[VectorItem]) -> None: + """ + Insert or update items in the given collection by primary key. + + Uses executemany() and updates embedding/text/metadata on conflicts. + """ + if not items: + return + with self._connect() as conn: + with conn.cursor() as cur: + try: + sql = """ + INSERT INTO document_chunk + (id, embedding, collection_name, text, vmetadata) + VALUES + (?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + embedding = VALUES(embedding), + collection_name = VALUES(collection_name), + text = VALUES(text), + vmetadata = VALUES(vmetadata) + """ + params: List[Tuple[Any, ...]] = [] + for item in items: + v = self.adjust_vector_length(item['vector']) + emb = _embedding_to_f32_bytes(v) + meta = process_metadata(item.get('metadata') or {}) + params.append( + ( + item['id'], + emb, + collection_name, + item.get('text'), + json.dumps(meta), + ) + ) + cur.executemany(sql, params) + conn.commit() + except Exception as e: + conn.rollback() + log.exception(f'Error during upsert: {e}') + raise + + def search( + self, + collection_name: str, + vectors: List[List[float]], + filter: Optional[Dict[str, Any]] = None, + limit: int = 10, + ) -> Optional[SearchResult]: + """ + Perform a vector similarity search. + + Args: + collection_name: Logical collection partition key. + vectors: One or more query vectors. + filter: Optional metadata filter (Mongo-like subset). + limit: Top-k per query vector. + + Returns a SearchResult where distances are normalized scores (higher is better). + """ + if not vectors: + return None + + dist_fn = self._dist_fn() + ids: List[List[str]] = [[] for _ in vectors] + distances: List[List[float]] = [[] for _ in vectors] + documents: List[List[str]] = [[] for _ in vectors] + metadatas: List[List[Any]] = [[] for _ in vectors] + + try: + with self._connect() as conn: + with conn.cursor() as cur: + fsql, fparams = self._build_filter_sql_qmark(filter or {}) + where = 'collection_name = ?' + base_params: List[Any] = [collection_name] + if fsql: + where = where + ' AND ' + fsql + base_params.extend(fparams) + + sql = f""" + SELECT + id, + text, + vmetadata, + {dist_fn}(embedding, ?) AS dist + FROM document_chunk + WHERE {where} + ORDER BY dist ASC + LIMIT ? + """ + + for q_idx, q in enumerate(vectors): + qv = self.adjust_vector_length(q) + qbin = _embedding_to_f32_bytes(qv) + params = [qbin] + list(base_params) + [int(limit)] + cur.execute(sql, params) + rows = cur.fetchall() + + for r in rows: + rid, rtext, rmeta, rdist = r[0], r[1], r[2], r[3] + ids[q_idx].append(str(rid)) + try: + dist = float(rdist) if rdist is not None else 1.0 + except Exception: + dist = 1.0 + if math.isnan(dist) or math.isinf(dist): + dist = 1.0 + distances[q_idx].append(self._score_from_dist(dist)) + documents[q_idx].append(rtext) + metadatas[q_idx].append(_safe_json(rmeta)) + + return SearchResult( + ids=ids, + distances=distances, + documents=documents, + metadatas=metadatas, + ) + except Exception as e: + log.exception(f'[MARIADB_VECTOR] search() failed: {e}') + return None + + def query(self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None) -> Optional[GetResult]: + """ + Retrieve documents by metadata filter (non-vector query). + """ + with self._connect() as conn: + with conn.cursor() as cur: + fsql, fparams = self._build_filter_sql_qmark(filter or {}) + where = 'collection_name = ?' + params: List[Any] = [collection_name] + if fsql: + where = where + ' AND ' + fsql + params.extend(fparams) + sql = f'SELECT id, text, vmetadata FROM document_chunk WHERE {where}' + if limit is not None: + sql += ' LIMIT ?' + params.append(int(limit)) + cur.execute(sql, params) + rows = cur.fetchall() + if not rows: + return None + ids = [[str(r[0]) for r in rows]] + documents = [[r[1] for r in rows]] + metadatas = [[_safe_json(r[2]) for r in rows]] + return GetResult(ids=ids, documents=documents, metadatas=metadatas) + + def get(self, collection_name: str, limit: Optional[int] = None) -> Optional[GetResult]: + """ + Retrieve documents in a collection without filtering (optionally limited). + """ + with self._connect() as conn: + with conn.cursor() as cur: + sql = 'SELECT id, text, vmetadata FROM document_chunk WHERE collection_name = ?' + params: List[Any] = [collection_name] + if limit is not None: + sql += ' LIMIT ?' + params.append(int(limit)) + cur.execute(sql, params) + rows = cur.fetchall() + if not rows: + return None + ids = [[str(r[0]) for r in rows]] + documents = [[r[1] for r in rows]] + metadatas = [[_safe_json(r[2]) for r in rows]] + return GetResult(ids=ids, documents=documents, metadatas=metadatas) + + def delete( + self, + collection_name: str, + ids: Optional[List[str]] = None, + filter: Optional[Dict[str, Any]] = None, + ) -> None: + """ + Delete rows from a collection by id list and/or metadata filter. + + If both are provided, they are combined with AND semantics. + """ + with self._connect() as conn: + with conn.cursor() as cur: + try: + where = ['collection_name = ?'] + params: List[Any] = [collection_name] + + if ids: + ph = ', '.join(['?'] * len(ids)) + where.append(f'id IN ({ph})') + params.extend(ids) + + if filter: + fsql, fparams = self._build_filter_sql_qmark(filter) + if fsql: + where.append(fsql) + params.extend(fparams) + + sql = 'DELETE FROM document_chunk WHERE ' + ' AND '.join(where) + cur.execute(sql, params) + conn.commit() + except Exception as e: + conn.rollback() + log.exception(f'Error during delete: {e}') + raise + + def reset(self) -> None: + """ + Truncate the vector table (drops all collections). + """ + with self._connect() as conn: + with conn.cursor() as cur: + try: + cur.execute('TRUNCATE TABLE document_chunk') + conn.commit() + except Exception as e: + conn.rollback() + log.exception(f'Error during reset: {e}') + raise + + def has_collection(self, collection_name: str) -> bool: + """ + Return True if the collection contains at least one row, else False. + """ + try: + with self._connect() as conn: + with conn.cursor() as cur: + cur.execute( + 'SELECT 1 FROM document_chunk WHERE collection_name = ? LIMIT 1', + (collection_name,), + ) + return cur.fetchone() is not None + except Exception: + return False + + def delete_collection(self, collection_name: str) -> None: + """ + Delete all rows in a collection. + """ + self.delete(collection_name) + + def close(self) -> None: + """ + Dispose the underlying SQLAlchemy engine. + """ + try: + self.engine.dispose() + except Exception as e: + log.exception(f'Error during dispose the underlying SQLAlchemy engine: {e}') diff --git a/backend/open_webui/retrieval/vector/dbs/milvus.py b/backend/open_webui/retrieval/vector/dbs/milvus.py new file mode 100644 index 0000000000000000000000000000000000000000..2f3d8f389045c126a673b04b1f6ec197238c0e98 --- /dev/null +++ b/backend/open_webui/retrieval/vector/dbs/milvus.py @@ -0,0 +1,362 @@ +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + +from pymilvus import MilvusClient as Client +from pymilvus import FieldSchema, DataType +from pymilvus import connections, Collection + +import json +import logging +from typing import Optional + +from open_webui.retrieval.vector.utils import process_metadata +from open_webui.retrieval.vector.main import ( + VectorDBBase, + VectorItem, + SearchResult, + GetResult, +) +from open_webui.config import ( + MILVUS_URI, + MILVUS_DB, + MILVUS_TOKEN, + MILVUS_INDEX_TYPE, + MILVUS_METRIC_TYPE, + MILVUS_HNSW_M, + MILVUS_HNSW_EFCONSTRUCTION, + MILVUS_IVF_FLAT_NLIST, + MILVUS_DISKANN_MAX_DEGREE, + MILVUS_DISKANN_SEARCH_LIST_SIZE, +) + +log = logging.getLogger(__name__) + + +class MilvusClient(VectorDBBase): + def __init__(self): + self.collection_prefix = 'open_webui' + if MILVUS_TOKEN is None: + self.client = Client(uri=MILVUS_URI, db_name=MILVUS_DB) + else: + self.client = Client(uri=MILVUS_URI, db_name=MILVUS_DB, token=MILVUS_TOKEN) + + def _result_to_get_result(self, result) -> GetResult: + ids = [] + documents = [] + metadatas = [] + for match in result: + _ids = [] + _documents = [] + _metadatas = [] + for item in match: + _ids.append(item.get('id')) + _documents.append(item.get('data', {}).get('text')) + _metadatas.append(item.get('metadata')) + ids.append(_ids) + documents.append(_documents) + metadatas.append(_metadatas) + return GetResult( + **{ + 'ids': ids, + 'documents': documents, + 'metadatas': metadatas, + } + ) + + def _result_to_search_result(self, result) -> SearchResult: + ids = [] + distances = [] + documents = [] + metadatas = [] + for match in result: + _ids = [] + _distances = [] + _documents = [] + _metadatas = [] + for item in match: + _ids.append(item.get('id')) + # normalize milvus score from [-1, 1] to [0, 1] range + # https://milvus.io/docs/de/metric.md + _dist = (item.get('distance') + 1.0) / 2.0 + _distances.append(_dist) + _documents.append(item.get('entity', {}).get('data', {}).get('text')) + _metadatas.append(item.get('entity', {}).get('metadata')) + ids.append(_ids) + distances.append(_distances) + documents.append(_documents) + metadatas.append(_metadatas) + return SearchResult( + **{ + 'ids': ids, + 'distances': distances, + 'documents': documents, + 'metadatas': metadatas, + } + ) + + def _create_collection(self, collection_name: str, dimension: int): + schema = self.client.create_schema( + auto_id=False, + enable_dynamic_field=True, + ) + schema.add_field( + field_name='id', + datatype=DataType.VARCHAR, + is_primary=True, + max_length=65535, + ) + schema.add_field( + field_name='vector', + datatype=DataType.FLOAT_VECTOR, + dim=dimension, + description='vector', + ) + schema.add_field(field_name='data', datatype=DataType.JSON, description='data') + schema.add_field(field_name='metadata', datatype=DataType.JSON, description='metadata') + + index_params = self.client.prepare_index_params() + + # Use configurations from config.py + index_type = MILVUS_INDEX_TYPE.upper() + metric_type = MILVUS_METRIC_TYPE.upper() + + log.info(f'Using Milvus index type: {index_type}, metric type: {metric_type}') + + index_creation_params = {} + if index_type == 'HNSW': + index_creation_params = { + 'M': MILVUS_HNSW_M, + 'efConstruction': MILVUS_HNSW_EFCONSTRUCTION, + } + log.info(f'HNSW params: {index_creation_params}') + elif index_type == 'IVF_FLAT': + index_creation_params = {'nlist': MILVUS_IVF_FLAT_NLIST} + log.info(f'IVF_FLAT params: {index_creation_params}') + elif index_type == 'DISKANN': + index_creation_params = { + 'max_degree': MILVUS_DISKANN_MAX_DEGREE, + 'search_list_size': MILVUS_DISKANN_SEARCH_LIST_SIZE, + } + log.info(f'DISKANN params: {index_creation_params}') + elif index_type in ['FLAT', 'AUTOINDEX']: + log.info(f'Using {index_type} index with no specific build-time params.') + else: + log.warning( + f"Unsupported MILVUS_INDEX_TYPE: '{index_type}'. " + f'Supported types: HNSW, IVF_FLAT, DISKANN, FLAT, AUTOINDEX. ' + f'Milvus will use its default for the collection if this type is not directly supported for index creation.' + ) + # For unsupported types, pass the type directly to Milvus; it might handle it or use a default. + # If Milvus errors out, the user needs to correct the MILVUS_INDEX_TYPE env var. + + index_params.add_index( + field_name='vector', + index_type=index_type, + metric_type=metric_type, + params=index_creation_params, + ) + + self.client.create_collection( + collection_name=f'{self.collection_prefix}_{collection_name}', + schema=schema, + index_params=index_params, + ) + log.info( + f"Successfully created collection '{self.collection_prefix}_{collection_name}' with index type '{index_type}' and metric '{metric_type}'." + ) + + def has_collection(self, collection_name: str) -> bool: + # Check if the collection exists based on the collection name. + collection_name = collection_name.replace('-', '_') + return self.client.has_collection(collection_name=f'{self.collection_prefix}_{collection_name}') + + def delete_collection(self, collection_name: str): + # Delete the collection based on the collection name. + collection_name = collection_name.replace('-', '_') + return self.client.drop_collection(collection_name=f'{self.collection_prefix}_{collection_name}') + + def search( + self, + collection_name: str, + vectors: list[list[float | int]], + filter: Optional[dict] = None, + limit: int = 10, + ) -> Optional[SearchResult]: + # Search for the nearest neighbor items based on the vectors and return 'limit' number of results. + collection_name = collection_name.replace('-', '_') + # For some index types like IVF_FLAT, search params like nprobe can be set. + # Example: search_params = {"nprobe": 10} if using IVF_FLAT + # For simplicity, not adding configurable search_params here, but could be extended. + result = self.client.search( + collection_name=f'{self.collection_prefix}_{collection_name}', + data=vectors, + limit=limit, + output_fields=['data', 'metadata'], + # search_params=search_params # Potentially add later if needed + ) + return self._result_to_search_result(result) + + def query(self, collection_name: str, filter: dict, limit: int = -1): + connections.connect(uri=MILVUS_URI, token=MILVUS_TOKEN, db_name=MILVUS_DB) + + collection_name = collection_name.replace('-', '_') + if not self.has_collection(collection_name): + log.warning(f'Query attempted on non-existent collection: {self.collection_prefix}_{collection_name}') + return None + + filter_expressions = [] + for key, value in filter.items(): + if isinstance(value, str): + filter_expressions.append(f'metadata["{key}"] == "{value}"') + else: + filter_expressions.append(f'metadata["{key}"] == {value}') + + filter_string = ' && '.join(filter_expressions) + + collection = Collection(f'{self.collection_prefix}_{collection_name}') + collection.load() + + try: + log.info( + f"Querying collection {self.collection_prefix}_{collection_name} with filter: '{filter_string}', limit: {limit}" + ) + + iterator = collection.query_iterator( + expr=filter_string, + output_fields=[ + 'id', + 'data', + 'metadata', + ], + limit=limit if limit > 0 else -1, + ) + + all_results = [] + while True: + batch = iterator.next() + if not batch: + iterator.close() + break + all_results.extend(batch) + + log.debug(f'Total results from query: {len(all_results)}') + return self._result_to_get_result([all_results] if all_results else [[]]) + + except Exception as e: + log.exception( + f"Error querying collection {self.collection_prefix}_{collection_name} with filter '{filter_string}' and limit {limit}: {e}" + ) + return None + + def get(self, collection_name: str) -> Optional[GetResult]: + # Get all the items in the collection. This can be very resource-intensive for large collections. + collection_name = collection_name.replace('-', '_') + log.warning( + f"Fetching ALL items from collection '{self.collection_prefix}_{collection_name}'. This might be slow for large collections." + ) + # Using query with a trivial filter to get all items. + # This will use the paginated query logic. + return self.query(collection_name=collection_name, filter={}, limit=-1) + + def insert(self, collection_name: str, items: list[VectorItem]): + # Insert the items into the collection, if the collection does not exist, it will be created. + collection_name = collection_name.replace('-', '_') + if not self.client.has_collection(collection_name=f'{self.collection_prefix}_{collection_name}'): + log.info(f'Collection {self.collection_prefix}_{collection_name} does not exist. Creating now.') + if not items: + log.error( + f'Cannot create collection {self.collection_prefix}_{collection_name} without items to determine dimension.' + ) + raise ValueError('Cannot create Milvus collection without items to determine vector dimension.') + self._create_collection(collection_name=collection_name, dimension=len(items[0]['vector'])) + + log.info(f'Inserting {len(items)} items into collection {self.collection_prefix}_{collection_name}.') + return self.client.insert( + collection_name=f'{self.collection_prefix}_{collection_name}', + data=[ + { + 'id': item['id'], + 'vector': item['vector'], + 'data': {'text': item['text']}, + 'metadata': process_metadata(item['metadata']), + } + for item in items + ], + ) + + def upsert(self, collection_name: str, items: list[VectorItem]): + # Update the items in the collection, if the items are not present, insert them. If the collection does not exist, it will be created. + collection_name = collection_name.replace('-', '_') + if not self.client.has_collection(collection_name=f'{self.collection_prefix}_{collection_name}'): + log.info(f'Collection {self.collection_prefix}_{collection_name} does not exist for upsert. Creating now.') + if not items: + log.error( + f'Cannot create collection {self.collection_prefix}_{collection_name} for upsert without items to determine dimension.' + ) + raise ValueError( + 'Cannot create Milvus collection for upsert without items to determine vector dimension.' + ) + self._create_collection(collection_name=collection_name, dimension=len(items[0]['vector'])) + + log.info(f'Upserting {len(items)} items into collection {self.collection_prefix}_{collection_name}.') + return self.client.upsert( + collection_name=f'{self.collection_prefix}_{collection_name}', + data=[ + { + 'id': item['id'], + 'vector': item['vector'], + 'data': {'text': item['text']}, + 'metadata': process_metadata(item['metadata']), + } + for item in items + ], + ) + + def delete( + self, + collection_name: str, + ids: Optional[list[str]] = None, + filter: Optional[dict] = None, + ): + # Delete the items from the collection based on the ids or filter. + collection_name = collection_name.replace('-', '_') + if not self.has_collection(collection_name): + log.warning(f'Delete attempted on non-existent collection: {self.collection_prefix}_{collection_name}') + return None + + if ids: + log.info(f'Deleting items by IDs from {self.collection_prefix}_{collection_name}. IDs: {ids}') + return self.client.delete( + collection_name=f'{self.collection_prefix}_{collection_name}', + ids=ids, + ) + elif filter: + filter_string = ' && '.join([f'metadata["{key}"] == {json.dumps(value)}' for key, value in filter.items()]) + log.info( + f'Deleting items by filter from {self.collection_prefix}_{collection_name}. Filter: {filter_string}' + ) + return self.client.delete( + collection_name=f'{self.collection_prefix}_{collection_name}', + filter=filter_string, + ) + else: + log.warning( + f'Delete operation on {self.collection_prefix}_{collection_name} called without IDs or filter. No action taken.' + ) + return None + + def reset(self): + # Resets the database. This will delete all collections and item entries that match the prefix. + log.warning(f"Resetting Milvus: Deleting all collections with prefix '{self.collection_prefix}'.") + collection_names = self.client.list_collections() + deleted_collections = [] + for collection_name_full in collection_names: + if collection_name_full.startswith(self.collection_prefix): + try: + self.client.drop_collection(collection_name=collection_name_full) + deleted_collections.append(collection_name_full) + log.info(f'Deleted collection: {collection_name_full}') + except Exception as e: + log.error(f'Error deleting collection {collection_name_full}: {e}') + log.info(f'Milvus reset complete. Deleted collections: {deleted_collections}') diff --git a/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py b/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py new file mode 100644 index 0000000000000000000000000000000000000000..93b4a8cbc41e722a10d25476e507ddb63d99ffa3 --- /dev/null +++ b/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py @@ -0,0 +1,277 @@ +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + +import logging +from typing import Optional, Tuple, List, Dict, Any + +from open_webui.config import ( + MILVUS_URI, + MILVUS_TOKEN, + MILVUS_DB, + MILVUS_COLLECTION_PREFIX, + MILVUS_INDEX_TYPE, + MILVUS_METRIC_TYPE, + MILVUS_HNSW_M, + MILVUS_HNSW_EFCONSTRUCTION, + MILVUS_IVF_FLAT_NLIST, +) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) +from pymilvus import ( + connections, + utility, + Collection, + CollectionSchema, + FieldSchema, + DataType, +) + +log = logging.getLogger(__name__) + +RESOURCE_ID_FIELD = 'resource_id' + + +class MilvusClient(VectorDBBase): + def __init__(self): + # Milvus collection names can only contain numbers, letters, and underscores. + self.collection_prefix = MILVUS_COLLECTION_PREFIX.replace('-', '_') + connections.connect( + alias='default', + uri=MILVUS_URI, + token=MILVUS_TOKEN, + db_name=MILVUS_DB, + ) + + # Main collection types for multi-tenancy + self.MEMORY_COLLECTION = f'{self.collection_prefix}_memories' + self.KNOWLEDGE_COLLECTION = f'{self.collection_prefix}_knowledge' + self.FILE_COLLECTION = f'{self.collection_prefix}_files' + self.WEB_SEARCH_COLLECTION = f'{self.collection_prefix}_web_search' + self.HASH_BASED_COLLECTION = f'{self.collection_prefix}_hash_based' + self.shared_collections = [ + self.MEMORY_COLLECTION, + self.KNOWLEDGE_COLLECTION, + self.FILE_COLLECTION, + self.WEB_SEARCH_COLLECTION, + self.HASH_BASED_COLLECTION, + ] + + def _get_collection_and_resource_id(self, collection_name: str) -> Tuple[str, str]: + """ + Maps the traditional collection name to multi-tenant collection and resource ID. + + WARNING: This mapping relies on current Open WebUI naming conventions for + collection names. If Open WebUI changes how it generates collection names + (e.g., "user-memory-" prefix, "file-" prefix, web search patterns, or hash + formats), this mapping will break and route data to incorrect collections. + POTENTIALLY CAUSING HUGE DATA CORRUPTION, DATA CONSISTENCY ISSUES AND INCORRECT + DATA MAPPING INSIDE THE DATABASE. + """ + resource_id = collection_name + + if collection_name.startswith('user-memory-'): + return self.MEMORY_COLLECTION, resource_id + elif collection_name.startswith('file-'): + return self.FILE_COLLECTION, resource_id + elif collection_name.startswith('web-search-'): + return self.WEB_SEARCH_COLLECTION, resource_id + elif len(collection_name) == 63 and all(c in '0123456789abcdef' for c in collection_name): + return self.HASH_BASED_COLLECTION, resource_id + else: + return self.KNOWLEDGE_COLLECTION, resource_id + + def _create_shared_collection(self, mt_collection_name: str, dimension: int): + fields = [ + FieldSchema( + name='id', + dtype=DataType.VARCHAR, + is_primary=True, + auto_id=False, + max_length=36, + ), + FieldSchema(name='vector', dtype=DataType.FLOAT_VECTOR, dim=dimension), + FieldSchema(name='text', dtype=DataType.VARCHAR, max_length=65535), + FieldSchema(name='metadata', dtype=DataType.JSON), + FieldSchema(name=RESOURCE_ID_FIELD, dtype=DataType.VARCHAR, max_length=255), + ] + schema = CollectionSchema(fields, 'Shared collection for multi-tenancy') + collection = Collection(mt_collection_name, schema) + + index_params = { + 'metric_type': MILVUS_METRIC_TYPE, + 'index_type': MILVUS_INDEX_TYPE, + 'params': {}, + } + if MILVUS_INDEX_TYPE == 'HNSW': + index_params['params'] = { + 'M': MILVUS_HNSW_M, + 'efConstruction': MILVUS_HNSW_EFCONSTRUCTION, + } + elif MILVUS_INDEX_TYPE == 'IVF_FLAT': + index_params['params'] = {'nlist': MILVUS_IVF_FLAT_NLIST} + + collection.create_index('vector', index_params) + collection.create_index(RESOURCE_ID_FIELD) + log.info(f'Created shared collection: {mt_collection_name}') + return collection + + def _ensure_collection(self, mt_collection_name: str, dimension: int): + if not utility.has_collection(mt_collection_name): + self._create_shared_collection(mt_collection_name, dimension) + + def has_collection(self, collection_name: str) -> bool: + mt_collection, resource_id = self._get_collection_and_resource_id(collection_name) + if not utility.has_collection(mt_collection): + return False + + collection = Collection(mt_collection) + collection.load() + res = collection.query(expr=f"{RESOURCE_ID_FIELD} == '{resource_id}'", limit=1) + return len(res) > 0 + + def upsert(self, collection_name: str, items: List[VectorItem]): + if not items: + return + mt_collection, resource_id = self._get_collection_and_resource_id(collection_name) + dimension = len(items[0]['vector']) + self._ensure_collection(mt_collection, dimension) + collection = Collection(mt_collection) + + entities = [ + { + 'id': item['id'], + 'vector': item['vector'], + 'text': item['text'], + 'metadata': item['metadata'], + RESOURCE_ID_FIELD: resource_id, + } + for item in items + ] + collection.insert(entities) + + def search( + self, + collection_name: str, + vectors: List[List[float]], + filter: Optional[Dict] = None, + limit: int = 10, + ) -> Optional[SearchResult]: + if not vectors: + return None + + mt_collection, resource_id = self._get_collection_and_resource_id(collection_name) + if not utility.has_collection(mt_collection): + return None + + collection = Collection(mt_collection) + collection.load() + + search_params = {'metric_type': MILVUS_METRIC_TYPE, 'params': {}} + results = collection.search( + data=vectors, + anns_field='vector', + param=search_params, + limit=limit, + expr=f"{RESOURCE_ID_FIELD} == '{resource_id}'", + output_fields=['id', 'text', 'metadata'], + ) + + ids, documents, metadatas, distances = [], [], [], [] + for hits in results: + batch_ids, batch_docs, batch_metadatas, batch_dists = [], [], [], [] + for hit in hits: + batch_ids.append(hit.entity.get('id')) + batch_docs.append(hit.entity.get('text')) + batch_metadatas.append(hit.entity.get('metadata')) + batch_dists.append(hit.distance) + ids.append(batch_ids) + documents.append(batch_docs) + metadatas.append(batch_metadatas) + distances.append(batch_dists) + + return SearchResult(ids=ids, documents=documents, metadatas=metadatas, distances=distances) + + def delete( + self, + collection_name: str, + ids: Optional[List[str]] = None, + filter: Optional[Dict[str, Any]] = None, + ): + mt_collection, resource_id = self._get_collection_and_resource_id(collection_name) + if not utility.has_collection(mt_collection): + return + + collection = Collection(mt_collection) + + # Build expression + expr = [f"{RESOURCE_ID_FIELD} == '{resource_id}'"] + if ids: + # Milvus expects a string list for 'in' operator + id_list_str = ', '.join([f"'{id_val}'" for id_val in ids]) + expr.append(f'id in [{id_list_str}]') + + if filter: + for key, value in filter.items(): + expr.append(f"metadata['{key}'] == '{value}'") + + collection.delete(' and '.join(expr)) + + def reset(self): + for collection_name in self.shared_collections: + if utility.has_collection(collection_name): + utility.drop_collection(collection_name) + + def delete_collection(self, collection_name: str): + mt_collection, resource_id = self._get_collection_and_resource_id(collection_name) + if not utility.has_collection(mt_collection): + return + + collection = Collection(mt_collection) + collection.delete(f"{RESOURCE_ID_FIELD} == '{resource_id}'") + + def query(self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None) -> Optional[GetResult]: + mt_collection, resource_id = self._get_collection_and_resource_id(collection_name) + if not utility.has_collection(mt_collection): + return None + + collection = Collection(mt_collection) + collection.load() + + expr = [f"{RESOURCE_ID_FIELD} == '{resource_id}'"] + if filter: + for key, value in filter.items(): + if isinstance(value, str): + expr.append(f"metadata['{key}'] == '{value}'") + else: + expr.append(f"metadata['{key}'] == {value}") + + iterator = collection.query_iterator( + expr=' and '.join(expr), + output_fields=['id', 'text', 'metadata'], + limit=limit if limit else -1, + ) + + all_results = [] + while True: + batch = iterator.next() + if not batch: + iterator.close() + break + all_results.extend(batch) + + ids = [res['id'] for res in all_results] + documents = [res['text'] for res in all_results] + metadatas = [res['metadata'] for res in all_results] + + return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas]) + + def get(self, collection_name: str) -> Optional[GetResult]: + return self.query(collection_name, filter={}, limit=None) + + def insert(self, collection_name: str, items: List[VectorItem]): + return self.upsert(collection_name, items) diff --git a/backend/open_webui/retrieval/vector/dbs/opengauss.py b/backend/open_webui/retrieval/vector/dbs/opengauss.py new file mode 100644 index 0000000000000000000000000000000000000000..ac97cf01fae9d890c94a99190129586d80f133b3 --- /dev/null +++ b/backend/open_webui/retrieval/vector/dbs/opengauss.py @@ -0,0 +1,392 @@ +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + +from typing import Optional, List, Dict, Any +import logging +import re +import json +from sqlalchemy import ( + func, + literal, + cast, + column, + create_engine, + Column, + Integer, + MetaData, + LargeBinary, + select, + text, + Text, + Table, + values, +) +from sqlalchemy.sql import true +from sqlalchemy.pool import NullPool, QueuePool + +from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker +from sqlalchemy.dialects.postgresql import JSONB, array +from pgvector.sqlalchemy import Vector +from sqlalchemy.ext.mutable import MutableDict +from sqlalchemy.exc import NoSuchTableError + +from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2 +from sqlalchemy.dialects import registry + + +class OpenGaussDialect(PGDialect_psycopg2): + name = 'opengauss' + + def _get_server_version_info(self, connection): + try: + version = connection.exec_driver_sql('SELECT version()').scalar() + if not version: + return (9, 0, 0) + + match = re.search(r'openGauss\s+(\d+)\.(\d+)\.(\d+)(?:-\w+)?', version, re.IGNORECASE) + if match: + return (int(match.group(1)), int(match.group(2)), int(match.group(3))) + + return super()._get_server_version_info(connection) + except Exception: + return (9, 0, 0) + + +# Register dialect +registry.register('opengauss', __name__, 'OpenGaussDialect') + +from open_webui.retrieval.vector.utils import process_metadata +from open_webui.retrieval.vector.main import ( + VectorDBBase, + VectorItem, + SearchResult, + GetResult, +) +from open_webui.config import ( + OPENGAUSS_DB_URL, + OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH, + OPENGAUSS_POOL_SIZE, + OPENGAUSS_POOL_MAX_OVERFLOW, + OPENGAUSS_POOL_TIMEOUT, + OPENGAUSS_POOL_RECYCLE, +) + +from open_webui.env import SRC_LOG_LEVELS + +VECTOR_LENGTH = OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH +Base = declarative_base() + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS['RAG']) + + +class DocumentChunk(Base): + __tablename__ = 'document_chunk' + + id = Column(Text, primary_key=True) + vector = Column(Vector(dim=VECTOR_LENGTH), nullable=True) + collection_name = Column(Text, nullable=False) + text = Column(Text, nullable=True) + vmetadata = Column(MutableDict.as_mutable(JSONB), nullable=True) + + +class OpenGaussClient(VectorDBBase): + def __init__(self) -> None: + if not OPENGAUSS_DB_URL: + from open_webui.internal.db import ScopedSession + + self.session = ScopedSession + else: + engine_kwargs = {'pool_pre_ping': True, 'dialect': OpenGaussDialect()} + + if isinstance(OPENGAUSS_POOL_SIZE, int) and OPENGAUSS_POOL_SIZE > 0: + engine_kwargs.update( + { + 'pool_size': OPENGAUSS_POOL_SIZE, + 'max_overflow': OPENGAUSS_POOL_MAX_OVERFLOW, + 'pool_timeout': OPENGAUSS_POOL_TIMEOUT, + 'pool_recycle': OPENGAUSS_POOL_RECYCLE, + 'poolclass': QueuePool, + } + ) + else: + engine_kwargs['poolclass'] = NullPool + + engine = create_engine(OPENGAUSS_DB_URL, **engine_kwargs) + + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False) + self.session = scoped_session(SessionLocal) + + try: + connection = self.session.connection() + Base.metadata.create_all(bind=connection) + + self.session.execute( + text( + 'CREATE INDEX IF NOT EXISTS idx_document_chunk_vector ' + 'ON document_chunk USING ivfflat (vector vector_cosine_ops) WITH (lists = 100);' + ) + ) + self.session.execute( + text( + 'CREATE INDEX IF NOT EXISTS idx_document_chunk_collection_name ON document_chunk (collection_name);' + ) + ) + self.session.commit() + log.info('OpenGauss vector database initialization completed.') + except Exception as e: + self.session.rollback() + log.exception(f'OpenGauss Initialization failed.: {e}') + raise + + def check_vector_length(self) -> None: + metadata = MetaData() + try: + document_chunk_table = Table('document_chunk', metadata, autoload_with=self.session.bind) + except NoSuchTableError: + return + + if 'vector' in document_chunk_table.columns: + vector_column = document_chunk_table.columns['vector'] + vector_type = vector_column.type + if isinstance(vector_type, Vector): + db_vector_length = vector_type.dim + if db_vector_length != VECTOR_LENGTH: + raise Exception( + f'Vector dimension mismatch: configured {VECTOR_LENGTH} vs. {db_vector_length} in the database.' + ) + else: + raise Exception("The 'vector' column type is not Vector.") + else: + raise Exception("The 'vector' column does not exist in the 'document_chunk' table.") + + def adjust_vector_length(self, vector: List[float]) -> List[float]: + current_length = len(vector) + if current_length < VECTOR_LENGTH: + vector += [0.0] * (VECTOR_LENGTH - current_length) + elif current_length > VECTOR_LENGTH: + vector = vector[:VECTOR_LENGTH] + return vector + + def insert(self, collection_name: str, items: List[VectorItem]) -> None: + try: + new_items = [] + for item in items: + vector = self.adjust_vector_length(item['vector']) + new_chunk = DocumentChunk( + id=item['id'], + vector=vector, + collection_name=collection_name, + text=item['text'], + vmetadata=process_metadata(item['metadata']), + ) + new_items.append(new_chunk) + self.session.bulk_save_objects(new_items) + self.session.commit() + log.info(f"Inserting {len(new_items)} items into collection '{collection_name}'.") + except Exception as e: + self.session.rollback() + log.exception(f'Failed to insert data: {e}') + raise + + def upsert(self, collection_name: str, items: List[VectorItem]) -> None: + try: + for item in items: + vector = self.adjust_vector_length(item['vector']) + existing = self.session.query(DocumentChunk).filter(DocumentChunk.id == item['id']).first() + if existing: + existing.vector = vector + existing.text = item['text'] + existing.vmetadata = process_metadata(item['metadata']) + existing.collection_name = collection_name + else: + new_chunk = DocumentChunk( + id=item['id'], + vector=vector, + collection_name=collection_name, + text=item['text'], + vmetadata=process_metadata(item['metadata']), + ) + self.session.add(new_chunk) + self.session.commit() + log.info(f"Inserting/updating {len(items)} items in collection '{collection_name}'.") + except Exception as e: + self.session.rollback() + log.exception(f'Failed to insert or update data.: {e}') + raise + + def search( + self, + collection_name: str, + vectors: List[List[float]], + filter: Optional[Dict[str, Any]] = None, + limit: int = 10, + ) -> Optional[SearchResult]: + try: + if not vectors: + return None + + vectors = [self.adjust_vector_length(vector) for vector in vectors] + num_queries = len(vectors) + + def vector_expr(vector): + return cast(array(vector), Vector(VECTOR_LENGTH)) + + qid_col = column('qid', Integer) + q_vector_col = column('q_vector', Vector(VECTOR_LENGTH)) + query_vectors = ( + values(qid_col, q_vector_col) + .data([(idx, vector_expr(vector)) for idx, vector in enumerate(vectors)]) + .alias('query_vectors') + ) + + result_fields = [ + DocumentChunk.id, + DocumentChunk.text, + DocumentChunk.vmetadata, + (DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector)).label('distance'), + ] + + subq = ( + select(*result_fields) + .where(DocumentChunk.collection_name == collection_name) + .order_by(DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector)) + ) + if limit is not None: + subq = subq.limit(limit) + subq = subq.lateral('result') + + stmt = ( + select( + query_vectors.c.qid, + subq.c.id, + subq.c.text, + subq.c.vmetadata, + subq.c.distance, + ) + .select_from(query_vectors) + .join(subq, true()) + .order_by(query_vectors.c.qid, subq.c.distance) + ) + + result_proxy = self.session.execute(stmt) + results = result_proxy.all() + + ids = [[] for _ in range(num_queries)] + distances = [[] for _ in range(num_queries)] + documents = [[] for _ in range(num_queries)] + metadatas = [[] for _ in range(num_queries)] + + for row in results: + qid = int(row.qid) + ids[qid].append(row.id) + distances[qid].append((2.0 - row.distance) / 2.0) + documents[qid].append(row.text) + metadatas[qid].append(row.vmetadata) + + self.session.rollback() + return SearchResult(ids=ids, distances=distances, documents=documents, metadatas=metadatas) + except Exception as e: + self.session.rollback() + log.exception(f'Vector search failed: {e}') + return None + + def query(self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None) -> Optional[GetResult]: + try: + query = self.session.query(DocumentChunk).filter(DocumentChunk.collection_name == collection_name) + + for key, value in filter.items(): + query = query.filter(DocumentChunk.vmetadata[key].astext == str(value)) + + if limit is not None: + query = query.limit(limit) + + results = query.all() + + if not results: + return None + + ids = [[result.id for result in results]] + documents = [[result.text for result in results]] + metadatas = [[result.vmetadata for result in results]] + + self.session.rollback() + return GetResult(ids=ids, documents=documents, metadatas=metadatas) + except Exception as e: + self.session.rollback() + log.exception(f'Conditional query failed: {e}') + return None + + def get(self, collection_name: str, limit: Optional[int] = None) -> Optional[GetResult]: + try: + query = self.session.query(DocumentChunk).filter(DocumentChunk.collection_name == collection_name) + if limit is not None: + query = query.limit(limit) + + results = query.all() + + if not results: + return None + + ids = [[result.id for result in results]] + documents = [[result.text for result in results]] + metadatas = [[result.vmetadata for result in results]] + + self.session.rollback() + return GetResult(ids=ids, documents=documents, metadatas=metadatas) + except Exception as e: + self.session.rollback() + log.exception(f'Failed to retrieve data: {e}') + return None + + def delete( + self, + collection_name: str, + ids: Optional[List[str]] = None, + filter: Optional[Dict[str, Any]] = None, + ) -> None: + try: + query = self.session.query(DocumentChunk).filter(DocumentChunk.collection_name == collection_name) + if ids: + query = query.filter(DocumentChunk.id.in_(ids)) + if filter: + for key, value in filter.items(): + query = query.filter(DocumentChunk.vmetadata[key].astext == str(value)) + deleted = query.delete(synchronize_session=False) + self.session.commit() + log.info(f"Deleted {deleted} items from collection '{collection_name}'") + except Exception as e: + self.session.rollback() + log.exception(f'Failed to delete data: {e}') + raise + + def reset(self) -> None: + try: + deleted = self.session.query(DocumentChunk).delete() + self.session.commit() + log.info(f'Reset completed. Deleted {deleted} items') + except Exception as e: + self.session.rollback() + log.exception(f'Reset failed: {e}') + raise + + def close(self) -> None: + pass + + def has_collection(self, collection_name: str) -> bool: + try: + exists = ( + self.session.query(DocumentChunk).filter(DocumentChunk.collection_name == collection_name).first() + is not None + ) + self.session.rollback() + return exists + except Exception as e: + self.session.rollback() + log.exception(f'Failed to check collection existence: {e}') + return False + + def delete_collection(self, collection_name: str) -> None: + self.delete(collection_name) + log.info(f"Collection '{collection_name}' has been deleted") diff --git a/backend/open_webui/retrieval/vector/dbs/opensearch.py b/backend/open_webui/retrieval/vector/dbs/opensearch.py new file mode 100644 index 0000000000000000000000000000000000000000..a08dca7865140ff1880ff5f9c8cbf79276df6055 --- /dev/null +++ b/backend/open_webui/retrieval/vector/dbs/opensearch.py @@ -0,0 +1,257 @@ +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + +from opensearchpy import OpenSearch +from opensearchpy.helpers import bulk +from typing import Optional + +from open_webui.retrieval.vector.utils import process_metadata +from open_webui.retrieval.vector.main import ( + VectorDBBase, + VectorItem, + SearchResult, + GetResult, +) +from open_webui.config import ( + OPENSEARCH_URI, + OPENSEARCH_SSL, + OPENSEARCH_CERT_VERIFY, + OPENSEARCH_USERNAME, + OPENSEARCH_PASSWORD, +) + + +class OpenSearchClient(VectorDBBase): + def __init__(self): + self.index_prefix = 'open_webui' + self.client = OpenSearch( + hosts=[OPENSEARCH_URI], + use_ssl=OPENSEARCH_SSL, + verify_certs=OPENSEARCH_CERT_VERIFY, + http_auth=(OPENSEARCH_USERNAME, OPENSEARCH_PASSWORD), + ) + + def _get_index_name(self, collection_name: str) -> str: + return f'{self.index_prefix}_{collection_name}' + + def _result_to_get_result(self, result) -> GetResult: + if not result['hits']['hits']: + return None + + ids = [] + documents = [] + metadatas = [] + + for hit in result['hits']['hits']: + ids.append(hit['_id']) + documents.append(hit['_source'].get('text')) + metadatas.append(hit['_source'].get('metadata')) + + return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas]) + + def _result_to_search_result(self, result) -> SearchResult: + if not result['hits']['hits']: + return None + + ids = [] + distances = [] + documents = [] + metadatas = [] + + for hit in result['hits']['hits']: + ids.append(hit['_id']) + distances.append(hit['_score']) + documents.append(hit['_source'].get('text')) + metadatas.append(hit['_source'].get('metadata')) + + return SearchResult( + ids=[ids], + distances=[distances], + documents=[documents], + metadatas=[metadatas], + ) + + def _create_index(self, collection_name: str, dimension: int): + body = { + 'settings': {'index': {'knn': True}}, + 'mappings': { + 'properties': { + 'id': {'type': 'keyword'}, + 'vector': { + 'type': 'knn_vector', + 'dimension': dimension, # Adjust based on your vector dimensions + 'index': True, + 'similarity': 'faiss', + 'method': { + 'name': 'hnsw', + 'space_type': 'innerproduct', # Use inner product to approximate cosine similarity + 'engine': 'faiss', + 'parameters': { + 'ef_construction': 128, + 'm': 16, + }, + }, + }, + 'text': {'type': 'text'}, + 'metadata': {'type': 'object'}, + } + }, + } + self.client.indices.create(index=self._get_index_name(collection_name), body=body) + + def _create_batches(self, items: list[VectorItem], batch_size=100): + for i in range(0, len(items), batch_size): + yield items[i : i + batch_size] + + def has_collection(self, collection_name: str) -> bool: + # has_collection here means has index. + # We are simply adapting to the norms of the other DBs. + return self.client.indices.exists(index=self._get_index_name(collection_name)) + + def delete_collection(self, collection_name: str): + # delete_collection here means delete index. + # We are simply adapting to the norms of the other DBs. + self.client.indices.delete(index=self._get_index_name(collection_name)) + + def search( + self, + collection_name: str, + vectors: list[list[float | int]], + filter: Optional[dict] = None, + limit: int = 10, + ) -> Optional[SearchResult]: + try: + if not self.has_collection(collection_name): + return None + + query = { + 'size': limit, + '_source': ['text', 'metadata'], + 'query': { + 'script_score': { + 'query': {'match_all': {}}, + 'script': { + 'source': '(cosineSimilarity(params.query_value, doc[params.field]) + 1.0) / 2.0', + 'params': { + 'field': 'vector', + 'query_value': vectors[0], + }, # Assuming single query vector + }, + } + }, + } + + result = self.client.search(index=self._get_index_name(collection_name), body=query) + + return self._result_to_search_result(result) + + except Exception as e: + return None + + def query(self, collection_name: str, filter: dict, limit: Optional[int] = None) -> Optional[GetResult]: + if not self.has_collection(collection_name): + return None + + query_body = { + 'query': {'bool': {'filter': []}}, + '_source': ['text', 'metadata'], + } + + for field, value in filter.items(): + query_body['query']['bool']['filter'].append({'term': {'metadata.' + str(field) + '.keyword': value}}) + + size = limit if limit else 10000 + + try: + result = self.client.search( + index=self._get_index_name(collection_name), + body=query_body, + size=size, + ) + + return self._result_to_get_result(result) + + except Exception as e: + return None + + def _create_index_if_not_exists(self, collection_name: str, dimension: int): + if not self.has_collection(collection_name): + self._create_index(collection_name, dimension) + + def get(self, collection_name: str) -> Optional[GetResult]: + query = {'query': {'match_all': {}}, '_source': ['text', 'metadata']} + + result = self.client.search(index=self._get_index_name(collection_name), body=query) + return self._result_to_get_result(result) + + def insert(self, collection_name: str, items: list[VectorItem]): + self._create_index_if_not_exists(collection_name=collection_name, dimension=len(items[0]['vector'])) + + for batch in self._create_batches(items): + actions = [ + { + '_op_type': 'index', + '_index': self._get_index_name(collection_name), + '_id': item['id'], + '_source': { + 'vector': item['vector'], + 'text': item['text'], + 'metadata': process_metadata(item['metadata']), + }, + } + for item in batch + ] + bulk(self.client, actions) + self.client.indices.refresh(index=self._get_index_name(collection_name)) + + def upsert(self, collection_name: str, items: list[VectorItem]): + self._create_index_if_not_exists(collection_name=collection_name, dimension=len(items[0]['vector'])) + + for batch in self._create_batches(items): + actions = [ + { + '_op_type': 'update', + '_index': self._get_index_name(collection_name), + '_id': item['id'], + 'doc': { + 'vector': item['vector'], + 'text': item['text'], + 'metadata': process_metadata(item['metadata']), + }, + 'doc_as_upsert': True, + } + for item in batch + ] + bulk(self.client, actions) + self.client.indices.refresh(index=self._get_index_name(collection_name)) + + def delete( + self, + collection_name: str, + ids: Optional[list[str]] = None, + filter: Optional[dict] = None, + ): + if ids: + actions = [ + { + '_op_type': 'delete', + '_index': self._get_index_name(collection_name), + '_id': id, + } + for id in ids + ] + bulk(self.client, actions) + elif filter: + query_body = { + 'query': {'bool': {'filter': []}}, + } + for field, value in filter.items(): + query_body['query']['bool']['filter'].append({'term': {'metadata.' + str(field) + '.keyword': value}}) + self.client.delete_by_query(index=self._get_index_name(collection_name), body=query_body) + self.client.indices.refresh(index=self._get_index_name(collection_name)) + + def reset(self): + indices = self.client.indices.get(index=f'{self.index_prefix}_*') + for index in indices: + self.client.indices.delete(index=index) diff --git a/backend/open_webui/retrieval/vector/dbs/oracle23ai.py b/backend/open_webui/retrieval/vector/dbs/oracle23ai.py new file mode 100644 index 0000000000000000000000000000000000000000..9a5bd638d9e1fd9c464302b0bedec4e43fe0e6f3 --- /dev/null +++ b/backend/open_webui/retrieval/vector/dbs/oracle23ai.py @@ -0,0 +1,898 @@ +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. + +Oracle 23ai Vector Database Client - Fixed Version + +# .env +VECTOR_DB = "oracle23ai" + +## DBCS or oracle 23ai free +ORACLE_DB_USE_WALLET = false +ORACLE_DB_USER = "DEMOUSER" +ORACLE_DB_PASSWORD = "Welcome123456" +ORACLE_DB_DSN = "localhost:1521/FREEPDB1" + +## ADW or ATP +# ORACLE_DB_USE_WALLET = true +# ORACLE_DB_USER = "DEMOUSER" +# ORACLE_DB_PASSWORD = "Welcome123456" +# ORACLE_DB_DSN = "medium" +# ORACLE_DB_DSN = "(description= (retry_count=3)(retry_delay=3)(address=(protocol=tcps)(port=1522)(host=xx.oraclecloud.com))(connect_data=(service_name=yy.adb.oraclecloud.com))(security=(ssl_server_dn_match=no)))" +# ORACLE_WALLET_DIR = "/home/opc/adb_wallet" +# ORACLE_WALLET_PASSWORD = "Welcome1" + +ORACLE_VECTOR_LENGTH = 768 + +ORACLE_DB_POOL_MIN = 2 +ORACLE_DB_POOL_MAX = 10 +ORACLE_DB_POOL_INCREMENT = 1 +""" + +from typing import Optional, List, Dict, Any, Union +from decimal import Decimal +import logging +import os +import threading +import time +import json +import array +import oracledb + +from open_webui.retrieval.vector.main import ( + VectorDBBase, + VectorItem, + SearchResult, + GetResult, +) + +from open_webui.config import ( + ORACLE_DB_USE_WALLET, + ORACLE_DB_USER, + ORACLE_DB_PASSWORD, + ORACLE_DB_DSN, + ORACLE_WALLET_DIR, + ORACLE_WALLET_PASSWORD, + ORACLE_VECTOR_LENGTH, + ORACLE_DB_POOL_MIN, + ORACLE_DB_POOL_MAX, + ORACLE_DB_POOL_INCREMENT, +) + +log = logging.getLogger(__name__) + + +class Oracle23aiClient(VectorDBBase): + """ + Oracle Vector Database Client for vector similarity search using Oracle Database 23ai. + + This client provides an interface to store, retrieve, and search vector embeddings + in an Oracle database. It uses connection pooling for efficient database access + and supports vector similarity search operations. + + Attributes: + pool: Connection pool for Oracle database connections + """ + + def __init__(self) -> None: + """ + Initialize the Oracle23aiClient with a connection pool. + + Creates a connection pool with configurable min/max connections, initializes + the database schema if needed, and sets up necessary tables and indexes. + + Raises: + ValueError: If required configuration parameters are missing + Exception: If database initialization fails + """ + self.pool = None + + try: + # Create the appropriate connection pool based on DB type + if ORACLE_DB_USE_WALLET: + self._create_adb_pool() + else: # DBCS + self._create_dbcs_pool() + + dsn = ORACLE_DB_DSN + log.info(f'Creating Connection Pool [{ORACLE_DB_USER}:**@{dsn}]') + + with self.get_connection() as connection: + log.info(f'Connection version: {connection.version}') + self._initialize_database(connection) + + log.info('Oracle Vector Search initialization complete.') + except Exception as e: + log.exception(f'Error during Oracle Vector Search initialization: {e}') + raise + + def _create_adb_pool(self) -> None: + """ + Create connection pool for Oracle Autonomous Database. + + Uses wallet-based authentication. + """ + self.pool = oracledb.create_pool( + user=ORACLE_DB_USER, + password=ORACLE_DB_PASSWORD, + dsn=ORACLE_DB_DSN, + min=ORACLE_DB_POOL_MIN, + max=ORACLE_DB_POOL_MAX, + increment=ORACLE_DB_POOL_INCREMENT, + config_dir=ORACLE_WALLET_DIR, + wallet_location=ORACLE_WALLET_DIR, + wallet_password=ORACLE_WALLET_PASSWORD, + ) + log.info('Created ADB connection pool with wallet authentication.') + + def _create_dbcs_pool(self) -> None: + """ + Create connection pool for Oracle Database Cloud Service. + + Uses basic authentication without wallet. + """ + self.pool = oracledb.create_pool( + user=ORACLE_DB_USER, + password=ORACLE_DB_PASSWORD, + dsn=ORACLE_DB_DSN, + min=ORACLE_DB_POOL_MIN, + max=ORACLE_DB_POOL_MAX, + increment=ORACLE_DB_POOL_INCREMENT, + ) + log.info('Created DB connection pool with basic authentication.') + + def get_connection(self): + """ + Acquire a connection from the connection pool with retry logic. + + Returns: + connection: A database connection with output type handler configured + """ + max_retries = 3 + for attempt in range(max_retries): + try: + connection = self.pool.acquire() + connection.outputtypehandler = self._output_type_handler + return connection + except oracledb.DatabaseError as e: + (error_obj,) = e.args + log.exception(f'Connection attempt {attempt + 1} failed: {error_obj.message}') + + if attempt < max_retries - 1: + wait_time = 2**attempt + log.info(f'Retrying in {wait_time} seconds...') + time.sleep(wait_time) + else: + raise + + def start_health_monitor(self, interval_seconds: int = 60): + """ + Start a background thread to periodically check the health of the connection pool. + + Args: + interval_seconds (int): Number of seconds between health checks + """ + + def _monitor(): + while True: + try: + log.info('[HealthCheck] Running periodic DB health check...') + self.ensure_connection() + log.info('[HealthCheck] Connection is healthy.') + except Exception as e: + log.exception(f'[HealthCheck] Connection health check failed: {e}') + time.sleep(interval_seconds) + + thread = threading.Thread(target=_monitor, daemon=True) + thread.start() + log.info(f'Started DB health monitor every {interval_seconds} seconds.') + + def _reconnect_pool(self): + """ + Attempt to reinitialize the connection pool if it's been closed or broken. + """ + try: + log.info('Attempting to reinitialize the Oracle connection pool...') + + # Close existing pool if it exists + if self.pool: + try: + self.pool.close() + except Exception as close_error: + log.warning(f'Error closing existing pool: {close_error}') + + # Re-create the appropriate connection pool based on DB type + if ORACLE_DB_USE_WALLET: + self._create_adb_pool() + else: # DBCS + self._create_dbcs_pool() + + log.info('Connection pool reinitialized.') + except Exception as e: + log.exception(f'Failed to reinitialize the connection pool: {e}') + raise + + def ensure_connection(self): + """ + Ensure the database connection is alive, reconnecting pool if needed. + """ + try: + with self.get_connection() as connection: + with connection.cursor() as cursor: + cursor.execute('SELECT 1 FROM dual') + except Exception as e: + log.exception(f'Connection check failed: {e}, attempting to reconnect pool...') + self._reconnect_pool() + + def _output_type_handler(self, cursor, metadata): + """ + Handle Oracle vector type conversion. + + Args: + cursor: Oracle database cursor + metadata: Metadata for the column + + Returns: + A variable with appropriate conversion for vector types + """ + if metadata.type_code is oracledb.DB_TYPE_VECTOR: + return cursor.var(metadata.type_code, arraysize=cursor.arraysize, outconverter=list) + + def _initialize_database(self, connection) -> None: + """ + Initialize database schema, tables and indexes. + + Creates the document_chunk table and necessary indexes if they don't exist. + + Args: + connection: Oracle database connection + + Raises: + Exception: If schema initialization fails + """ + with connection.cursor() as cursor: + try: + log.info('Creating Table document_chunk') + cursor.execute( + """ + BEGIN + EXECUTE IMMEDIATE ' + CREATE TABLE IF NOT EXISTS document_chunk ( + id VARCHAR2(255) PRIMARY KEY, + collection_name VARCHAR2(255) NOT NULL, + text CLOB, + vmetadata JSON, + vector vector(*, float32) + ) + '; + EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -955 THEN + RAISE; + END IF; + END; + """ + ) + + log.info('Creating Index document_chunk_collection_name_idx') + cursor.execute( + """ + BEGIN + EXECUTE IMMEDIATE ' + CREATE INDEX IF NOT EXISTS document_chunk_collection_name_idx + ON document_chunk (collection_name) + '; + EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -955 THEN + RAISE; + END IF; + END; + """ + ) + + log.info('Creating VECTOR INDEX document_chunk_vector_ivf_idx') + cursor.execute( + """ + BEGIN + EXECUTE IMMEDIATE ' + CREATE VECTOR INDEX IF NOT EXISTS document_chunk_vector_ivf_idx + ON document_chunk(vector) + ORGANIZATION NEIGHBOR PARTITIONS + DISTANCE COSINE + WITH TARGET ACCURACY 95 + PARAMETERS (TYPE IVF, NEIGHBOR PARTITIONS 100) + '; + EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -955 THEN + RAISE; + END IF; + END; + """ + ) + + connection.commit() + log.info('Database initialization completed successfully.') + + except Exception as e: + connection.rollback() + log.exception(f'Error during database initialization: {e}') + raise + + def check_vector_length(self) -> None: + """ + Check vector length compatibility (placeholder). + + This method would check if the configured vector length matches the database schema. + Currently implemented as a placeholder. + """ + pass + + def _vector_to_blob(self, vector: List[float]) -> bytes: + """ + Convert a vector to Oracle BLOB format. + + Args: + vector (List[float]): The vector to convert + + Returns: + bytes: The vector in Oracle BLOB format + """ + return array.array('f', vector) + + def adjust_vector_length(self, vector: List[float]) -> List[float]: + """ + Adjust vector to the expected length if needed. + + Args: + vector (List[float]): The vector to adjust + + Returns: + List[float]: The adjusted vector + """ + return vector + + def _decimal_handler(self, obj): + """ + Handle Decimal objects for JSON serialization. + + Args: + obj: Object to serialize + + Returns: + float: Converted decimal value + + Raises: + TypeError: If object is not JSON serializable + """ + if isinstance(obj, Decimal): + return float(obj) + raise TypeError(f'{obj} is not JSON serializable') + + def _metadata_to_json(self, metadata: Dict) -> str: + """ + Convert metadata dictionary to JSON string. + + Args: + metadata (Dict): Metadata dictionary + + Returns: + str: JSON representation of metadata + """ + return json.dumps(metadata, default=self._decimal_handler) if metadata else '{}' + + def _json_to_metadata(self, json_str: str) -> Dict: + """ + Convert JSON string to metadata dictionary. + + Args: + json_str (str): JSON string + + Returns: + Dict: Metadata dictionary + """ + return json.loads(json_str) if json_str else {} + + def insert(self, collection_name: str, items: List[VectorItem]) -> None: + """ + Insert vector items into the database. + + Args: + collection_name (str): Name of the collection + items (List[VectorItem]): List of vector items to insert + + Raises: + Exception: If insertion fails + + Example: + >>> client = Oracle23aiClient() + >>> items = [ + ... {"id": "1", "text": "Sample text", "vector": [0.1, 0.2, ...], "metadata": {"source": "doc1"}}, + ... {"id": "2", "text": "Another text", "vector": [0.3, 0.4, ...], "metadata": {"source": "doc2"}} + ... ] + >>> client.insert("my_collection", items) + """ + log.info(f"Inserting {len(items)} items into collection '{collection_name}'.") + + with self.get_connection() as connection: + try: + with connection.cursor() as cursor: + for item in items: + vector_blob = self._vector_to_blob(item['vector']) + metadata_json = self._metadata_to_json(item['metadata']) + + cursor.execute( + """ + INSERT INTO document_chunk + (id, collection_name, text, vmetadata, vector) + VALUES (:id, :collection_name, :text, :metadata, :vector) + """, + { + 'id': item['id'], + 'collection_name': collection_name, + 'text': item['text'], + 'metadata': metadata_json, + 'vector': vector_blob, + }, + ) + + connection.commit() + log.info(f"Successfully inserted {len(items)} items into collection '{collection_name}'.") + + except Exception as e: + connection.rollback() + log.exception(f'Error during insert: {e}') + raise + + def upsert(self, collection_name: str, items: List[VectorItem]) -> None: + """ + Update or insert vector items into the database. + + If an item with the same ID exists, it will be updated; + otherwise, it will be inserted. + + Args: + collection_name (str): Name of the collection + items (List[VectorItem]): List of vector items to upsert + + Raises: + Exception: If upsert operation fails + + Example: + >>> client = Oracle23aiClient() + >>> items = [ + ... {"id": "1", "text": "Updated text", "vector": [0.1, 0.2, ...], "metadata": {"source": "doc1"}}, + ... {"id": "3", "text": "New item", "vector": [0.5, 0.6, ...], "metadata": {"source": "doc3"}} + ... ] + >>> client.upsert("my_collection", items) + """ + log.info(f"Upserting {len(items)} items into collection '{collection_name}'.") + + with self.get_connection() as connection: + try: + with connection.cursor() as cursor: + for item in items: + vector_blob = self._vector_to_blob(item['vector']) + metadata_json = self._metadata_to_json(item['metadata']) + + cursor.execute( + """ + MERGE INTO document_chunk d + USING (SELECT :merge_id as id FROM dual) s + ON (d.id = s.id) + WHEN MATCHED THEN + UPDATE SET + collection_name = :upd_collection_name, + text = :upd_text, + vmetadata = :upd_metadata, + vector = :upd_vector + WHEN NOT MATCHED THEN + INSERT (id, collection_name, text, vmetadata, vector) + VALUES (:ins_id, :ins_collection_name, :ins_text, :ins_metadata, :ins_vector) + """, + { + 'merge_id': item['id'], + 'upd_collection_name': collection_name, + 'upd_text': item['text'], + 'upd_metadata': metadata_json, + 'upd_vector': vector_blob, + 'ins_id': item['id'], + 'ins_collection_name': collection_name, + 'ins_text': item['text'], + 'ins_metadata': metadata_json, + 'ins_vector': vector_blob, + }, + ) + + connection.commit() + log.info(f"Successfully upserted {len(items)} items into collection '{collection_name}'.") + + except Exception as e: + connection.rollback() + log.exception(f'Error during upsert: {e}') + raise + + def search( + self, + collection_name: str, + vectors: List[List[Union[float, int]]], + filter: Optional[dict] = None, + limit: int = 10, + ) -> Optional[SearchResult]: + """ + Search for similar vectors in the database. + + Performs vector similarity search using cosine distance. + + Args: + collection_name (str): Name of the collection to search + vectors (List[List[Union[float, int]]]): Query vectors to find similar items for + limit (int): Maximum number of results to return per query + + Returns: + Optional[SearchResult]: Search results containing ids, distances, documents, and metadata + + Example: + >>> client = Oracle23aiClient() + >>> query_vector = [0.1, 0.2, 0.3, ...] # Must match VECTOR_LENGTH + >>> results = client.search("my_collection", [query_vector], limit=5) + >>> if results: + ... log.info(f"Found {len(results.ids[0])} matches") + ... for i, (id, dist) in enumerate(zip(results.ids[0], results.distances[0])): + ... log.info(f"Match {i+1}: id={id}, distance={dist}") + """ + log.info(f"Searching items from collection '{collection_name}' with limit {limit}.") + + try: + if not vectors: + log.warning('No vectors provided for search.') + return None + + num_queries = len(vectors) + + ids = [[] for _ in range(num_queries)] + distances = [[] for _ in range(num_queries)] + documents = [[] for _ in range(num_queries)] + metadatas = [[] for _ in range(num_queries)] + + with self.get_connection() as connection: + with connection.cursor() as cursor: + for qid, vector in enumerate(vectors): + vector_blob = self._vector_to_blob(vector) + + cursor.execute( + """ + SELECT dc.id, dc.text, + JSON_SERIALIZE(dc.vmetadata RETURNING VARCHAR2(4096)) as vmetadata, + VECTOR_DISTANCE(dc.vector, :query_vector, COSINE) as distance + FROM document_chunk dc + WHERE dc.collection_name = :collection_name + ORDER BY VECTOR_DISTANCE(dc.vector, :query_vector, COSINE) + FETCH APPROX FIRST :limit ROWS ONLY + """, + { + 'query_vector': vector_blob, + 'collection_name': collection_name, + 'limit': limit, + }, + ) + + results = cursor.fetchall() + + for row in results: + ids[qid].append(row[0]) + documents[qid].append(row[1].read() if isinstance(row[1], oracledb.LOB) else str(row[1])) + # 🔧 FIXED: Parse JSON metadata properly + metadata_str = row[2].read() if isinstance(row[2], oracledb.LOB) else row[2] + metadatas[qid].append(self._json_to_metadata(metadata_str)) + distances[qid].append(float(row[3])) + + log.info(f'Search completed. Found {sum(len(ids[i]) for i in range(num_queries))} total results.') + + return SearchResult(ids=ids, distances=distances, documents=documents, metadatas=metadatas) + + except Exception as e: + log.exception(f'Error during search: {e}') + return None + + def query(self, collection_name: str, filter: Dict, limit: Optional[int] = None) -> Optional[GetResult]: + """ + Query items based on metadata filters. + + Retrieves items that match specified metadata criteria. + + Args: + collection_name (str): Name of the collection to query + filter (Dict[str, Any]): Metadata filters to apply + limit (Optional[int]): Maximum number of results to return + + Returns: + Optional[GetResult]: Query results containing ids, documents, and metadata + + Example: + >>> client = Oracle23aiClient() + >>> filter = {"source": "doc1", "category": "finance"} + >>> results = client.query("my_collection", filter, limit=20) + >>> if results: + ... print(f"Found {len(results.ids[0])} matching documents") + """ + log.info(f"Querying items from collection '{collection_name}' with filters.") + + try: + limit = limit or 100 + + query = """ + SELECT id, text, JSON_SERIALIZE(vmetadata RETURNING VARCHAR2(4096)) as vmetadata + FROM document_chunk + WHERE collection_name = :collection_name + """ + + params = {'collection_name': collection_name} + + for i, (key, value) in enumerate(filter.items()): + param_name = f'value_{i}' + query += f" AND JSON_VALUE(vmetadata, '$.{key}' RETURNING VARCHAR2(4096)) = :{param_name}" + params[param_name] = str(value) + + query += ' FETCH FIRST :limit ROWS ONLY' + params['limit'] = limit + + with self.get_connection() as connection: + with connection.cursor() as cursor: + cursor.execute(query, params) + results = cursor.fetchall() + + if not results: + log.info('No results found for query.') + return None + + ids = [[row[0] for row in results]] + documents = [[row[1].read() if isinstance(row[1], oracledb.LOB) else str(row[1]) for row in results]] + # 🔧 FIXED: Parse JSON metadata properly + metadatas = [ + [ + self._json_to_metadata(row[2].read() if isinstance(row[2], oracledb.LOB) else row[2]) + for row in results + ] + ] + + log.info(f'Query completed. Found {len(results)} results.') + + return GetResult(ids=ids, documents=documents, metadatas=metadatas) + + except Exception as e: + log.exception(f'Error during query: {e}') + return None + + def get(self, collection_name: str) -> Optional[GetResult]: + """ + Get all items in a collection. + + Retrieves items from a specified collection up to the limit. + + Args: + collection_name (str): Name of the collection to retrieve + limit (Optional[int]): Maximum number of items to retrieve + + Returns: + Optional[GetResult]: Result containing ids, documents, and metadata + + Example: + >>> client = Oracle23aiClient() + >>> results = client.get("my_collection", limit=50) + >>> if results: + ... print(f"Retrieved {len(results.ids[0])} documents from collection") + """ + + try: + limit = 1000 # Hardcoded limit for get operation + + with self.get_connection() as connection: + with connection.cursor() as cursor: + cursor.execute( + """ + SELECT /*+ MONITOR */ id, text, JSON_SERIALIZE(vmetadata RETURNING VARCHAR2(4096)) as vmetadata + FROM document_chunk + WHERE collection_name = :collection_name + FETCH FIRST :limit ROWS ONLY + """, + {'collection_name': collection_name, 'limit': limit}, + ) + + results = cursor.fetchall() + + if not results: + log.info('No results found.') + return None + + ids = [[row[0] for row in results]] + documents = [[row[1].read() if isinstance(row[1], oracledb.LOB) else str(row[1]) for row in results]] + # 🔧 FIXED: Parse JSON metadata properly + metadatas = [ + [ + self._json_to_metadata(row[2].read() if isinstance(row[2], oracledb.LOB) else row[2]) + for row in results + ] + ] + + return GetResult(ids=ids, documents=documents, metadatas=metadatas) + + except Exception as e: + log.exception(f'Error during get: {e}') + return None + + def delete( + self, + collection_name: str, + ids: Optional[List[str]] = None, + filter: Optional[Dict[str, Any]] = None, + ) -> None: + """ + Delete items from the database. + + Deletes items from a collection based on IDs or metadata filters. + + Args: + collection_name (str): Name of the collection to delete from + ids (Optional[List[str]]): Specific item IDs to delete + filter (Optional[Dict[str, Any]]): Metadata filters for deletion + + Raises: + Exception: If deletion fails + + Example: + >>> client = Oracle23aiClient() + >>> # Delete specific items by ID + >>> client.delete("my_collection", ids=["1", "3", "5"]) + >>> # Or delete by metadata filter + >>> client.delete("my_collection", filter={"source": "deprecated_source"}) + """ + log.info(f"Deleting items from collection '{collection_name}'.") + + try: + query = 'DELETE FROM document_chunk WHERE collection_name = :collection_name' + params = {'collection_name': collection_name} + + if ids: + # 🔧 FIXED: Use proper parameterized query to prevent SQL injection + placeholders = ','.join([f':id_{i}' for i in range(len(ids))]) + query += f' AND id IN ({placeholders})' + for i, id_val in enumerate(ids): + params[f'id_{i}'] = id_val + + if filter: + for i, (key, value) in enumerate(filter.items()): + param_name = f'value_{i}' + query += f" AND JSON_VALUE(vmetadata, '$.{key}' RETURNING VARCHAR2(4096)) = :{param_name}" + params[param_name] = str(value) + + with self.get_connection() as connection: + with connection.cursor() as cursor: + cursor.execute(query, params) + deleted = cursor.rowcount + connection.commit() + + log.info(f"Deleted {deleted} items from collection '{collection_name}'.") + + except Exception as e: + log.exception(f'Error during delete: {e}') + raise + + def reset(self) -> None: + """ + Reset the database by deleting all items. + + Deletes all items from the document_chunk table. + + Raises: + Exception: If reset fails + + Example: + >>> client = Oracle23aiClient() + >>> client.reset() # Warning: Removes all data! + """ + log.info('Resetting database - deleting all items.') + + try: + with self.get_connection() as connection: + with connection.cursor() as cursor: + cursor.execute('DELETE FROM document_chunk') + deleted = cursor.rowcount + connection.commit() + + log.info(f"Reset complete. Deleted {deleted} items from 'document_chunk' table.") + + except Exception as e: + log.exception(f'Error during reset: {e}') + raise + + def close(self) -> None: + """ + Close the database connection pool. + + Properly closes the connection pool and releases all resources. + + Example: + >>> client = Oracle23aiClient() + >>> # After finishing all operations + >>> client.close() + """ + try: + if hasattr(self, 'pool') and self.pool: + self.pool.close() + log.info('Oracle Vector Search connection pool closed.') + except Exception as e: + log.exception(f'Error closing connection pool: {e}') + + def has_collection(self, collection_name: str) -> bool: + """ + Check if a collection exists. + + Args: + collection_name (str): Name of the collection to check + + Returns: + bool: True if the collection exists, False otherwise + + Example: + >>> client = Oracle23aiClient() + >>> if client.has_collection("my_collection"): + ... print("Collection exists!") + ... else: + ... print("Collection does not exist.") + """ + try: + with self.get_connection() as connection: + with connection.cursor() as cursor: + cursor.execute( + """ + SELECT COUNT(*) + FROM document_chunk + WHERE collection_name = :collection_name + FETCH FIRST 1 ROWS ONLY + """, + {'collection_name': collection_name}, + ) + + count = cursor.fetchone()[0] + + return count > 0 + + except Exception as e: + log.exception(f'Error checking collection existence: {e}') + return False + + def delete_collection(self, collection_name: str) -> None: + """ + Delete an entire collection. + + Removes all items belonging to the specified collection. + + Args: + collection_name (str): Name of the collection to delete + + Example: + >>> client = Oracle23aiClient() + >>> client.delete_collection("obsolete_collection") + """ + log.info(f"Deleting collection '{collection_name}'.") + + try: + with self.get_connection() as connection: + with connection.cursor() as cursor: + cursor.execute( + """ + DELETE FROM document_chunk + WHERE collection_name = :collection_name + """, + {'collection_name': collection_name}, + ) + + deleted = cursor.rowcount + connection.commit() + + log.info(f"Collection '{collection_name}' deleted. Removed {deleted} items.") + + except Exception as e: + log.exception(f"Error deleting collection '{collection_name}': {e}") + raise diff --git a/backend/open_webui/retrieval/vector/dbs/pgvector.py b/backend/open_webui/retrieval/vector/dbs/pgvector.py new file mode 100644 index 0000000000000000000000000000000000000000..90e65b9ad07c3cefea096108525b6a7f7f6f0b9b --- /dev/null +++ b/backend/open_webui/retrieval/vector/dbs/pgvector.py @@ -0,0 +1,672 @@ +from typing import Optional, List, Dict, Any, Tuple +import logging +import json +from sqlalchemy import ( + func, + literal, + cast, + column, + create_engine, + Column, + Integer, + MetaData, + LargeBinary, + select, + text, + Text, + Table, + values, +) +from sqlalchemy.sql import true +from sqlalchemy.pool import NullPool, QueuePool + +from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker +from sqlalchemy.dialects.postgresql import JSONB, array +from pgvector.sqlalchemy import Vector, HALFVEC +from sqlalchemy.ext.mutable import MutableDict +from sqlalchemy.exc import NoSuchTableError + + +from open_webui.retrieval.vector.utils import process_metadata +from open_webui.retrieval.vector.main import ( + VectorDBBase, + VectorItem, + SearchResult, + GetResult, +) +from open_webui.utils.misc import sanitize_text_for_db +from open_webui.config import ( + PGVECTOR_DB_URL, + PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH, + PGVECTOR_CREATE_EXTENSION, + PGVECTOR_PGCRYPTO, + PGVECTOR_PGCRYPTO_KEY, + PGVECTOR_POOL_SIZE, + PGVECTOR_POOL_MAX_OVERFLOW, + PGVECTOR_POOL_TIMEOUT, + PGVECTOR_POOL_RECYCLE, + PGVECTOR_INDEX_METHOD, + PGVECTOR_HNSW_M, + PGVECTOR_HNSW_EF_CONSTRUCTION, + PGVECTOR_IVFFLAT_LISTS, + PGVECTOR_USE_HALFVEC, +) + +VECTOR_LENGTH = PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH +USE_HALFVEC = PGVECTOR_USE_HALFVEC + +VECTOR_TYPE_FACTORY = HALFVEC if USE_HALFVEC else Vector +VECTOR_OPCLASS = 'halfvec_cosine_ops' if USE_HALFVEC else 'vector_cosine_ops' +Base = declarative_base() + +log = logging.getLogger(__name__) + + +def pgcrypto_encrypt(val, key): + return func.pgp_sym_encrypt(val, literal(key)) + + +def pgcrypto_decrypt(col, key, outtype='text'): + return func.cast(func.pgp_sym_decrypt(col, literal(key)), outtype) + + +class DocumentChunk(Base): + __tablename__ = 'document_chunk' + + id = Column(Text, primary_key=True) + vector = Column(VECTOR_TYPE_FACTORY(dim=VECTOR_LENGTH), nullable=True) + collection_name = Column(Text, nullable=False) + + if PGVECTOR_PGCRYPTO: + text = Column(LargeBinary, nullable=True) + vmetadata = Column(LargeBinary, nullable=True) + else: + text = Column(Text, nullable=True) + vmetadata = Column(MutableDict.as_mutable(JSONB), nullable=True) + + +class PgvectorClient(VectorDBBase): + def __init__(self) -> None: + # if no pgvector uri, use the existing database connection + if not PGVECTOR_DB_URL: + from open_webui.internal.db import ScopedSession + + self.session = ScopedSession + else: + if isinstance(PGVECTOR_POOL_SIZE, int): + if PGVECTOR_POOL_SIZE > 0: + engine = create_engine( + PGVECTOR_DB_URL, + pool_size=PGVECTOR_POOL_SIZE, + max_overflow=PGVECTOR_POOL_MAX_OVERFLOW, + pool_timeout=PGVECTOR_POOL_TIMEOUT, + pool_recycle=PGVECTOR_POOL_RECYCLE, + pool_pre_ping=True, + poolclass=QueuePool, + ) + else: + engine = create_engine(PGVECTOR_DB_URL, pool_pre_ping=True, poolclass=NullPool) + else: + engine = create_engine(PGVECTOR_DB_URL, pool_pre_ping=True) + + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False) + self.session = scoped_session(SessionLocal) + + try: + # Ensure the pgvector extension is available + # Use a conditional check to avoid permission issues on Azure PostgreSQL + if PGVECTOR_CREATE_EXTENSION: + self.session.execute( + text(""" + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'vector') THEN + CREATE EXTENSION IF NOT EXISTS vector; + END IF; + END $$; + """) + ) + + if PGVECTOR_PGCRYPTO: + # Ensure the pgcrypto extension is available for encryption + # Use a conditional check to avoid permission issues on Azure PostgreSQL + self.session.execute( + text(""" + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pgcrypto') THEN + CREATE EXTENSION IF NOT EXISTS pgcrypto; + END IF; + END $$; + """) + ) + + if not PGVECTOR_PGCRYPTO_KEY: + raise ValueError('PGVECTOR_PGCRYPTO_KEY must be set when PGVECTOR_PGCRYPTO is enabled.') + + # Check vector length consistency + self.check_vector_length() + + # Create the tables if they do not exist + # Base.metadata.create_all requires a bind (engine or connection) + # Get the connection from the session + connection = self.session.connection() + Base.metadata.create_all(bind=connection) + + index_method, index_options = self._vector_index_configuration() + self._ensure_vector_index(index_method, index_options) + + self.session.execute( + text( + 'CREATE INDEX IF NOT EXISTS idx_document_chunk_collection_name ON document_chunk (collection_name);' + ) + ) + self.session.commit() + log.info('Initialization complete.') + except Exception as e: + self.session.rollback() + log.exception(f'Error during initialization: {e}') + raise + + @staticmethod + def _extract_index_method(index_def: Optional[str]) -> Optional[str]: + if not index_def: + return None + try: + after_using = index_def.lower().split('using ', 1)[1] + return after_using.split()[0] + except (IndexError, AttributeError): + return None + + def _vector_index_configuration(self) -> Tuple[str, str]: + if PGVECTOR_INDEX_METHOD: + index_method = PGVECTOR_INDEX_METHOD + log.info( + "Using vector index method '%s' from PGVECTOR_INDEX_METHOD.", + index_method, + ) + elif USE_HALFVEC: + index_method = 'hnsw' + log.info( + 'VECTOR_LENGTH=%s exceeds 2000; using halfvec column type with hnsw index.', + VECTOR_LENGTH, + ) + else: + index_method = 'ivfflat' + + if index_method == 'hnsw': + index_options = f'WITH (m = {PGVECTOR_HNSW_M}, ef_construction = {PGVECTOR_HNSW_EF_CONSTRUCTION})' + else: + index_options = f'WITH (lists = {PGVECTOR_IVFFLAT_LISTS})' + + return index_method, index_options + + def _ensure_vector_index(self, index_method: str, index_options: str) -> None: + index_name = 'idx_document_chunk_vector' + existing_index_def = self.session.execute( + text(""" + SELECT indexdef + FROM pg_indexes + WHERE schemaname = current_schema() + AND tablename = 'document_chunk' + AND indexname = :index_name + """), + {'index_name': index_name}, + ).scalar() + + existing_method = self._extract_index_method(existing_index_def) + if existing_method and existing_method != index_method: + raise RuntimeError( + f"Existing pgvector index '{index_name}' uses method '{existing_method}' but configuration now " + f"requires '{index_method}'. Automatic rebuild is disabled to prevent long-running maintenance. " + 'Drop the index manually (optionally after tuning maintenance_work_mem/max_parallel_maintenance_workers) ' + 'and recreate it with the new method before restarting Open WebUI.' + ) + + if not existing_index_def: + index_sql = ( + f'CREATE INDEX IF NOT EXISTS {index_name} ' + f'ON document_chunk USING {index_method} (vector {VECTOR_OPCLASS})' + ) + if index_options: + index_sql = f'{index_sql} {index_options}' + self.session.execute(text(index_sql)) + log.info( + "Ensured vector index '%s' using %s%s.", + index_name, + index_method, + f' {index_options}' if index_options else '', + ) + + def check_vector_length(self) -> None: + """ + Check if the VECTOR_LENGTH matches the existing vector column dimension in the database. + Raises an exception if there is a mismatch. + """ + metadata = MetaData() + try: + # Attempt to reflect the 'document_chunk' table + document_chunk_table = Table('document_chunk', metadata, autoload_with=self.session.bind) + except NoSuchTableError: + # Table does not exist; no action needed + return + + # Proceed to check the vector column + if 'vector' in document_chunk_table.columns: + vector_column = document_chunk_table.columns['vector'] + vector_type = vector_column.type + expected_type = HALFVEC if USE_HALFVEC else Vector + + if not isinstance(vector_type, expected_type): + raise Exception( + "The 'vector' column type does not match the expected type " + f"('{expected_type.__name__}') for VECTOR_LENGTH {VECTOR_LENGTH}." + ) + + db_vector_length = getattr(vector_type, 'dim', None) + if db_vector_length is not None and db_vector_length != VECTOR_LENGTH: + raise Exception( + f'VECTOR_LENGTH {VECTOR_LENGTH} does not match existing vector column dimension {db_vector_length}. ' + 'Cannot change vector size after initialization without migrating the data.' + ) + else: + raise Exception("The 'vector' column does not exist in the 'document_chunk' table.") + + def adjust_vector_length(self, vector: List[float]) -> List[float]: + # Adjust vector to have length VECTOR_LENGTH + current_length = len(vector) + if current_length < VECTOR_LENGTH: + # Pad the vector with zeros + vector += [0.0] * (VECTOR_LENGTH - current_length) + elif current_length > VECTOR_LENGTH: + # Truncate the vector to VECTOR_LENGTH + vector = vector[:VECTOR_LENGTH] + return vector + + def insert(self, collection_name: str, items: List[VectorItem]) -> None: + try: + if PGVECTOR_PGCRYPTO: + for item in items: + vector = self.adjust_vector_length(item['vector']) + # Use raw SQL for BYTEA/pgcrypto + # Ensure metadata is converted to its JSON text representation + # Sanitize to strip null bytes / surrogates that PostgreSQL cannot store + json_metadata = sanitize_text_for_db(json.dumps(item['metadata'])) + item_text = sanitize_text_for_db(item['text']) + self.session.execute( + text(""" + INSERT INTO document_chunk + (id, vector, collection_name, text, vmetadata) + VALUES ( + :id, :vector, :collection_name, + pgp_sym_encrypt(:text, :key), + pgp_sym_encrypt(:metadata_text, :key) + ) + ON CONFLICT (id) DO NOTHING + """), + { + 'id': item['id'], + 'vector': vector, + 'collection_name': collection_name, + 'text': item_text, + 'metadata_text': json_metadata, + 'key': PGVECTOR_PGCRYPTO_KEY, + }, + ) + self.session.commit() + log.info(f"Encrypted & inserted {len(items)} into '{collection_name}'") + + else: + new_items = [] + for item in items: + vector = self.adjust_vector_length(item['vector']) + new_chunk = DocumentChunk( + id=item['id'], + vector=vector, + collection_name=collection_name, + text=item['text'], + vmetadata=process_metadata(item['metadata']), + ) + new_items.append(new_chunk) + self.session.bulk_save_objects(new_items) + self.session.commit() + log.info(f"Inserted {len(new_items)} items into collection '{collection_name}'.") + except Exception as e: + self.session.rollback() + log.exception(f'Error during insert: {e}') + raise + + def upsert(self, collection_name: str, items: List[VectorItem]) -> None: + try: + if PGVECTOR_PGCRYPTO: + for item in items: + vector = self.adjust_vector_length(item['vector']) + # Sanitize to strip null bytes / surrogates that PostgreSQL cannot store + json_metadata = sanitize_text_for_db(json.dumps(item['metadata'])) + item_text = sanitize_text_for_db(item['text']) + self.session.execute( + text(""" + INSERT INTO document_chunk + (id, vector, collection_name, text, vmetadata) + VALUES ( + :id, :vector, :collection_name, + pgp_sym_encrypt(:text, :key), + pgp_sym_encrypt(:metadata_text, :key) + ) + ON CONFLICT (id) DO UPDATE SET + vector = EXCLUDED.vector, + collection_name = EXCLUDED.collection_name, + text = EXCLUDED.text, + vmetadata = EXCLUDED.vmetadata + """), + { + 'id': item['id'], + 'vector': vector, + 'collection_name': collection_name, + 'text': item_text, + 'metadata_text': json_metadata, + 'key': PGVECTOR_PGCRYPTO_KEY, + }, + ) + self.session.commit() + log.info(f"Encrypted & upserted {len(items)} into '{collection_name}'") + else: + for item in items: + vector = self.adjust_vector_length(item['vector']) + existing = self.session.query(DocumentChunk).filter(DocumentChunk.id == item['id']).first() + if existing: + existing.vector = vector + existing.text = item['text'] + existing.vmetadata = process_metadata(item['metadata']) + existing.collection_name = collection_name # Update collection_name if necessary + else: + new_chunk = DocumentChunk( + id=item['id'], + vector=vector, + collection_name=collection_name, + text=item['text'], + vmetadata=process_metadata(item['metadata']), + ) + self.session.add(new_chunk) + self.session.commit() + log.info(f"Upserted {len(items)} items into collection '{collection_name}'.") + except Exception as e: + self.session.rollback() + log.exception(f'Error during upsert: {e}') + raise + + def search( + self, + collection_name: str, + vectors: List[List[float]], + filter: Optional[Dict[str, Any]] = None, + limit: int = 10, + ) -> Optional[SearchResult]: + try: + if not vectors: + return None + + # Adjust query vectors to VECTOR_LENGTH + vectors = [self.adjust_vector_length(vector) for vector in vectors] + num_queries = len(vectors) + + def vector_expr(vector): + return cast(array(vector), VECTOR_TYPE_FACTORY(VECTOR_LENGTH)) + + # Create the values for query vectors + qid_col = column('qid', Integer) + q_vector_col = column('q_vector', VECTOR_TYPE_FACTORY(VECTOR_LENGTH)) + query_vectors = ( + values(qid_col, q_vector_col) + .data([(idx, vector_expr(vector)) for idx, vector in enumerate(vectors)]) + .alias('query_vectors') + ) + + result_fields = [ + DocumentChunk.id, + ] + if PGVECTOR_PGCRYPTO: + result_fields.append(pgcrypto_decrypt(DocumentChunk.text, PGVECTOR_PGCRYPTO_KEY, Text).label('text')) + result_fields.append( + pgcrypto_decrypt(DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB).label('vmetadata') + ) + else: + result_fields.append(DocumentChunk.text) + result_fields.append(DocumentChunk.vmetadata) + result_fields.append((DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector)).label('distance')) + + # Build the lateral subquery for each query vector + where_clauses = [DocumentChunk.collection_name == collection_name] + + # Apply metadata filter if provided + if filter: + for key, value in filter.items(): + if isinstance(value, dict) and '$in' in value: + # Handle $in operator: {"field": {"$in": [values]}} + in_values = value['$in'] + if PGVECTOR_PGCRYPTO: + where_clauses.append( + pgcrypto_decrypt( + DocumentChunk.vmetadata, + PGVECTOR_PGCRYPTO_KEY, + JSONB, + )[key].astext.in_([str(v) for v in in_values]) + ) + else: + where_clauses.append(DocumentChunk.vmetadata[key].astext.in_([str(v) for v in in_values])) + else: + # Handle simple equality: {"field": "value"} + if PGVECTOR_PGCRYPTO: + where_clauses.append( + pgcrypto_decrypt( + DocumentChunk.vmetadata, + PGVECTOR_PGCRYPTO_KEY, + JSONB, + )[key].astext + == str(value) + ) + else: + where_clauses.append(DocumentChunk.vmetadata[key].astext == str(value)) + + subq = ( + select(*result_fields) + .where(*where_clauses) + .order_by((DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector))) + ) + if limit is not None: + subq = subq.limit(limit) + subq = subq.lateral('result') + + # Build the main query by joining query_vectors and the lateral subquery + stmt = ( + select( + query_vectors.c.qid, + subq.c.id, + subq.c.text, + subq.c.vmetadata, + subq.c.distance, + ) + .select_from(query_vectors) + .join(subq, true()) + .order_by(query_vectors.c.qid, subq.c.distance) + ) + + result_proxy = self.session.execute(stmt) + results = result_proxy.all() + + ids = [[] for _ in range(num_queries)] + distances = [[] for _ in range(num_queries)] + documents = [[] for _ in range(num_queries)] + metadatas = [[] for _ in range(num_queries)] + + if not results: + return SearchResult( + ids=ids, + distances=distances, + documents=documents, + metadatas=metadatas, + ) + + for row in results: + qid = int(row.qid) + ids[qid].append(row.id) + # normalize and re-orders pgvec distance from [2, 0] to [0, 1] score range + # https://github.com/pgvector/pgvector?tab=readme-ov-file#querying + distances[qid].append((2.0 - row.distance) / 2.0) + documents[qid].append(row.text) + metadatas[qid].append(row.vmetadata) + + self.session.rollback() # read-only transaction + return SearchResult(ids=ids, distances=distances, documents=documents, metadatas=metadatas) + except Exception as e: + self.session.rollback() + log.exception(f'Error during search: {e}') + return None + + def query(self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None) -> Optional[GetResult]: + try: + if PGVECTOR_PGCRYPTO: + # Build where clause for vmetadata filter + where_clauses = [DocumentChunk.collection_name == collection_name] + for key, value in filter.items(): + # decrypt then check key: JSON filter after decryption + where_clauses.append( + pgcrypto_decrypt(DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB)[key].astext + == str(value) + ) + stmt = select( + DocumentChunk.id, + pgcrypto_decrypt(DocumentChunk.text, PGVECTOR_PGCRYPTO_KEY, Text).label('text'), + pgcrypto_decrypt(DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB).label('vmetadata'), + ).where(*where_clauses) + if limit is not None: + stmt = stmt.limit(limit) + results = self.session.execute(stmt).all() + else: + query = self.session.query(DocumentChunk).filter(DocumentChunk.collection_name == collection_name) + + for key, value in filter.items(): + query = query.filter(DocumentChunk.vmetadata[key].astext == str(value)) + + if limit is not None: + query = query.limit(limit) + + results = query.all() + + if not results: + return None + + ids = [[result.id for result in results]] + documents = [[result.text for result in results]] + metadatas = [[result.vmetadata for result in results]] + + self.session.rollback() # read-only transaction + return GetResult( + ids=ids, + documents=documents, + metadatas=metadatas, + ) + except Exception as e: + self.session.rollback() + log.exception(f'Error during query: {e}') + return None + + def get(self, collection_name: str, limit: Optional[int] = None) -> Optional[GetResult]: + try: + if PGVECTOR_PGCRYPTO: + stmt = select( + DocumentChunk.id, + pgcrypto_decrypt(DocumentChunk.text, PGVECTOR_PGCRYPTO_KEY, Text).label('text'), + pgcrypto_decrypt(DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB).label('vmetadata'), + ).where(DocumentChunk.collection_name == collection_name) + if limit is not None: + stmt = stmt.limit(limit) + results = self.session.execute(stmt).all() + ids = [[row.id for row in results]] + documents = [[row.text for row in results]] + metadatas = [[row.vmetadata for row in results]] + else: + query = self.session.query(DocumentChunk).filter(DocumentChunk.collection_name == collection_name) + if limit is not None: + query = query.limit(limit) + + results = query.all() + + if not results: + return None + + ids = [[result.id for result in results]] + documents = [[result.text for result in results]] + metadatas = [[result.vmetadata for result in results]] + + self.session.rollback() # read-only transaction + return GetResult(ids=ids, documents=documents, metadatas=metadatas) + except Exception as e: + self.session.rollback() + log.exception(f'Error during get: {e}') + return None + + def delete( + self, + collection_name: str, + ids: Optional[List[str]] = None, + filter: Optional[Dict[str, Any]] = None, + ) -> None: + try: + if PGVECTOR_PGCRYPTO: + wheres = [DocumentChunk.collection_name == collection_name] + if ids: + wheres.append(DocumentChunk.id.in_(ids)) + if filter: + for key, value in filter.items(): + wheres.append( + pgcrypto_decrypt(DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB)[key].astext + == str(value) + ) + stmt = DocumentChunk.__table__.delete().where(*wheres) + result = self.session.execute(stmt) + deleted = result.rowcount + else: + query = self.session.query(DocumentChunk).filter(DocumentChunk.collection_name == collection_name) + if ids: + query = query.filter(DocumentChunk.id.in_(ids)) + if filter: + for key, value in filter.items(): + query = query.filter(DocumentChunk.vmetadata[key].astext == str(value)) + deleted = query.delete(synchronize_session=False) + self.session.commit() + log.info(f"Deleted {deleted} items from collection '{collection_name}'.") + except Exception as e: + self.session.rollback() + log.exception(f'Error during delete: {e}') + raise + + def reset(self) -> None: + try: + deleted = self.session.query(DocumentChunk).delete() + self.session.commit() + log.info(f"Reset complete. Deleted {deleted} items from 'document_chunk' table.") + except Exception as e: + self.session.rollback() + log.exception(f'Error during reset: {e}') + raise + + def close(self) -> None: + pass + + def has_collection(self, collection_name: str) -> bool: + try: + exists = ( + self.session.query(DocumentChunk).filter(DocumentChunk.collection_name == collection_name).first() + is not None + ) + self.session.rollback() # read-only transaction + return exists + except Exception as e: + self.session.rollback() + log.exception(f'Error checking collection existence: {e}') + return False + + def delete_collection(self, collection_name: str) -> None: + self.delete(collection_name) + log.info(f"Collection '{collection_name}' deleted.") diff --git a/backend/open_webui/retrieval/vector/dbs/pinecone.py b/backend/open_webui/retrieval/vector/dbs/pinecone.py new file mode 100644 index 0000000000000000000000000000000000000000..6469ac917210b0b8e3d80309665bd35f9d61b80a --- /dev/null +++ b/backend/open_webui/retrieval/vector/dbs/pinecone.py @@ -0,0 +1,520 @@ +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + +from typing import Optional, List, Dict, Any, Union +import logging +import time # for measuring elapsed time +from pinecone import Pinecone, ServerlessSpec + +# Add gRPC support for better performance (Pinecone best practice) +try: + from pinecone.grpc import PineconeGRPC + + GRPC_AVAILABLE = True +except ImportError: + GRPC_AVAILABLE = False + +import asyncio # for async upserts +import functools # for partial binding in async tasks + +import concurrent.futures # for parallel batch upserts +import random # for jitter in retry backoff + +from open_webui.retrieval.vector.main import ( + VectorDBBase, + VectorItem, + SearchResult, + GetResult, +) +from open_webui.config import ( + PINECONE_API_KEY, + PINECONE_ENVIRONMENT, + PINECONE_INDEX_NAME, + PINECONE_DIMENSION, + PINECONE_METRIC, + PINECONE_CLOUD, +) +from open_webui.retrieval.vector.utils import process_metadata + +NO_LIMIT = 10000 # Reasonable limit to avoid overwhelming the system +BATCH_SIZE = 100 # Recommended batch size for Pinecone operations + +log = logging.getLogger(__name__) + + +class PineconeClient(VectorDBBase): + def __init__(self): + self.collection_prefix = 'open-webui' + + # Validate required configuration + self._validate_config() + + # Store configuration values + self.api_key = PINECONE_API_KEY + self.environment = PINECONE_ENVIRONMENT + self.index_name = PINECONE_INDEX_NAME + self.dimension = PINECONE_DIMENSION + self.metric = PINECONE_METRIC + self.cloud = PINECONE_CLOUD + + # Initialize Pinecone client for improved performance + if GRPC_AVAILABLE: + # Use gRPC client for better performance (Pinecone recommendation) + self.client = PineconeGRPC( + api_key=self.api_key, + pool_threads=20, # Improved connection pool size + timeout=30, # Reasonable timeout for operations + ) + self.using_grpc = True + log.info('Using Pinecone gRPC client for optimal performance') + else: + # Fallback to HTTP client with enhanced connection pooling + self.client = Pinecone( + api_key=self.api_key, + pool_threads=20, # Improved connection pool size + timeout=30, # Reasonable timeout for operations + ) + self.using_grpc = False + log.info('Using Pinecone HTTP client (gRPC not available)') + + # Persistent executor for batch operations + self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=5) + + # Create index if it doesn't exist + self._initialize_index() + + def _validate_config(self) -> None: + """Validate that all required configuration variables are set.""" + missing_vars = [] + if not PINECONE_API_KEY: + missing_vars.append('PINECONE_API_KEY') + if not PINECONE_ENVIRONMENT: + missing_vars.append('PINECONE_ENVIRONMENT') + if not PINECONE_INDEX_NAME: + missing_vars.append('PINECONE_INDEX_NAME') + if not PINECONE_DIMENSION: + missing_vars.append('PINECONE_DIMENSION') + if not PINECONE_CLOUD: + missing_vars.append('PINECONE_CLOUD') + + if missing_vars: + raise ValueError(f'Required configuration missing: {", ".join(missing_vars)}') + + def _initialize_index(self) -> None: + """Initialize the Pinecone index.""" + try: + # Check if index exists + if self.index_name not in self.client.list_indexes().names(): + log.info(f"Creating Pinecone index '{self.index_name}'...") + self.client.create_index( + name=self.index_name, + dimension=self.dimension, + metric=self.metric, + spec=ServerlessSpec(cloud=self.cloud, region=self.environment), + ) + log.info(f"Successfully created Pinecone index '{self.index_name}'") + else: + log.info(f"Using existing Pinecone index '{self.index_name}'") + + # Connect to the index + self.index = self.client.Index( + self.index_name, + pool_threads=20, # Enhanced connection pool for index operations + ) + + except Exception as e: + log.error(f'Failed to initialize Pinecone index: {e}') + raise RuntimeError(f'Failed to initialize Pinecone index: {e}') + + def _retry_pinecone_operation(self, operation_func, max_retries=3): + """Retry Pinecone operations with exponential backoff for rate limits and network issues.""" + for attempt in range(max_retries): + try: + return operation_func() + except Exception as e: + error_str = str(e).lower() + # Check if it's a retryable error (rate limits, network issues, timeouts) + is_retryable = any( + keyword in error_str + for keyword in [ + 'rate limit', + 'quota', + 'timeout', + 'network', + 'connection', + 'unavailable', + 'internal error', + '429', + '500', + '502', + '503', + '504', + ] + ) + + if not is_retryable or attempt == max_retries - 1: + # Don't retry for non-retryable errors or on final attempt + raise + + # Exponential backoff with jitter + delay = (2**attempt) + random.uniform(0, 1) + log.warning( + f'Pinecone operation failed (attempt {attempt + 1}/{max_retries}), retrying in {delay:.2f}s: {e}' + ) + time.sleep(delay) + + def _create_points(self, items: List[VectorItem], collection_name_with_prefix: str) -> List[Dict[str, Any]]: + """Convert VectorItem objects to Pinecone point format.""" + points = [] + for item in items: + # Start with any existing metadata or an empty dict + metadata = item.get('metadata', {}).copy() if item.get('metadata') else {} + + # Add text to metadata if available + if 'text' in item: + metadata['text'] = item['text'] + + # Always add collection_name to metadata for filtering + metadata['collection_name'] = collection_name_with_prefix + + point = { + 'id': item['id'], + 'values': item['vector'], + 'metadata': process_metadata(metadata), + } + points.append(point) + return points + + def _get_collection_name_with_prefix(self, collection_name: str) -> str: + """Get the collection name with prefix.""" + return f'{self.collection_prefix}_{collection_name}' + + def _normalize_distance(self, score: float) -> float: + """Normalize distance score based on the metric used.""" + if self.metric.lower() == 'cosine': + # Cosine similarity ranges from -1 to 1, normalize to 0 to 1 + return (score + 1.0) / 2.0 + elif self.metric.lower() in ['euclidean', 'dotproduct']: + # These are already suitable for ranking (smaller is better for Euclidean) + return score + else: + # For other metrics, use as is + return score + + def _result_to_get_result(self, matches: list) -> GetResult: + """Convert Pinecone matches to GetResult format.""" + ids = [] + documents = [] + metadatas = [] + + for match in matches: + metadata = getattr(match, 'metadata', {}) or {} + ids.append(match.id if hasattr(match, 'id') else match['id']) + documents.append(metadata.get('text', '')) + metadatas.append(metadata) + + return GetResult( + **{ + 'ids': [ids], + 'documents': [documents], + 'metadatas': [metadatas], + } + ) + + def has_collection(self, collection_name: str) -> bool: + """Check if a collection exists by searching for at least one item.""" + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) + + try: + # Search for at least 1 item with this collection name in metadata + response = self.index.query( + vector=[0.0] * self.dimension, # dummy vector + top_k=1, + filter={'collection_name': collection_name_with_prefix}, + include_metadata=False, + ) + matches = getattr(response, 'matches', []) or [] + return len(matches) > 0 + except Exception as e: + log.exception(f"Error checking collection '{collection_name_with_prefix}': {e}") + return False + + def delete_collection(self, collection_name: str) -> None: + """Delete a collection by removing all vectors with the collection name in metadata.""" + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) + try: + self.index.delete(filter={'collection_name': collection_name_with_prefix}) + log.info(f"Collection '{collection_name_with_prefix}' deleted (all vectors removed).") + except Exception as e: + log.warning(f"Failed to delete collection '{collection_name_with_prefix}': {e}") + raise + + def insert(self, collection_name: str, items: List[VectorItem]) -> None: + """Insert vectors into a collection.""" + if not items: + log.warning('No items to insert') + return + + start_time = time.time() + + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) + points = self._create_points(items, collection_name_with_prefix) + + # Parallelize batch inserts for performance + executor = self._executor + futures = [] + for i in range(0, len(points), BATCH_SIZE): + batch = points[i : i + BATCH_SIZE] + futures.append(executor.submit(self.index.upsert, vectors=batch)) + for future in concurrent.futures.as_completed(futures): + try: + future.result() + except Exception as e: + log.error(f'Error inserting batch: {e}') + raise + elapsed = time.time() - start_time + log.debug(f'Insert of {len(points)} vectors took {elapsed:.2f} seconds') + log.info( + f"Successfully inserted {len(points)} vectors in parallel batches into '{collection_name_with_prefix}'" + ) + + def upsert(self, collection_name: str, items: List[VectorItem]) -> None: + """Upsert (insert or update) vectors into a collection.""" + if not items: + log.warning('No items to upsert') + return + + start_time = time.time() + + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) + points = self._create_points(items, collection_name_with_prefix) + + # Parallelize batch upserts for performance + executor = self._executor + futures = [] + for i in range(0, len(points), BATCH_SIZE): + batch = points[i : i + BATCH_SIZE] + futures.append(executor.submit(self.index.upsert, vectors=batch)) + for future in concurrent.futures.as_completed(futures): + try: + future.result() + except Exception as e: + log.error(f'Error upserting batch: {e}') + raise + elapsed = time.time() - start_time + log.debug(f'Upsert of {len(points)} vectors took {elapsed:.2f} seconds') + log.info( + f"Successfully upserted {len(points)} vectors in parallel batches into '{collection_name_with_prefix}'" + ) + + async def insert_async(self, collection_name: str, items: List[VectorItem]) -> None: + """Async version of insert using asyncio and run_in_executor for improved performance.""" + if not items: + log.warning('No items to insert') + return + + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) + points = self._create_points(items, collection_name_with_prefix) + + # Create batches + batches = [points[i : i + BATCH_SIZE] for i in range(0, len(points), BATCH_SIZE)] + loop = asyncio.get_event_loop() + tasks = [loop.run_in_executor(None, functools.partial(self.index.upsert, vectors=batch)) for batch in batches] + results = await asyncio.gather(*tasks, return_exceptions=True) + for result in results: + if isinstance(result, Exception): + log.error(f'Error in async insert batch: {result}') + raise result + log.info(f"Successfully async inserted {len(points)} vectors in batches into '{collection_name_with_prefix}'") + + async def upsert_async(self, collection_name: str, items: List[VectorItem]) -> None: + """Async version of upsert using asyncio and run_in_executor for improved performance.""" + if not items: + log.warning('No items to upsert') + return + + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) + points = self._create_points(items, collection_name_with_prefix) + + # Create batches + batches = [points[i : i + BATCH_SIZE] for i in range(0, len(points), BATCH_SIZE)] + loop = asyncio.get_event_loop() + tasks = [loop.run_in_executor(None, functools.partial(self.index.upsert, vectors=batch)) for batch in batches] + results = await asyncio.gather(*tasks, return_exceptions=True) + for result in results: + if isinstance(result, Exception): + log.error(f'Error in async upsert batch: {result}') + raise result + log.info(f"Successfully async upserted {len(points)} vectors in batches into '{collection_name_with_prefix}'") + + def search( + self, + collection_name: str, + vectors: List[List[Union[float, int]]], + filter: Optional[dict] = None, + limit: int = 10, + ) -> Optional[SearchResult]: + """Search for similar vectors in a collection.""" + if not vectors or not vectors[0]: + log.warning('No vectors provided for search') + return None + + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) + + if limit is None or limit <= 0: + limit = NO_LIMIT + + try: + # Search using the first vector (assuming this is the intended behavior) + query_vector = vectors[0] + + # Perform the search + query_response = self.index.query( + vector=query_vector, + top_k=limit, + include_metadata=True, + filter={'collection_name': collection_name_with_prefix}, + ) + + matches = getattr(query_response, 'matches', []) or [] + if not matches: + # Return empty result if no matches + return SearchResult( + ids=[[]], + documents=[[]], + metadatas=[[]], + distances=[[]], + ) + + # Convert to GetResult format + get_result = self._result_to_get_result(matches) + + # Calculate normalized distances based on metric + distances = [[self._normalize_distance(getattr(match, 'score', 0.0)) for match in matches]] + + return SearchResult( + ids=get_result.ids, + documents=get_result.documents, + metadatas=get_result.metadatas, + distances=distances, + ) + except Exception as e: + log.error(f"Error searching in '{collection_name_with_prefix}': {e}") + return None + + def query(self, collection_name: str, filter: Dict, limit: Optional[int] = None) -> Optional[GetResult]: + """Query vectors by metadata filter.""" + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) + + if limit is None or limit <= 0: + limit = NO_LIMIT + + try: + # Create a zero vector for the dimension as Pinecone requires a vector + zero_vector = [0.0] * self.dimension + + # Combine user filter with collection_name + pinecone_filter = {'collection_name': collection_name_with_prefix} + if filter: + pinecone_filter.update(filter) + + # Perform metadata-only query + query_response = self.index.query( + vector=zero_vector, + filter=pinecone_filter, + top_k=limit, + include_metadata=True, + ) + + matches = getattr(query_response, 'matches', []) or [] + return self._result_to_get_result(matches) + + except Exception as e: + log.error(f"Error querying collection '{collection_name}': {e}") + return None + + def get(self, collection_name: str) -> Optional[GetResult]: + """Get all vectors in a collection.""" + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) + + try: + # Use a zero vector for fetching all entries + zero_vector = [0.0] * self.dimension + + # Add filter to only get vectors for this collection + query_response = self.index.query( + vector=zero_vector, + top_k=NO_LIMIT, + include_metadata=True, + filter={'collection_name': collection_name_with_prefix}, + ) + + matches = getattr(query_response, 'matches', []) or [] + return self._result_to_get_result(matches) + + except Exception as e: + log.error(f"Error getting collection '{collection_name}': {e}") + return None + + def delete( + self, + collection_name: str, + ids: Optional[List[str]] = None, + filter: Optional[Dict] = None, + ) -> None: + """Delete vectors by IDs or filter.""" + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) + + try: + if ids: + # Delete by IDs (in batches for large deletions) + for i in range(0, len(ids), BATCH_SIZE): + batch_ids = ids[i : i + BATCH_SIZE] + # Note: When deleting by ID, we can't filter by collection_name + # This is a limitation of Pinecone - be careful with ID uniqueness + self.index.delete(ids=batch_ids) + log.debug(f"Deleted batch of {len(batch_ids)} vectors by ID from '{collection_name_with_prefix}'") + log.info(f"Successfully deleted {len(ids)} vectors by ID from '{collection_name_with_prefix}'") + + elif filter: + # Combine user filter with collection_name + pinecone_filter = {'collection_name': collection_name_with_prefix} + if filter: + pinecone_filter.update(filter) + # Delete by metadata filter + self.index.delete(filter=pinecone_filter) + log.info(f"Successfully deleted vectors by filter from '{collection_name_with_prefix}'") + + else: + log.warning('No ids or filter provided for delete operation') + + except Exception as e: + log.error(f"Error deleting from collection '{collection_name}': {e}") + raise + + def reset(self) -> None: + """Reset the database by deleting all collections.""" + try: + self.index.delete(delete_all=True) + log.info('All vectors successfully deleted from the index.') + except Exception as e: + log.error(f'Failed to reset Pinecone index: {e}') + raise + + def close(self): + """Shut down resources.""" + try: + # The new Pinecone client doesn't need explicit closing + pass + except Exception as e: + log.warning(f'Failed to clean up Pinecone resources: {e}') + self._executor.shutdown(wait=True) + + def __enter__(self): + """Enter context manager.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Exit context manager, ensuring resources are cleaned up.""" + self.close() diff --git a/backend/open_webui/retrieval/vector/dbs/qdrant.py b/backend/open_webui/retrieval/vector/dbs/qdrant.py new file mode 100644 index 0000000000000000000000000000000000000000..f050bebeb5a38ff6e4714c5a4b738382a286b12c --- /dev/null +++ b/backend/open_webui/retrieval/vector/dbs/qdrant.py @@ -0,0 +1,254 @@ +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + +from typing import Optional +import logging +from urllib.parse import urlparse + +from qdrant_client import QdrantClient as Qclient +from qdrant_client.http.models import PointStruct +from qdrant_client.models import models + +from open_webui.retrieval.vector.main import ( + VectorDBBase, + VectorItem, + SearchResult, + GetResult, +) +from open_webui.config import ( + QDRANT_URI, + QDRANT_API_KEY, + QDRANT_ON_DISK, + QDRANT_GRPC_PORT, + QDRANT_PREFER_GRPC, + QDRANT_COLLECTION_PREFIX, + QDRANT_TIMEOUT, + QDRANT_HNSW_M, +) + +NO_LIMIT = 999999999 + +log = logging.getLogger(__name__) + + +class QdrantClient(VectorDBBase): + def __init__(self): + self.collection_prefix = QDRANT_COLLECTION_PREFIX + self.QDRANT_URI = QDRANT_URI + self.QDRANT_API_KEY = QDRANT_API_KEY + self.QDRANT_ON_DISK = QDRANT_ON_DISK + self.PREFER_GRPC = QDRANT_PREFER_GRPC + self.GRPC_PORT = QDRANT_GRPC_PORT + self.QDRANT_TIMEOUT = QDRANT_TIMEOUT + self.QDRANT_HNSW_M = QDRANT_HNSW_M + + if not self.QDRANT_URI: + self.client = None + return + + # Unified handling for either scheme + parsed = urlparse(self.QDRANT_URI) + host = parsed.hostname or self.QDRANT_URI + http_port = parsed.port or 6333 # default REST port + + if self.PREFER_GRPC: + self.client = Qclient( + host=host, + port=http_port, + grpc_port=self.GRPC_PORT, + prefer_grpc=self.PREFER_GRPC, + api_key=self.QDRANT_API_KEY, + timeout=self.QDRANT_TIMEOUT, + ) + else: + self.client = Qclient( + url=self.QDRANT_URI, + api_key=self.QDRANT_API_KEY, + timeout=QDRANT_TIMEOUT, + ) + + def _result_to_get_result(self, points) -> GetResult: + ids = [] + documents = [] + metadatas = [] + + for point in points: + payload = point.payload + ids.append(point.id) + documents.append(payload['text']) + metadatas.append(payload['metadata']) + + return GetResult( + **{ + 'ids': [ids], + 'documents': [documents], + 'metadatas': [metadatas], + } + ) + + def _create_collection(self, collection_name: str, dimension: int): + collection_name_with_prefix = f'{self.collection_prefix}_{collection_name}' + self.client.create_collection( + collection_name=collection_name_with_prefix, + vectors_config=models.VectorParams( + size=dimension, + distance=models.Distance.COSINE, + on_disk=self.QDRANT_ON_DISK, + ), + hnsw_config=models.HnswConfigDiff( + m=self.QDRANT_HNSW_M, + ), + ) + + # Create payload indexes for efficient filtering + self.client.create_payload_index( + collection_name=collection_name_with_prefix, + field_name='metadata.hash', + field_schema=models.KeywordIndexParams( + type=models.KeywordIndexType.KEYWORD, + is_tenant=False, + on_disk=self.QDRANT_ON_DISK, + ), + ) + self.client.create_payload_index( + collection_name=collection_name_with_prefix, + field_name='metadata.file_id', + field_schema=models.KeywordIndexParams( + type=models.KeywordIndexType.KEYWORD, + is_tenant=False, + on_disk=self.QDRANT_ON_DISK, + ), + ) + log.info(f'collection {collection_name_with_prefix} successfully created!') + + def _create_collection_if_not_exists(self, collection_name, dimension): + if not self.has_collection(collection_name=collection_name): + self._create_collection(collection_name=collection_name, dimension=dimension) + + def _create_points(self, items: list[VectorItem]): + return [ + PointStruct( + id=item['id'], + vector=item['vector'], + payload={'text': item['text'], 'metadata': item['metadata']}, + ) + for item in items + ] + + def has_collection(self, collection_name: str) -> bool: + return self.client.collection_exists(f'{self.collection_prefix}_{collection_name}') + + def delete_collection(self, collection_name: str): + return self.client.delete_collection(collection_name=f'{self.collection_prefix}_{collection_name}') + + def search( + self, + collection_name: str, + vectors: list[list[float | int]], + filter: Optional[dict] = None, + limit: int = 10, + ) -> Optional[SearchResult]: + # Search for the nearest neighbor items based on the vectors and return 'limit' number of results. + if limit is None: + limit = NO_LIMIT # otherwise qdrant would set limit to 10! + + query_response = self.client.query_points( + collection_name=f'{self.collection_prefix}_{collection_name}', + query=vectors[0], + limit=limit, + ) + get_result = self._result_to_get_result(query_response.points) + return SearchResult( + ids=get_result.ids, + documents=get_result.documents, + metadatas=get_result.metadatas, + # qdrant distance is [-1, 1], normalize to [0, 1] + distances=[[(point.score + 1.0) / 2.0 for point in query_response.points]], + ) + + def query(self, collection_name: str, filter: dict, limit: Optional[int] = None): + # Construct the filter string for querying + if not self.has_collection(collection_name): + return None + try: + if limit is None: + limit = NO_LIMIT # otherwise qdrant would set limit to 10! + + field_conditions = [] + for key, value in filter.items(): + field_conditions.append( + models.FieldCondition(key=f'metadata.{key}', match=models.MatchValue(value=value)) + ) + + points = self.client.scroll( + collection_name=f'{self.collection_prefix}_{collection_name}', + scroll_filter=models.Filter(should=field_conditions), + limit=limit, + ) + return self._result_to_get_result(points[0]) + except Exception as e: + log.exception(f"Error querying a collection '{collection_name}': {e}") + return None + + def get(self, collection_name: str) -> Optional[GetResult]: + # Get all the items in the collection. + points = self.client.scroll( + collection_name=f'{self.collection_prefix}_{collection_name}', + limit=NO_LIMIT, # otherwise qdrant would set limit to 10! + ) + return self._result_to_get_result(points[0]) + + def insert(self, collection_name: str, items: list[VectorItem]): + # Insert the items into the collection, if the collection does not exist, it will be created. + self._create_collection_if_not_exists(collection_name, len(items[0]['vector'])) + points = self._create_points(items) + self.client.upload_points(f'{self.collection_prefix}_{collection_name}', points) + + def upsert(self, collection_name: str, items: list[VectorItem]): + # Update the items in the collection, if the items are not present, insert them. If the collection does not exist, it will be created. + self._create_collection_if_not_exists(collection_name, len(items[0]['vector'])) + points = self._create_points(items) + return self.client.upsert(f'{self.collection_prefix}_{collection_name}', points) + + def delete( + self, + collection_name: str, + ids: Optional[list[str]] = None, + filter: Optional[dict] = None, + ): + # Delete the items from the collection based on the ids. + field_conditions = [] + + if ids: + for id_value in ids: + ( + field_conditions.append( + models.FieldCondition( + key='metadata.id', + match=models.MatchValue(value=id_value), + ), + ), + ) + elif filter: + for key, value in filter.items(): + ( + field_conditions.append( + models.FieldCondition( + key=f'metadata.{key}', + match=models.MatchValue(value=value), + ), + ), + ) + + return self.client.delete( + collection_name=f'{self.collection_prefix}_{collection_name}', + points_selector=models.FilterSelector(filter=models.Filter(must=field_conditions)), + ) + + def reset(self): + # Resets the database. This will delete all collections and item entries. + collection_names = self.client.get_collections().collections + for collection_name in collection_names: + if collection_name.name.startswith(self.collection_prefix): + self.client.delete_collection(collection_name=collection_name.name) diff --git a/backend/open_webui/retrieval/vector/dbs/qdrant_multitenancy.py b/backend/open_webui/retrieval/vector/dbs/qdrant_multitenancy.py new file mode 100644 index 0000000000000000000000000000000000000000..c3c2ba41d03d0cbdb10e840f3bbee4a04a4ffed3 --- /dev/null +++ b/backend/open_webui/retrieval/vector/dbs/qdrant_multitenancy.py @@ -0,0 +1,356 @@ +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + +import logging +from typing import Optional, Tuple, List, Dict, Any +from urllib.parse import urlparse + +import grpc +from open_webui.config import ( + QDRANT_API_KEY, + QDRANT_GRPC_PORT, + QDRANT_ON_DISK, + QDRANT_PREFER_GRPC, + QDRANT_URI, + QDRANT_COLLECTION_PREFIX, + QDRANT_TIMEOUT, + QDRANT_HNSW_M, +) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) +from qdrant_client import QdrantClient as Qclient +from qdrant_client.http.exceptions import UnexpectedResponse +from qdrant_client.http.models import PointStruct +from qdrant_client.models import models + +NO_LIMIT = 999999999 +TENANT_ID_FIELD = 'tenant_id' +DEFAULT_DIMENSION = 384 + +log = logging.getLogger(__name__) + + +def _tenant_filter(tenant_id: str) -> models.FieldCondition: + return models.FieldCondition(key=TENANT_ID_FIELD, match=models.MatchValue(value=tenant_id)) + + +def _metadata_filter(key: str, value: Any) -> models.FieldCondition: + return models.FieldCondition(key=f'metadata.{key}', match=models.MatchValue(value=value)) + + +class QdrantClient(VectorDBBase): + def __init__(self): + self.collection_prefix = QDRANT_COLLECTION_PREFIX + self.QDRANT_URI = QDRANT_URI + self.QDRANT_API_KEY = QDRANT_API_KEY + self.QDRANT_ON_DISK = QDRANT_ON_DISK + self.PREFER_GRPC = QDRANT_PREFER_GRPC + self.GRPC_PORT = QDRANT_GRPC_PORT + self.QDRANT_TIMEOUT = QDRANT_TIMEOUT + self.QDRANT_HNSW_M = QDRANT_HNSW_M + + if not self.QDRANT_URI: + raise ValueError('QDRANT_URI is not set. Please configure it in the environment variables.') + + # Unified handling for either scheme + parsed = urlparse(self.QDRANT_URI) + host = parsed.hostname or self.QDRANT_URI + http_port = parsed.port or 6333 # default REST port + + self.client = ( + Qclient( + host=host, + port=http_port, + grpc_port=self.GRPC_PORT, + prefer_grpc=self.PREFER_GRPC, + api_key=self.QDRANT_API_KEY, + timeout=self.QDRANT_TIMEOUT, + ) + if self.PREFER_GRPC + else Qclient( + url=self.QDRANT_URI, + api_key=self.QDRANT_API_KEY, + timeout=self.QDRANT_TIMEOUT, + ) + ) + + # Main collection types for multi-tenancy + self.MEMORY_COLLECTION = f'{self.collection_prefix}_memories' + self.KNOWLEDGE_COLLECTION = f'{self.collection_prefix}_knowledge' + self.FILE_COLLECTION = f'{self.collection_prefix}_files' + self.WEB_SEARCH_COLLECTION = f'{self.collection_prefix}_web-search' + self.HASH_BASED_COLLECTION = f'{self.collection_prefix}_hash-based' + + def _result_to_get_result(self, points) -> GetResult: + ids, documents, metadatas = [], [], [] + for point in points: + payload = point.payload + ids.append(point.id) + documents.append(payload['text']) + metadatas.append(payload['metadata']) + return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas]) + + def _get_collection_and_tenant_id(self, collection_name: str) -> Tuple[str, str]: + """ + Maps the traditional collection name to multi-tenant collection and tenant ID. + + Returns: + tuple: (collection_name, tenant_id) + + WARNING: This mapping relies on current Open WebUI naming conventions for + collection names. If Open WebUI changes how it generates collection names + (e.g., "user-memory-" prefix, "file-" prefix, web search patterns, or hash + formats), this mapping will break and route data to incorrect collections. + POTENTIALLY CAUSING HUGE DATA CORRUPTION, DATA CONSISTENCY ISSUES AND INCORRECT + DATA MAPPING INSIDE THE DATABASE. + """ + # Check for user memory collections + tenant_id = collection_name + + if collection_name.startswith('user-memory-'): + return self.MEMORY_COLLECTION, tenant_id + + # Check for file collections + elif collection_name.startswith('file-'): + return self.FILE_COLLECTION, tenant_id + + # Check for web search collections + elif collection_name.startswith('web-search-'): + return self.WEB_SEARCH_COLLECTION, tenant_id + + # Handle hash-based collections (YouTube and web URLs) + elif len(collection_name) == 63 and all(c in '0123456789abcdef' for c in collection_name): + return self.HASH_BASED_COLLECTION, tenant_id + + else: + return self.KNOWLEDGE_COLLECTION, tenant_id + + def _create_multi_tenant_collection(self, mt_collection_name: str, dimension: int = DEFAULT_DIMENSION): + """ + Creates a collection with multi-tenancy configuration and payload indexes for tenant_id and metadata fields. + """ + self.client.create_collection( + collection_name=mt_collection_name, + vectors_config=models.VectorParams( + size=dimension, + distance=models.Distance.COSINE, + on_disk=self.QDRANT_ON_DISK, + ), + # Disable global index building due to multitenancy + # For more details https://qdrant.tech/documentation/guides/multiple-partitions/#calibrate-performance + hnsw_config=models.HnswConfigDiff( + payload_m=self.QDRANT_HNSW_M, + m=0, + ), + ) + log.info(f'Multi-tenant collection {mt_collection_name} created with dimension {dimension}!') + + self.client.create_payload_index( + collection_name=mt_collection_name, + field_name=TENANT_ID_FIELD, + field_schema=models.KeywordIndexParams( + type=models.KeywordIndexType.KEYWORD, + is_tenant=True, + on_disk=self.QDRANT_ON_DISK, + ), + ) + + for field in ('metadata.hash', 'metadata.file_id'): + self.client.create_payload_index( + collection_name=mt_collection_name, + field_name=field, + field_schema=models.KeywordIndexParams( + type=models.KeywordIndexType.KEYWORD, + on_disk=self.QDRANT_ON_DISK, + ), + ) + + def _create_points(self, items: List[VectorItem], tenant_id: str) -> List[PointStruct]: + """ + Create point structs from vector items with tenant ID. + """ + return [ + PointStruct( + id=item['id'], + vector=item['vector'], + payload={ + 'text': item['text'], + 'metadata': item['metadata'], + TENANT_ID_FIELD: tenant_id, + }, + ) + for item in items + ] + + def _ensure_collection(self, mt_collection_name: str, dimension: int = DEFAULT_DIMENSION): + """ + Ensure the collection exists and payload indexes are created for tenant_id and metadata fields. + """ + if not self.client.collection_exists(collection_name=mt_collection_name): + self._create_multi_tenant_collection(mt_collection_name, dimension) + + def has_collection(self, collection_name: str) -> bool: + """ + Check if a logical collection exists by checking for any points with the tenant ID. + """ + if not self.client: + return False + mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name) + if not self.client.collection_exists(collection_name=mt_collection): + return False + tenant_filter = _tenant_filter(tenant_id) + count_result = self.client.count( + collection_name=mt_collection, + count_filter=models.Filter(must=[tenant_filter]), + ) + return count_result.count > 0 + + def delete( + self, + collection_name: str, + ids: Optional[List[str]] = None, + filter: Optional[Dict[str, Any]] = None, + ): + """ + Delete vectors by ID or filter from a collection with tenant isolation. + """ + if not self.client: + return None + + mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name) + if not self.client.collection_exists(collection_name=mt_collection): + log.debug(f"Collection {mt_collection} doesn't exist, nothing to delete") + return None + + must_conditions = [_tenant_filter(tenant_id)] + should_conditions = [] + if ids: + should_conditions = [_metadata_filter('id', id_value) for id_value in ids] + elif filter: + must_conditions += [_metadata_filter(k, v) for k, v in filter.items()] + + return self.client.delete( + collection_name=mt_collection, + points_selector=models.FilterSelector(filter=models.Filter(must=must_conditions, should=should_conditions)), + ) + + def search( + self, + collection_name: str, + vectors: List[List[float | int]], + filter: Optional[Dict] = None, + limit: int = 10, + ) -> Optional[SearchResult]: + """ + Search for the nearest neighbor items based on the vectors with tenant isolation. + """ + if not self.client or not vectors: + return None + mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name) + if not self.client.collection_exists(collection_name=mt_collection): + log.debug(f"Collection {mt_collection} doesn't exist, search returns None") + return None + + tenant_filter = _tenant_filter(tenant_id) + query_response = self.client.query_points( + collection_name=mt_collection, + query=vectors[0], + limit=limit, + query_filter=models.Filter(must=[tenant_filter]), + ) + get_result = self._result_to_get_result(query_response.points) + return SearchResult( + ids=get_result.ids, + documents=get_result.documents, + metadatas=get_result.metadatas, + distances=[[(point.score + 1.0) / 2.0 for point in query_response.points]], + ) + + def query(self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None): + """ + Query points with filters and tenant isolation. + """ + if not self.client: + return None + mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name) + if not self.client.collection_exists(collection_name=mt_collection): + log.debug(f"Collection {mt_collection} doesn't exist, query returns None") + return None + if limit is None: + limit = NO_LIMIT + tenant_filter = _tenant_filter(tenant_id) + field_conditions = [_metadata_filter(k, v) for k, v in filter.items()] + combined_filter = models.Filter(must=[tenant_filter, *field_conditions]) + points = self.client.scroll( + collection_name=mt_collection, + scroll_filter=combined_filter, + limit=limit, + ) + return self._result_to_get_result(points[0]) + + def get(self, collection_name: str) -> Optional[GetResult]: + """ + Get all items in a collection with tenant isolation. + """ + if not self.client: + return None + mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name) + if not self.client.collection_exists(collection_name=mt_collection): + log.debug(f"Collection {mt_collection} doesn't exist, get returns None") + return None + tenant_filter = _tenant_filter(tenant_id) + points = self.client.scroll( + collection_name=mt_collection, + scroll_filter=models.Filter(must=[tenant_filter]), + limit=NO_LIMIT, + ) + return self._result_to_get_result(points[0]) + + def upsert(self, collection_name: str, items: List[VectorItem]): + """ + Upsert items with tenant ID. + """ + if not self.client or not items: + return None + mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name) + dimension = len(items[0]['vector']) + self._ensure_collection(mt_collection, dimension) + points = self._create_points(items, tenant_id) + self.client.upload_points(mt_collection, points) + return None + + def insert(self, collection_name: str, items: List[VectorItem]): + """ + Insert items with tenant ID. + """ + return self.upsert(collection_name, items) + + def reset(self): + """ + Reset the database by deleting all collections. + """ + if not self.client: + return None + for collection in self.client.get_collections().collections: + if collection.name.startswith(self.collection_prefix): + self.client.delete_collection(collection_name=collection.name) + + def delete_collection(self, collection_name: str): + """ + Delete a collection. + """ + if not self.client: + return None + mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name) + if not self.client.collection_exists(collection_name=mt_collection): + log.debug(f"Collection {mt_collection} doesn't exist, nothing to delete") + return None + self.client.delete( + collection_name=mt_collection, + points_selector=models.FilterSelector(filter=models.Filter(must=[_tenant_filter(tenant_id)])), + ) diff --git a/backend/open_webui/retrieval/vector/dbs/s3vector.py b/backend/open_webui/retrieval/vector/dbs/s3vector.py new file mode 100644 index 0000000000000000000000000000000000000000..8877d206e657aaad120031c20dc921205041f73f --- /dev/null +++ b/backend/open_webui/retrieval/vector/dbs/s3vector.py @@ -0,0 +1,716 @@ +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + +from open_webui.retrieval.vector.utils import process_metadata +from open_webui.retrieval.vector.main import ( + VectorDBBase, + VectorItem, + GetResult, + SearchResult, +) +from open_webui.config import S3_VECTOR_BUCKET_NAME, S3_VECTOR_REGION +from typing import List, Optional, Dict, Any, Union +import logging +import boto3 + +log = logging.getLogger(__name__) + + +class S3VectorClient(VectorDBBase): + """ + AWS S3 Vector integration for Open WebUI Knowledge. + """ + + def __init__(self): + self.bucket_name = S3_VECTOR_BUCKET_NAME + self.region = S3_VECTOR_REGION + + # Simple validation - log warnings instead of raising exceptions + if not self.bucket_name: + log.warning('S3_VECTOR_BUCKET_NAME not set - S3Vector will not work') + if not self.region: + log.warning('S3_VECTOR_REGION not set - S3Vector will not work') + + if self.bucket_name and self.region: + try: + self.client = boto3.client('s3vectors', region_name=self.region) + log.info(f"S3Vector client initialized for bucket '{self.bucket_name}' in region '{self.region}'") + except Exception as e: + log.error(f'Failed to initialize S3Vector client: {e}') + self.client = None + else: + self.client = None + + def _create_index( + self, + index_name: str, + dimension: int, + data_type: str = 'float32', + distance_metric: str = 'cosine', + ) -> None: + """ + Create a new index in the S3 vector bucket for the given collection if it does not exist. + """ + if self.has_collection(index_name): + log.debug(f"Index '{index_name}' already exists, skipping creation") + return + + try: + self.client.create_index( + vectorBucketName=self.bucket_name, + indexName=index_name, + dataType=data_type, + dimension=dimension, + distanceMetric=distance_metric, + metadataConfiguration={ + 'nonFilterableMetadataKeys': [ + 'text', + ] + }, + ) + log.info(f'Created S3 index: {index_name} (dim={dimension}, type={data_type}, metric={distance_metric})') + except Exception as e: + log.error(f"Error creating S3 index '{index_name}': {e}") + raise + + def _filter_metadata(self, metadata: Dict[str, Any], item_id: str) -> Dict[str, Any]: + """ + Filter vector metadata keys to comply with S3 Vector API limit of 10 keys maximum. + """ + if not isinstance(metadata, dict) or len(metadata) <= 10: + return metadata + + # Keep only the first 10 keys, prioritizing important ones based on actual Open WebUI metadata + important_keys = [ + 'text', # The actual document content + 'file_id', # File ID + 'source', # Document source file + 'title', # Document title + 'page', # Page number + 'total_pages', # Total pages in document + 'embedding_config', # Embedding configuration + 'created_by', # User who created it + 'name', # Document name + 'hash', # Content hash + ] + filtered_metadata = {} + + # First, add important keys if they exist + for key in important_keys: + if key in metadata: + filtered_metadata[key] = metadata[key] + if len(filtered_metadata) >= 10: + break + + # If we still have room, add other keys + if len(filtered_metadata) < 10: + for key, value in metadata.items(): + if key not in filtered_metadata: + filtered_metadata[key] = value + if len(filtered_metadata) >= 10: + break + + log.warning(f"Metadata for key '{item_id}' had {len(metadata)} keys, limited to 10 keys") + return filtered_metadata + + def has_collection(self, collection_name: str) -> bool: + """ + Check if a vector index exists using direct lookup. + This avoids pagination issues with list_indexes() and is significantly faster. + """ + try: + self.client.get_index(vectorBucketName=self.bucket_name, indexName=collection_name) + return True + except Exception as e: + log.error(f"Error checking if index '{collection_name}' exists: {e}") + return False + + def delete_collection(self, collection_name: str) -> None: + """ + Delete an entire S3 Vector index/collection. + """ + + if not self.has_collection(collection_name): + log.warning(f"Collection '{collection_name}' does not exist, nothing to delete") + return + + try: + log.info(f"Deleting collection '{collection_name}'") + self.client.delete_index(vectorBucketName=self.bucket_name, indexName=collection_name) + log.info(f"Successfully deleted collection '{collection_name}'") + except Exception as e: + log.error(f"Error deleting collection '{collection_name}': {e}") + raise + + def insert(self, collection_name: str, items: List[VectorItem]) -> None: + """ + Insert vector items into the S3 Vector index. Create index if it does not exist. + """ + if not items: + log.warning('No items to insert') + return + + dimension = len(items[0]['vector']) + + try: + if not self.has_collection(collection_name): + log.info(f"Index '{collection_name}' does not exist. Creating index.") + self._create_index( + index_name=collection_name, + dimension=dimension, + data_type='float32', + distance_metric='cosine', + ) + + # Prepare vectors for insertion + vectors = [] + for item in items: + # Ensure vector data is in the correct format for S3 Vector API + vector_data = item['vector'] + if isinstance(vector_data, list): + # Convert list to float32 values as required by S3 Vector API + vector_data = [float(x) for x in vector_data] + + # Prepare metadata, ensuring the text field is preserved + metadata = item.get('metadata', {}).copy() + + # Add the text field to metadata so it's available for retrieval + metadata['text'] = item['text'] + + # Convert metadata to string format for consistency + metadata = process_metadata(metadata) + + # Filter metadata to comply with S3 Vector API limit of 10 keys + metadata = self._filter_metadata(metadata, item['id']) + + vectors.append( + { + 'key': item['id'], + 'data': {'float32': vector_data}, + 'metadata': metadata, + } + ) + + # Insert vectors in batches of 500 (S3 Vector API limit) + batch_size = 500 + for i in range(0, len(vectors), batch_size): + batch = vectors[i : i + batch_size] + self.client.put_vectors( + vectorBucketName=self.bucket_name, + indexName=collection_name, + vectors=batch, + ) + log.info(f"Inserted batch {i // batch_size + 1}: {len(batch)} vectors into index '{collection_name}'.") + + log.info(f"Completed insertion of {len(vectors)} vectors into index '{collection_name}'.") + except Exception as e: + log.error(f'Error inserting vectors: {e}') + raise + + def upsert(self, collection_name: str, items: List[VectorItem]) -> None: + """ + Insert or update vector items in the S3 Vector index. Create index if it does not exist. + """ + if not items: + log.warning('No items to upsert') + return + + dimension = len(items[0]['vector']) + log.info(f'Upsert dimension: {dimension}') + + try: + if not self.has_collection(collection_name): + log.info(f"Index '{collection_name}' does not exist. Creating index for upsert.") + self._create_index( + index_name=collection_name, + dimension=dimension, + data_type='float32', + distance_metric='cosine', + ) + + # Prepare vectors for upsert + vectors = [] + for item in items: + # Ensure vector data is in the correct format for S3 Vector API + vector_data = item['vector'] + if isinstance(vector_data, list): + # Convert list to float32 values as required by S3 Vector API + vector_data = [float(x) for x in vector_data] + + # Prepare metadata, ensuring the text field is preserved + metadata = item.get('metadata', {}).copy() + # Add the text field to metadata so it's available for retrieval + metadata['text'] = item['text'] + + # Convert metadata to string format for consistency + metadata = process_metadata(metadata) + + # Filter metadata to comply with S3 Vector API limit of 10 keys + metadata = self._filter_metadata(metadata, item['id']) + + vectors.append( + { + 'key': item['id'], + 'data': {'float32': vector_data}, + 'metadata': metadata, + } + ) + + # Upsert vectors in batches of 500 (S3 Vector API limit) + batch_size = 500 + for i in range(0, len(vectors), batch_size): + batch = vectors[i : i + batch_size] + if i == 0: # Log sample info for first batch only + log.info( + f'Upserting batch 1: {len(batch)} vectors. First vector sample: key={batch[0]["key"]}, data_type={type(batch[0]["data"]["float32"])}, data_len={len(batch[0]["data"]["float32"])}' + ) + else: + log.info(f'Upserting batch {i // batch_size + 1}: {len(batch)} vectors.') + + self.client.put_vectors( + vectorBucketName=self.bucket_name, + indexName=collection_name, + vectors=batch, + ) + + log.info(f"Completed upsert of {len(vectors)} vectors into index '{collection_name}'.") + except Exception as e: + log.error(f'Error upserting vectors: {e}') + raise + + def search( + self, + collection_name: str, + vectors: List[List[Union[float, int]]], + filter: Optional[dict] = None, + limit: int = 10, + ) -> Optional[SearchResult]: + """ + Search for similar vectors in a collection using multiple query vectors. + """ + + if not self.has_collection(collection_name): + log.warning(f"Collection '{collection_name}' does not exist") + return None + + if not vectors: + log.warning('No query vectors provided') + return None + + try: + log.info(f"Searching collection '{collection_name}' with {len(vectors)} query vectors, limit={limit}") + + # Initialize result lists + all_ids = [] + all_documents = [] + all_metadatas = [] + all_distances = [] + + # Process each query vector + for i, query_vector in enumerate(vectors): + log.debug(f'Processing query vector {i + 1}/{len(vectors)}') + + # Prepare the query vector in S3 Vector format + query_vector_dict = {'float32': [float(x) for x in query_vector]} + + # Call S3 Vector query API + response = self.client.query_vectors( + vectorBucketName=self.bucket_name, + indexName=collection_name, + topK=limit, + queryVector=query_vector_dict, + returnMetadata=True, + returnDistance=True, + ) + + # Process results for this query + query_ids = [] + query_documents = [] + query_metadatas = [] + query_distances = [] + + result_vectors = response.get('vectors', []) + + for vector in result_vectors: + vector_id = vector.get('key') + vector_metadata = vector.get('metadata', {}) + vector_distance = vector.get('distance', 0.0) + + # Extract document text from metadata + document_text = '' + if isinstance(vector_metadata, dict): + # Get the text field first (highest priority) + document_text = vector_metadata.get('text') + if not document_text: + # Fallback to other possible text fields + document_text = ( + vector_metadata.get('content') or vector_metadata.get('document') or vector_id + ) + else: + document_text = vector_id + + query_ids.append(vector_id) + query_documents.append(document_text) + query_metadatas.append(vector_metadata) + query_distances.append(vector_distance) + + # Add this query's results to the overall results + all_ids.append(query_ids) + all_documents.append(query_documents) + all_metadatas.append(query_metadatas) + all_distances.append(query_distances) + + log.info(f'Search completed. Found results for {len(all_ids)} queries') + + # Return SearchResult format + return SearchResult( + ids=all_ids if all_ids else None, + documents=all_documents if all_documents else None, + metadatas=all_metadatas if all_metadatas else None, + distances=all_distances if all_distances else None, + ) + + except Exception as e: + log.error(f"Error searching collection '{collection_name}': {str(e)}") + # Handle specific AWS exceptions + if hasattr(e, 'response') and 'Error' in e.response: + error_code = e.response['Error']['Code'] + if error_code == 'NotFoundException': + log.warning(f"Collection '{collection_name}' not found") + return None + elif error_code == 'ValidationException': + log.error(f'Invalid query vector dimensions or parameters') + return None + elif error_code == 'AccessDeniedException': + log.error(f"Access denied for collection '{collection_name}'. Check permissions.") + return None + raise + + def query(self, collection_name: str, filter: Dict, limit: Optional[int] = None) -> Optional[GetResult]: + """ + Query vectors from a collection using metadata filter. + """ + + if not self.has_collection(collection_name): + log.warning(f"Collection '{collection_name}' does not exist") + return GetResult(ids=[[]], documents=[[]], metadatas=[[]]) + + if not filter: + log.warning('No filter provided, returning all vectors') + return self.get(collection_name) + + try: + log.info(f"Querying collection '{collection_name}' with filter: {filter}") + + # For S3 Vector, we need to use list_vectors and then filter results + # Since S3 Vector may not support complex server-side filtering, + # we'll retrieve all vectors and filter client-side + + # Get all vectors first + all_vectors_result = self.get(collection_name) + + if not all_vectors_result or not all_vectors_result.ids: + log.warning('No vectors found in collection') + return GetResult(ids=[[]], documents=[[]], metadatas=[[]]) + + # Extract the lists from the result + all_ids = all_vectors_result.ids[0] if all_vectors_result.ids else [] + all_documents = all_vectors_result.documents[0] if all_vectors_result.documents else [] + all_metadatas = all_vectors_result.metadatas[0] if all_vectors_result.metadatas else [] + + # Apply client-side filtering + filtered_ids = [] + filtered_documents = [] + filtered_metadatas = [] + + for i, metadata in enumerate(all_metadatas): + if self._matches_filter(metadata, filter): + if i < len(all_ids): + filtered_ids.append(all_ids[i]) + if i < len(all_documents): + filtered_documents.append(all_documents[i]) + filtered_metadatas.append(metadata) + + # Apply limit if specified + if limit and len(filtered_ids) >= limit: + break + + log.info(f'Filter applied: {len(filtered_ids)} vectors match out of {len(all_ids)} total') + + # Return GetResult format + if filtered_ids: + return GetResult( + ids=[filtered_ids], + documents=[filtered_documents], + metadatas=[filtered_metadatas], + ) + else: + return GetResult(ids=[[]], documents=[[]], metadatas=[[]]) + + except Exception as e: + log.error(f"Error querying collection '{collection_name}': {str(e)}") + # Handle specific AWS exceptions + if hasattr(e, 'response') and 'Error' in e.response: + error_code = e.response['Error']['Code'] + if error_code == 'NotFoundException': + log.warning(f"Collection '{collection_name}' not found") + return GetResult(ids=[[]], documents=[[]], metadatas=[[]]) + elif error_code == 'AccessDeniedException': + log.error(f"Access denied for collection '{collection_name}'. Check permissions.") + return GetResult(ids=[[]], documents=[[]], metadatas=[[]]) + raise + + def get(self, collection_name: str) -> Optional[GetResult]: + """ + Retrieve all vectors from a collection. + """ + + if not self.has_collection(collection_name): + log.warning(f"Collection '{collection_name}' does not exist") + return GetResult(ids=[[]], documents=[[]], metadatas=[[]]) + + try: + log.info(f"Retrieving all vectors from collection '{collection_name}'") + + # Initialize result lists + all_ids = [] + all_documents = [] + all_metadatas = [] + + # Handle pagination + next_token = None + + while True: + # Prepare request parameters + request_params = { + 'vectorBucketName': self.bucket_name, + 'indexName': collection_name, + 'returnData': False, # Don't include vector data (not needed for get) + 'returnMetadata': True, # Include metadata + 'maxResults': 500, # Use reasonable page size + } + + if next_token: + request_params['nextToken'] = next_token + + # Call S3 Vector API + response = self.client.list_vectors(**request_params) + + # Process vectors in this page + vectors = response.get('vectors', []) + + for vector in vectors: + vector_id = vector.get('key') + vector_data = vector.get('data', {}) + vector_metadata = vector.get('metadata', {}) + + # Extract the actual vector array + vector_array = vector_data.get('float32', []) + + # For documents, we try to extract text from metadata or use the vector ID + document_text = '' + if isinstance(vector_metadata, dict): + # Get the text field first (highest priority) + document_text = vector_metadata.get('text') + if not document_text: + # Fallback to other possible text fields + document_text = ( + vector_metadata.get('content') or vector_metadata.get('document') or vector_id + ) + + # Log the actual content for debugging + log.debug(f'Document text preview (first 200 chars): {str(document_text)[:200]}') + else: + document_text = vector_id + + all_ids.append(vector_id) + all_documents.append(document_text) + all_metadatas.append(vector_metadata) + + # Check if there are more pages + next_token = response.get('nextToken') + if not next_token: + break + + log.info(f"Retrieved {len(all_ids)} vectors from collection '{collection_name}'") + + # Return in GetResult format + # The Open WebUI GetResult expects lists of lists, so we wrap each list + if all_ids: + return GetResult(ids=[all_ids], documents=[all_documents], metadatas=[all_metadatas]) + else: + return GetResult(ids=[[]], documents=[[]], metadatas=[[]]) + + except Exception as e: + log.error(f"Error retrieving vectors from collection '{collection_name}': {str(e)}") + # Handle specific AWS exceptions + if hasattr(e, 'response') and 'Error' in e.response: + error_code = e.response['Error']['Code'] + if error_code == 'NotFoundException': + log.warning(f"Collection '{collection_name}' not found") + return GetResult(ids=[[]], documents=[[]], metadatas=[[]]) + elif error_code == 'AccessDeniedException': + log.error(f"Access denied for collection '{collection_name}'. Check permissions.") + return GetResult(ids=[[]], documents=[[]], metadatas=[[]]) + raise + + def delete( + self, + collection_name: str, + ids: Optional[List[str]] = None, + filter: Optional[Dict] = None, + ) -> None: + """ + Delete vectors by ID or filter from a collection. + """ + + if not self.has_collection(collection_name): + log.warning(f"Collection '{collection_name}' does not exist, nothing to delete") + return + + # Check if this is a knowledge collection (not file-specific) + is_knowledge_collection = not collection_name.startswith('file-') + + try: + if ids: + # Delete by specific vector IDs/keys + log.info(f"Deleting {len(ids)} vectors by IDs from collection '{collection_name}'") + self.client.delete_vectors( + vectorBucketName=self.bucket_name, + indexName=collection_name, + keys=ids, + ) + log.info(f"Deleted {len(ids)} vectors from index '{collection_name}'") + + elif filter: + # Handle filter-based deletion + log.info(f"Deleting vectors by filter from collection '{collection_name}': {filter}") + + # If this is a knowledge collection and we have a file_id filter, + # also clean up the corresponding file-specific collection + if is_knowledge_collection and 'file_id' in filter: + file_id = filter['file_id'] + file_collection_name = f'file-{file_id}' + if self.has_collection(file_collection_name): + log.info( + f"Found related file-specific collection '{file_collection_name}', deleting it to prevent duplicates" + ) + self.delete_collection(file_collection_name) + + # For the main collection, implement query-then-delete + # First, query to get IDs matching the filter + query_result = self.query(collection_name, filter) + if query_result and query_result.ids and query_result.ids[0]: + matching_ids = query_result.ids[0] + log.info(f'Found {len(matching_ids)} vectors matching filter, deleting them') + + # Delete the matching vectors by ID + self.client.delete_vectors( + vectorBucketName=self.bucket_name, + indexName=collection_name, + keys=matching_ids, + ) + log.info(f"Deleted {len(matching_ids)} vectors from index '{collection_name}' using filter") + else: + log.warning('No vectors found matching the filter criteria') + else: + log.warning('No IDs or filter provided for deletion') + except Exception as e: + log.error(f"Error deleting vectors from collection '{collection_name}': {e}") + raise + + def reset(self) -> None: + """ + Reset/clear all vector data. For S3 Vector, this deletes all indexes. + """ + + try: + log.warning('Reset called - this will delete all vector indexes in the S3 bucket') + + # List all indexes + response = self.client.list_indexes(vectorBucketName=self.bucket_name) + indexes = response.get('indexes', []) + + if not indexes: + log.warning('No indexes found to delete') + return + + # Delete all indexes + deleted_count = 0 + for index in indexes: + index_name = index.get('indexName') + if index_name: + try: + self.client.delete_index(vectorBucketName=self.bucket_name, indexName=index_name) + deleted_count += 1 + log.info(f'Deleted index: {index_name}') + except Exception as e: + log.error(f"Error deleting index '{index_name}': {e}") + + log.info(f'Reset completed: deleted {deleted_count} indexes') + + except Exception as e: + log.error(f'Error during reset: {e}') + raise + + def _matches_filter(self, metadata: Dict[str, Any], filter: Dict[str, Any]) -> bool: + """ + Check if metadata matches the given filter conditions. + """ + if not isinstance(metadata, dict) or not isinstance(filter, dict): + return False + + # Check each filter condition + for key, expected_value in filter.items(): + # Handle special operators + if key.startswith('$'): + if key == '$and': + # All conditions must match + if not isinstance(expected_value, list): + continue + for condition in expected_value: + if not self._matches_filter(metadata, condition): + return False + elif key == '$or': + # At least one condition must match + if not isinstance(expected_value, list): + continue + any_match = False + for condition in expected_value: + if self._matches_filter(metadata, condition): + any_match = True + break + if not any_match: + return False + continue + + # Get the actual value from metadata + actual_value = metadata.get(key) + + # Handle different types of expected values + if isinstance(expected_value, dict): + # Handle comparison operators + for op, op_value in expected_value.items(): + if op == '$eq': + if actual_value != op_value: + return False + elif op == '$ne': + if actual_value == op_value: + return False + elif op == '$in': + if not isinstance(op_value, list) or actual_value not in op_value: + return False + elif op == '$nin': + if isinstance(op_value, list) and actual_value in op_value: + return False + elif op == '$exists': + if bool(op_value) != (key in metadata): + return False + # Add more operators as needed + else: + # Simple equality check + if actual_value != expected_value: + return False + + return True diff --git a/backend/open_webui/retrieval/vector/dbs/weaviate.py b/backend/open_webui/retrieval/vector/dbs/weaviate.py new file mode 100644 index 0000000000000000000000000000000000000000..2cf4c135c5f80335c093096903d452122825aa5e --- /dev/null +++ b/backend/open_webui/retrieval/vector/dbs/weaviate.py @@ -0,0 +1,326 @@ +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + +import weaviate +import re +import uuid +from typing import Any, Dict, List, Optional, Union + +from open_webui.retrieval.vector.main import ( + VectorDBBase, + VectorItem, + SearchResult, + GetResult, +) +from open_webui.retrieval.vector.utils import process_metadata +from open_webui.config import ( + WEAVIATE_HTTP_HOST, + WEAVIATE_GRPC_HOST, + WEAVIATE_HTTP_PORT, + WEAVIATE_GRPC_PORT, + WEAVIATE_API_KEY, + WEAVIATE_HTTP_SECURE, + WEAVIATE_GRPC_SECURE, + WEAVIATE_SKIP_INIT_CHECKS, +) + + +def _convert_uuids_to_strings(obj: Any) -> Any: + """ + Recursively convert UUID objects to strings in nested data structures. + + This function handles: + - UUID objects -> string + - Dictionaries with UUID values + - Lists/Tuples with UUID values + - Nested combinations of the above + + Args: + obj: Any object that might contain UUIDs + + Returns: + The same object structure with UUIDs converted to strings + """ + if isinstance(obj, uuid.UUID): + return str(obj) + elif isinstance(obj, dict): + return {key: _convert_uuids_to_strings(value) for key, value in obj.items()} + elif isinstance(obj, (list, tuple)): + return type(obj)(_convert_uuids_to_strings(item) for item in obj) + elif isinstance(obj, (str, int, float, bool, type(None))): + return obj + else: + return obj + + +class WeaviateClient(VectorDBBase): + def __init__(self): + self.url = WEAVIATE_HTTP_HOST + try: + # Build connection parameters + connection_params = { + 'http_host': WEAVIATE_HTTP_HOST, + 'http_port': WEAVIATE_HTTP_PORT, + 'http_secure': WEAVIATE_HTTP_SECURE, + 'grpc_host': WEAVIATE_GRPC_HOST, + 'grpc_port': WEAVIATE_GRPC_PORT, + 'grpc_secure': WEAVIATE_GRPC_SECURE, + 'skip_init_checks': WEAVIATE_SKIP_INIT_CHECKS, + } + + # Only add auth_credentials if WEAVIATE_API_KEY exists and is not empty + if WEAVIATE_API_KEY: + connection_params['auth_credentials'] = weaviate.classes.init.Auth.api_key(WEAVIATE_API_KEY) + + self.client = weaviate.connect_to_custom(**connection_params) + self.client.connect() + except Exception as e: + raise ConnectionError(f'Failed to connect to Weaviate: {e}') from e + + def _sanitize_collection_name(self, collection_name: str) -> str: + """Sanitize collection name to be a valid Weaviate class name.""" + if not isinstance(collection_name, str) or not collection_name.strip(): + raise ValueError('Collection name must be a non-empty string') + + # Requirements for a valid Weaviate class name: + # The collection name must begin with a capital letter. + # The name can only contain letters, numbers, and the underscore (_) character. Spaces are not allowed. + + # Replace hyphens with underscores and keep only alphanumeric characters + name = re.sub(r'[^a-zA-Z0-9_]', '', collection_name.replace('-', '_')) + name = name.strip('_') + + if not name: + raise ValueError('Could not sanitize collection name to be a valid Weaviate class name') + + # Ensure it starts with a letter and is capitalized + if not name[0].isalpha(): + name = 'C' + name + + return name[0].upper() + name[1:] + + def has_collection(self, collection_name: str) -> bool: + sane_collection_name = self._sanitize_collection_name(collection_name) + return self.client.collections.exists(sane_collection_name) + + def delete_collection(self, collection_name: str) -> None: + sane_collection_name = self._sanitize_collection_name(collection_name) + if self.client.collections.exists(sane_collection_name): + self.client.collections.delete(sane_collection_name) + + def _create_collection(self, collection_name: str) -> None: + self.client.collections.create( + name=collection_name, + vector_config=weaviate.classes.config.Configure.Vectors.self_provided(), + properties=[ + weaviate.classes.config.Property(name='text', data_type=weaviate.classes.config.DataType.TEXT), + ], + ) + + def insert(self, collection_name: str, items: List[VectorItem]) -> None: + sane_collection_name = self._sanitize_collection_name(collection_name) + if not self.client.collections.exists(sane_collection_name): + self._create_collection(sane_collection_name) + + collection = self.client.collections.get(sane_collection_name) + + with collection.batch.fixed_size(batch_size=100) as batch: + for item in items: + item_uuid = str(uuid.uuid4()) if not item['id'] else str(item['id']) + + properties = {'text': item['text']} + if item['metadata']: + clean_metadata = _convert_uuids_to_strings(process_metadata(item['metadata'])) + clean_metadata.pop('text', None) + properties.update(clean_metadata) + + batch.add_object(properties=properties, uuid=item_uuid, vector=item['vector']) + + def upsert(self, collection_name: str, items: List[VectorItem]) -> None: + sane_collection_name = self._sanitize_collection_name(collection_name) + if not self.client.collections.exists(sane_collection_name): + self._create_collection(sane_collection_name) + + collection = self.client.collections.get(sane_collection_name) + + with collection.batch.fixed_size(batch_size=100) as batch: + for item in items: + item_uuid = str(item['id']) if item['id'] else None + + properties = {'text': item['text']} + if item['metadata']: + clean_metadata = _convert_uuids_to_strings(process_metadata(item['metadata'])) + clean_metadata.pop('text', None) + properties.update(clean_metadata) + + batch.add_object(properties=properties, uuid=item_uuid, vector=item['vector']) + + def search( + self, + collection_name: str, + vectors: List[List[Union[float, int]]], + filter: Optional[dict] = None, + limit: int = 10, + ) -> Optional[SearchResult]: + sane_collection_name = self._sanitize_collection_name(collection_name) + if not self.client.collections.exists(sane_collection_name): + return None + + collection = self.client.collections.get(sane_collection_name) + + result_ids, result_documents, result_metadatas, result_distances = ( + [], + [], + [], + [], + ) + + for vector_embedding in vectors: + try: + response = collection.query.near_vector( + near_vector=vector_embedding, + limit=limit, + return_metadata=weaviate.classes.query.MetadataQuery(distance=True), + ) + + ids = [str(obj.uuid) for obj in response.objects] + documents = [] + metadatas = [] + distances = [] + + for obj in response.objects: + properties = dict(obj.properties) if obj.properties else {} + documents.append(properties.pop('text', '')) + metadatas.append(_convert_uuids_to_strings(properties)) + + # Weaviate has cosine distance, 2 (worst) -> 0 (best). Re-ordering to 0 -> 1 + raw_distances = [ + (obj.metadata.distance if obj.metadata and obj.metadata.distance else 2.0) + for obj in response.objects + ] + distances = [(2 - dist) / 2 for dist in raw_distances] + + result_ids.append(ids) + result_documents.append(documents) + result_metadatas.append(metadatas) + result_distances.append(distances) + except Exception: + result_ids.append([]) + result_documents.append([]) + result_metadatas.append([]) + result_distances.append([]) + + return SearchResult( + **{ + 'ids': result_ids, + 'documents': result_documents, + 'metadatas': result_metadatas, + 'distances': result_distances, + } + ) + + def query(self, collection_name: str, filter: Dict, limit: Optional[int] = None) -> Optional[GetResult]: + sane_collection_name = self._sanitize_collection_name(collection_name) + if not self.client.collections.exists(sane_collection_name): + return None + + collection = self.client.collections.get(sane_collection_name) + + weaviate_filter = None + if filter: + for key, value in filter.items(): + prop_filter = weaviate.classes.query.Filter.by_property(name=key).equal(value) + weaviate_filter = ( + prop_filter + if weaviate_filter is None + else weaviate.classes.query.Filter.all_of([weaviate_filter, prop_filter]) + ) + + try: + response = collection.query.fetch_objects(filters=weaviate_filter, limit=limit) + + ids = [str(obj.uuid) for obj in response.objects] + documents = [] + metadatas = [] + + for obj in response.objects: + properties = dict(obj.properties) if obj.properties else {} + documents.append(properties.pop('text', '')) + metadatas.append(_convert_uuids_to_strings(properties)) + + return GetResult( + **{ + 'ids': [ids], + 'documents': [documents], + 'metadatas': [metadatas], + } + ) + except Exception: + return None + + def get(self, collection_name: str) -> Optional[GetResult]: + sane_collection_name = self._sanitize_collection_name(collection_name) + if not self.client.collections.exists(sane_collection_name): + return None + + collection = self.client.collections.get(sane_collection_name) + ids, documents, metadatas = [], [], [] + + try: + for item in collection.iterator(): + ids.append(str(item.uuid)) + properties = dict(item.properties) if item.properties else {} + documents.append(properties.pop('text', '')) + metadatas.append(_convert_uuids_to_strings(properties)) + + if not ids: + return None + + return GetResult( + **{ + 'ids': [ids], + 'documents': [documents], + 'metadatas': [metadatas], + } + ) + except Exception: + return None + + def delete( + self, + collection_name: str, + ids: Optional[List[str]] = None, + filter: Optional[Dict] = None, + ) -> None: + sane_collection_name = self._sanitize_collection_name(collection_name) + if not self.client.collections.exists(sane_collection_name): + return + + collection = self.client.collections.get(sane_collection_name) + + try: + if ids: + for item_id in ids: + collection.data.delete_by_id(uuid=item_id) + elif filter: + weaviate_filter = None + for key, value in filter.items(): + prop_filter = weaviate.classes.query.Filter.by_property(name=key).equal(value) + weaviate_filter = ( + prop_filter + if weaviate_filter is None + else weaviate.classes.query.Filter.all_of([weaviate_filter, prop_filter]) + ) + + if weaviate_filter: + collection.data.delete_many(where=weaviate_filter) + except Exception: + pass + + def reset(self) -> None: + try: + for collection_name in self.client.collections.list_all().keys(): + self.client.collections.delete(collection_name) + except Exception: + pass diff --git a/backend/open_webui/retrieval/vector/factory.py b/backend/open_webui/retrieval/vector/factory.py new file mode 100644 index 0000000000000000000000000000000000000000..8c0208fd4f28809e42d6e909497ee39813d58045 --- /dev/null +++ b/backend/open_webui/retrieval/vector/factory.py @@ -0,0 +1,87 @@ +from open_webui.retrieval.vector.main import VectorDBBase +from open_webui.retrieval.vector.type import VectorType +from open_webui.config import ( + VECTOR_DB, + ENABLE_QDRANT_MULTITENANCY_MODE, + ENABLE_MILVUS_MULTITENANCY_MODE, +) + + +class Vector: + @staticmethod + def get_vector(vector_type: str) -> VectorDBBase: + """ + get vector db instance by vector type + """ + match vector_type: + case VectorType.MILVUS: + if ENABLE_MILVUS_MULTITENANCY_MODE: + from open_webui.retrieval.vector.dbs.milvus_multitenancy import ( + MilvusClient, + ) + + return MilvusClient() + else: + from open_webui.retrieval.vector.dbs.milvus import MilvusClient + + return MilvusClient() + case VectorType.QDRANT: + if ENABLE_QDRANT_MULTITENANCY_MODE: + from open_webui.retrieval.vector.dbs.qdrant_multitenancy import ( + QdrantClient, + ) + + return QdrantClient() + else: + from open_webui.retrieval.vector.dbs.qdrant import QdrantClient + + return QdrantClient() + case VectorType.PINECONE: + from open_webui.retrieval.vector.dbs.pinecone import PineconeClient + + return PineconeClient() + case VectorType.S3VECTOR: + from open_webui.retrieval.vector.dbs.s3vector import S3VectorClient + + return S3VectorClient() + case VectorType.OPENSEARCH: + from open_webui.retrieval.vector.dbs.opensearch import OpenSearchClient + + return OpenSearchClient() + case VectorType.PGVECTOR: + from open_webui.retrieval.vector.dbs.pgvector import PgvectorClient + + return PgvectorClient() + case VectorType.OPENGAUSS: + from open_webui.retrieval.vector.dbs.opengauss import OpenGaussClient + + return OpenGaussClient() + case VectorType.MARIADB_VECTOR: + from open_webui.retrieval.vector.dbs.mariadb_vector import ( + MariaDBVectorClient, + ) + + return MariaDBVectorClient() + case VectorType.ELASTICSEARCH: + from open_webui.retrieval.vector.dbs.elasticsearch import ( + ElasticsearchClient, + ) + + return ElasticsearchClient() + case VectorType.CHROMA: + from open_webui.retrieval.vector.dbs.chroma import ChromaClient + + return ChromaClient() + case VectorType.ORACLE23AI: + from open_webui.retrieval.vector.dbs.oracle23ai import Oracle23aiClient + + return Oracle23aiClient() + case VectorType.WEAVIATE: + from open_webui.retrieval.vector.dbs.weaviate import WeaviateClient + + return WeaviateClient() + case _: + raise ValueError(f'Unsupported vector type: {vector_type}') + + +VECTOR_DB_CLIENT = Vector.get_vector(VECTOR_DB) diff --git a/backend/open_webui/retrieval/vector/main.py b/backend/open_webui/retrieval/vector/main.py new file mode 100644 index 0000000000000000000000000000000000000000..f7904baa20621aa2bea3ff975b51c75551791147 --- /dev/null +++ b/backend/open_webui/retrieval/vector/main.py @@ -0,0 +1,88 @@ +from pydantic import BaseModel +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Optional, Union + + +class VectorItem(BaseModel): + id: str + text: str + vector: List[float | int] + metadata: Any + + +class GetResult(BaseModel): + ids: Optional[List[List[str]]] + documents: Optional[List[List[str]]] + metadatas: Optional[List[List[Any]]] + + +class SearchResult(GetResult): + distances: Optional[List[List[float | int]]] + + +class VectorDBBase(ABC): + """ + Abstract base class for all vector database backends. + + Implementations of this class provide methods for collection management, + vector insertion, deletion, similarity search, and metadata filtering. + + Any custom vector database integration must inherit from this class and + implement all abstract methods. + """ + + @abstractmethod + def has_collection(self, collection_name: str) -> bool: + """Check if the collection exists in the vector DB.""" + pass + + @abstractmethod + def delete_collection(self, collection_name: str) -> None: + """Delete a collection from the vector DB.""" + pass + + @abstractmethod + def insert(self, collection_name: str, items: List[VectorItem]) -> None: + """Insert a list of vector items into a collection.""" + pass + + @abstractmethod + def upsert(self, collection_name: str, items: List[VectorItem]) -> None: + """Insert or update vector items in a collection.""" + pass + + @abstractmethod + def search( + self, + collection_name: str, + vectors: List[List[Union[float, int]]], + filter: Optional[Dict] = None, + limit: int = 10, + ) -> Optional[SearchResult]: + """Search for similar vectors in a collection.""" + pass + + @abstractmethod + def query(self, collection_name: str, filter: Dict, limit: Optional[int] = None) -> Optional[GetResult]: + """Query vectors from a collection using metadata filter.""" + pass + + @abstractmethod + def get(self, collection_name: str) -> Optional[GetResult]: + """Retrieve all vectors from a collection.""" + pass + + @abstractmethod + def delete( + self, + collection_name: str, + ids: Optional[List[str]] = None, + filter: Optional[Dict] = None, + ) -> None: + """Delete vectors by ID or filter from a collection.""" + pass + + @abstractmethod + def reset(self) -> None: + """Reset the vector database by removing all collections or those matching a condition.""" + pass diff --git a/backend/open_webui/retrieval/vector/type.py b/backend/open_webui/retrieval/vector/type.py new file mode 100644 index 0000000000000000000000000000000000000000..999aee9c54d9d3ec968b89c41cf2c1c9fccf36ca --- /dev/null +++ b/backend/open_webui/retrieval/vector/type.py @@ -0,0 +1,16 @@ +from enum import StrEnum + + +class VectorType(StrEnum): + MILVUS = 'milvus' + MARIADB_VECTOR = 'mariadb-vector' + QDRANT = 'qdrant' + CHROMA = 'chroma' + PINECONE = 'pinecone' + ELASTICSEARCH = 'elasticsearch' + OPENSEARCH = 'opensearch' + PGVECTOR = 'pgvector' + ORACLE23AI = 'oracle23ai' + S3VECTOR = 's3vector' + WEAVIATE = 'weaviate' + OPENGAUSS = 'opengauss' diff --git a/backend/open_webui/retrieval/vector/utils.py b/backend/open_webui/retrieval/vector/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..4915b024c307dcb969e0cd0f0c97807de4585855 --- /dev/null +++ b/backend/open_webui/retrieval/vector/utils.py @@ -0,0 +1,29 @@ +from datetime import datetime + +from open_webui.utils.misc import sanitize_text_for_db + +KEYS_TO_EXCLUDE = ['content', 'pages', 'tables', 'paragraphs', 'sections', 'figures'] + + +def filter_metadata(metadata: dict[str, any]) -> dict[str, any]: + # Removes large/redundant fields from metadata dict. + metadata = {key: value for key, value in metadata.items() if key not in KEYS_TO_EXCLUDE} + return metadata + + +def process_metadata( + metadata: dict[str, any], +) -> dict[str, any]: + # Removes large fields, converts non-serializable types (datetime, list, dict) to strings, + # and sanitizes strings for database storage (strips null bytes and invalid surrogates). + result = {} + for key, value in metadata.items(): + # Skip large fields + if key in KEYS_TO_EXCLUDE: + continue + # Convert non-serializable fields to strings + if isinstance(value, (datetime, list, dict)): + result[key] = sanitize_text_for_db(str(value)) + else: + result[key] = sanitize_text_for_db(value) + return result diff --git a/backend/open_webui/retrieval/web/azure.py b/backend/open_webui/retrieval/web/azure.py new file mode 100644 index 0000000000000000000000000000000000000000..4f74ecc9823065d2f863b6ee8b64b0715304ed56 --- /dev/null +++ b/backend/open_webui/retrieval/web/azure.py @@ -0,0 +1,123 @@ +import logging +from typing import Optional +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + +""" +Azure AI Search integration for Open WebUI. +Documentation: https://learn.microsoft.com/en-us/python/api/overview/azure/search-documents-readme?view=azure-python + +Required package: azure-search-documents +Install: pip install azure-search-documents +""" + + +def search_azure( + api_key: str, + endpoint: str, + index_name: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, +) -> list[SearchResult]: + """ + Search using Azure AI Search. + + Args: + api_key: Azure Search API key (query key or admin key) + endpoint: Azure Search service endpoint (e.g., https://myservice.search.windows.net) + index_name: Name of the search index to query + query: Search query string + count: Number of results to return + filter_list: Optional list of domains to filter results + + Returns: + List of SearchResult objects with link, title, and snippet + """ + try: + from azure.core.credentials import AzureKeyCredential + from azure.search.documents import SearchClient + except ImportError: + log.error( + 'azure-search-documents package is not installed. Install it with: pip install azure-search-documents' + ) + raise ImportError( + 'azure-search-documents is required for Azure AI Search. ' + 'Install it with: pip install azure-search-documents' + ) + + try: + # Create search client with API key authentication + credential = AzureKeyCredential(api_key) + search_client = SearchClient(endpoint=endpoint, index_name=index_name, credential=credential) + + # Perform the search + results = search_client.search(search_text=query, top=count) + + # Convert results to list and extract fields + search_results = [] + for result in results: + # Azure AI Search returns documents with custom schemas + # We need to extract common fields that might represent URL, title, and content + # Common field names to look for: + result_dict = dict(result) + + # Try to find URL field (common names) + link = ( + result_dict.get('url') + or result_dict.get('link') + or result_dict.get('uri') + or result_dict.get('metadata_storage_path') + or '' + ) + + # Try to find title field (common names) + title = ( + result_dict.get('title') + or result_dict.get('name') + or result_dict.get('metadata_title') + or result_dict.get('metadata_storage_name') + or None + ) + + # Try to find content/snippet field (common names) + snippet = ( + result_dict.get('content') + or result_dict.get('snippet') + or result_dict.get('description') + or result_dict.get('summary') + or result_dict.get('text') + or None + ) + + # Truncate snippet if too long + if snippet and len(snippet) > 500: + snippet = snippet[:497] + '...' + + if link: # Only add if we found a valid link + search_results.append( + { + 'link': link, + 'title': title, + 'snippet': snippet, + } + ) + + # Apply domain filtering if specified + if filter_list: + search_results = get_filtered_results(search_results, filter_list) + + # Convert to SearchResult objects + return [ + SearchResult( + link=result['link'], + title=result.get('title'), + snippet=result.get('snippet'), + ) + for result in search_results + ] + + except Exception as ex: + log.error(f'Azure AI Search error: {ex}') + raise ex diff --git a/backend/open_webui/retrieval/web/bing.py b/backend/open_webui/retrieval/web/bing.py new file mode 100644 index 0000000000000000000000000000000000000000..b7cfea89def80f018bd12a0eea36eba9cd23c487 --- /dev/null +++ b/backend/open_webui/retrieval/web/bing.py @@ -0,0 +1,67 @@ +import logging +import os +from pprint import pprint +from typing import Optional +import requests +from open_webui.retrieval.web.main import SearchResult, get_filtered_results +import argparse + +log = logging.getLogger(__name__) +""" +Documentation: https://docs.microsoft.com/en-us/bing/search-apis/bing-web-search/overview +""" + + +def search_bing( + subscription_key: str, + endpoint: str, + locale: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, +) -> list[SearchResult]: + mkt = locale + params = {'q': query, 'mkt': mkt, 'count': count} + headers = {'Ocp-Apim-Subscription-Key': subscription_key} + + try: + response = requests.get(endpoint, headers=headers, params=params) + response.raise_for_status() + json_response = response.json() + results = json_response.get('webPages', {}).get('value', []) + if filter_list: + results = get_filtered_results(results, filter_list) + return [ + SearchResult( + link=result['url'], + title=result.get('name'), + snippet=result.get('snippet'), + ) + for result in results + ] + except Exception as ex: + log.error(f'Error: {ex}') + raise ex + + +def main(): + parser = argparse.ArgumentParser(description='Search Bing from the command line.') + parser.add_argument( + 'query', + type=str, + default='Top 10 international news today', + help='The search query.', + ) + parser.add_argument('--count', type=int, default=10, help='Number of search results to return.') + parser.add_argument('--filter', nargs='*', help='List of filters to apply to the search results.') + parser.add_argument( + '--locale', + type=str, + default='en-US', + help='The locale to use for the search, maps to market in api', + ) + + args = parser.parse_args() + + results = search_bing(args.locale, args.query, args.count, args.filter) + pprint(results) diff --git a/backend/open_webui/retrieval/web/bocha.py b/backend/open_webui/retrieval/web/bocha.py new file mode 100644 index 0000000000000000000000000000000000000000..3557dcffb9840f8a61dbf364e5ac75ba039e9bb0 --- /dev/null +++ b/backend/open_webui/retrieval/web/bocha.py @@ -0,0 +1,56 @@ +import logging +from typing import Optional + +import requests +import json +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def _parse_response(response): + results = [] + if 'data' in response: + data = response['data'] + if 'webPages' in data: + webPages = data['webPages'] + if 'value' in webPages: + results = [ + { + 'id': item.get('id', ''), + 'name': item.get('name', ''), + 'url': item.get('url', ''), + 'snippet': item.get('snippet', ''), + 'summary': item.get('summary', ''), + 'siteName': item.get('siteName', ''), + 'siteIcon': item.get('siteIcon', ''), + 'datePublished': item.get('datePublished', '') or item.get('dateLastCrawled', ''), + } + for item in webPages['value'] + ] + return results + + +def search_bocha(api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None) -> list[SearchResult]: + """Search using Bocha's Search API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A Bocha Search API key + query (str): The query to search for + """ + url = 'https://api.bochaai.com/v1/web-search?utm_source=ollama' + headers = {'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'} + + payload = json.dumps({'query': query, 'summary': True, 'freshness': 'noLimit', 'count': count}) + + response = requests.post(url, headers=headers, data=payload, timeout=5) + response.raise_for_status() + results = _parse_response(response.json()) + + if filter_list: + results = get_filtered_results(results, filter_list) + + return [ + SearchResult(link=result['url'], title=result.get('name'), snippet=result.get('summary')) + for result in results[:count] + ] diff --git a/backend/open_webui/retrieval/web/brave.py b/backend/open_webui/retrieval/web/brave.py new file mode 100644 index 0000000000000000000000000000000000000000..9e663c2684cc0794dea2844422195a6234a9d54e --- /dev/null +++ b/backend/open_webui/retrieval/web/brave.py @@ -0,0 +1,49 @@ +import logging +import time +from typing import Optional + +import requests +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def search_brave(api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None) -> list[SearchResult]: + """Search using Brave's Search API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A Brave Search API key + query (str): The query to search for + """ + url = 'https://api.search.brave.com/res/v1/web/search' + headers = { + 'Accept': 'application/json', + 'Accept-Encoding': 'gzip', + 'X-Subscription-Token': api_key, + } + params = {'q': query, 'count': count} + + response = requests.get(url, headers=headers, params=params) + + # Handle 429 rate limiting - Brave free tier allows 1 request/second + # If rate limited, wait 1 second and retry once before failing + if response.status_code == 429: + log.info('Brave Search API rate limited (429), retrying after 1 second...') + time.sleep(1) + response = requests.get(url, headers=headers, params=params) + + response.raise_for_status() + + json_response = response.json() + results = json_response.get('web', {}).get('results', []) + if filter_list: + results = get_filtered_results(results, filter_list) + + return [ + SearchResult( + link=result['url'], + title=result.get('title'), + snippet=result.get('description'), + ) + for result in results[:count] + ] diff --git a/backend/open_webui/retrieval/web/duckduckgo.py b/backend/open_webui/retrieval/web/duckduckgo.py new file mode 100644 index 0000000000000000000000000000000000000000..da1c3f77ecc0cd9b484a5479384d9d8b395b9678 --- /dev/null +++ b/backend/open_webui/retrieval/web/duckduckgo.py @@ -0,0 +1,50 @@ +import logging +from typing import Optional + +from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from ddgs import DDGS +from ddgs.exceptions import RatelimitException + +log = logging.getLogger(__name__) + + +def search_duckduckgo( + query: str, + count: int, + filter_list: Optional[list[str]] = None, + concurrent_requests: Optional[int] = None, + backend: Optional[str] = 'auto', +) -> list[SearchResult]: + """ + Search using DuckDuckGo's Search API and return the results as a list of SearchResult objects. + Args: + query (str): The query to search for + count (int): The number of results to return + backend (str): The search backend to use (auto, duckduckgo, google, brave, etc.) + + Returns: + list[SearchResult]: A list of search results + """ + # Use the DDGS context manager to create a DDGS object + search_results = [] + with DDGS() as ddgs: + if concurrent_requests: + ddgs.threads = concurrent_requests + + # Use the ddgs.text() method to perform the search + try: + search_results = ddgs.text(query, safesearch='moderate', max_results=count, backend=backend) + except RatelimitException as e: + log.error(f'RatelimitException: {e}') + if filter_list: + search_results = get_filtered_results(search_results, filter_list) + + # Return the list of search results + return [ + SearchResult( + link=result['href'], + title=result.get('title'), + snippet=result.get('body'), + ) + for result in search_results + ] diff --git a/backend/open_webui/retrieval/web/exa.py b/backend/open_webui/retrieval/web/exa.py new file mode 100644 index 0000000000000000000000000000000000000000..860917854e1d110859aa662366f5365db744da89 --- /dev/null +++ b/backend/open_webui/retrieval/web/exa.py @@ -0,0 +1,72 @@ +import logging +from dataclasses import dataclass +from typing import Optional + +import requests +from open_webui.retrieval.web.main import SearchResult + +log = logging.getLogger(__name__) + +EXA_API_BASE = 'https://api.exa.ai' + + +@dataclass +class ExaResult: + url: str + title: str + text: str + + +def search_exa( + api_key: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, +) -> list[SearchResult]: + """Search using Exa Search API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A Exa Search API key + query (str): The query to search for + count (int): Number of results to return + filter_list (Optional[list[str]]): List of domains to filter results by + """ + log.info(f'Searching with Exa for query: {query}') + + headers = {'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'} + + payload = { + 'query': query, + 'numResults': count or 5, + 'includeDomains': filter_list, + 'contents': {'text': True, 'highlights': True}, + 'type': 'auto', # Use the auto search type (keyword or neural) + } + + try: + response = requests.post(f'{EXA_API_BASE}/search', headers=headers, json=payload) + response.raise_for_status() + data = response.json() + + results = [] + for result in data['results']: + results.append( + ExaResult( + url=result['url'], + title=result['title'], + text=result['text'], + ) + ) + + log.info(f'Found {len(results)} results') + return [ + SearchResult( + link=result.url, + title=result.title, + snippet=result.text, + ) + for result in results + ] + except Exception as e: + log.error(f'Error searching Exa: {e}') + return [] diff --git a/backend/open_webui/retrieval/web/external.py b/backend/open_webui/retrieval/web/external.py new file mode 100644 index 0000000000000000000000000000000000000000..7f5a2bf2aff336dc166e9dc358ab5f69d269df7c --- /dev/null +++ b/backend/open_webui/retrieval/web/external.py @@ -0,0 +1,60 @@ +import logging +from typing import Optional, List + +import requests + +from fastapi import Request + + +from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from open_webui.utils.headers import include_user_info_headers +from open_webui.env import FORWARD_SESSION_INFO_HEADER_CHAT_ID + +log = logging.getLogger(__name__) + + +def search_external( + request: Request, + external_url: str, + external_api_key: str, + query: str, + count: int, + filter_list: Optional[List[str]] = None, + user=None, +) -> List[SearchResult]: + try: + headers = { + 'User-Agent': 'Open WebUI (https://github.com/open-webui/open-webui) RAG Bot', + 'Authorization': f'Bearer {external_api_key}', + } + headers = include_user_info_headers(headers, user) + + chat_id = getattr(request.state, 'chat_id', None) + if chat_id: + headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = str(chat_id) + + response = requests.post( + external_url, + headers=headers, + json={ + 'query': query, + 'count': count, + }, + ) + response.raise_for_status() + results = response.json() + if filter_list: + results = get_filtered_results(results, filter_list) + results = [ + SearchResult( + link=result.get('link'), + title=result.get('title'), + snippet=result.get('snippet'), + ) + for result in results[:count] + ] + log.info(f'External search results: {results}') + return results + except Exception as e: + log.error(f'Error in External search: {e}') + return [] diff --git a/backend/open_webui/retrieval/web/firecrawl.py b/backend/open_webui/retrieval/web/firecrawl.py new file mode 100644 index 0000000000000000000000000000000000000000..4bbd4f212b6a7826be13823b5e81e49477e114a2 --- /dev/null +++ b/backend/open_webui/retrieval/web/firecrawl.py @@ -0,0 +1,229 @@ +from __future__ import annotations + +import logging +import time +from typing import TYPE_CHECKING, Any + +import requests +from langchain_core.documents import Document + +if TYPE_CHECKING: + from open_webui.retrieval.web.main import SearchResult + +log = logging.getLogger(__name__) + +DEFAULT_FIRECRAWL_API_BASE_URL = 'https://api.firecrawl.dev' +FIRECRAWL_RETRY_STATUS_CODES = {429, 500, 502, 503, 504} +FIRECRAWL_MAX_RETRIES = 2 + + +def build_firecrawl_url(base_url: str | None, path: str) -> str: + base_url = (base_url or DEFAULT_FIRECRAWL_API_BASE_URL).rstrip('/') + path = path.lstrip('/') + + if base_url.endswith('/v2'): + return f'{base_url}/{path}' + + return f'{base_url}/v2/{path}' + + +def build_firecrawl_headers(api_key: str | None) -> dict[str, str]: + return { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {api_key or ""}', + } + + +def get_firecrawl_timeout_seconds(timeout: Any) -> float | None: + if timeout in (None, ''): + return None + + try: + timeout = float(timeout) + except (TypeError, ValueError): + return None + + return timeout if timeout > 0 else None + + +def get_firecrawl_scrape_timeout_ms(timeout: Any) -> int | None: + timeout_seconds = get_firecrawl_timeout_seconds(timeout) + if timeout_seconds is None: + return None + + # Firecrawl v2 expects scrape timeouts in milliseconds. + return min(300000, max(1000, int(timeout_seconds * 1000))) + + +def get_firecrawl_client_timeout_seconds(timeout: Any, fallback: float = 60) -> float: + # Keep the local HTTP timeout slightly above Firecrawl's scrape timeout. + return (get_firecrawl_timeout_seconds(timeout) or fallback) + 10 + + +def get_firecrawl_retry_delay(headers: Any, attempt: int) -> float: + retry_after = headers.get('Retry-After') if headers else None + if retry_after: + try: + return min(10.0, max(0.0, float(retry_after))) + except (TypeError, ValueError): + pass + + return min(8.0, float(2**attempt)) + + +def request_firecrawl_json( + method: str, + url: str, + *, + headers: dict[str, str], + json: dict[str, Any] | None = None, + timeout: float | None = None, + verify: bool = True, +) -> dict[str, Any]: + last_error = None + + for attempt in range(FIRECRAWL_MAX_RETRIES + 1): + try: + response = requests.request( + method, + url, + headers=headers, + json=json, + timeout=timeout, + verify=verify, + ) + + if response.status_code in FIRECRAWL_RETRY_STATUS_CODES and attempt < FIRECRAWL_MAX_RETRIES: + delay = get_firecrawl_retry_delay(response.headers, attempt) + log.warning( + 'Firecrawl %s %s returned HTTP %s; retrying in %.1fs', + method, + url, + response.status_code, + delay, + ) + time.sleep(delay) + continue + + response.raise_for_status() + return response.json() + except (requests.ConnectionError, requests.Timeout) as e: + last_error = e + if attempt >= FIRECRAWL_MAX_RETRIES: + break + + delay = get_firecrawl_retry_delay(None, attempt) + log.warning('Firecrawl %s %s failed; retrying in %.1fs: %s', method, url, delay, e) + time.sleep(delay) + + if last_error: + raise last_error + + raise RuntimeError(f'Firecrawl {method} {url} failed without a response') + + +def get_firecrawl_result_url(result: dict[str, Any]) -> str: + metadata = result.get('metadata') or {} + return ( + result.get('url') + or result.get('link') + or metadata.get('url') + or metadata.get('sourceURL') + or metadata.get('source_url') + or '' + ) + + +def scrape_firecrawl_url( + firecrawl_url: str, + firecrawl_api_key: str, + url: str, + *, + verify_ssl: bool = True, + timeout: Any = None, + params: dict[str, Any] | None = None, +) -> Document | None: + payload = { + 'url': url, + 'formats': ['markdown'], + 'skipTlsVerification': not verify_ssl, + 'removeBase64Images': True, + **(params or {}), + } + scrape_timeout_ms = get_firecrawl_scrape_timeout_ms(timeout) + if scrape_timeout_ms is not None: + payload['timeout'] = scrape_timeout_ms + + response = request_firecrawl_json( + 'POST', + build_firecrawl_url(firecrawl_url, 'scrape'), + headers=build_firecrawl_headers(firecrawl_api_key), + json=payload, + timeout=get_firecrawl_client_timeout_seconds(timeout), + verify=verify_ssl, + ) + data = response.get('data') or {} + content = data.get('markdown') or '' + if not isinstance(content, str) or not content.strip(): + return None + + metadata = data.get('metadata') or {} + document_metadata = {'source': get_firecrawl_result_url(data) or url} + if metadata.get('title'): + document_metadata['title'] = metadata['title'] + if metadata.get('description'): + document_metadata['description'] = metadata['description'] + + return Document(page_content=content, metadata=document_metadata) + + +def search_firecrawl( + firecrawl_url: str, + firecrawl_api_key: str, + query: str, + count: int, + filter_list: list[str] | None = None, +) -> list[SearchResult]: + try: + response = request_firecrawl_json( + 'POST', + build_firecrawl_url(firecrawl_url, 'search'), + headers=build_firecrawl_headers(firecrawl_api_key), + json={ + 'query': query, + 'limit': count, + 'timeout': count * 3000, + 'ignoreInvalidURLs': True, + }, + timeout=count * 3 + 10, + ) + data = response.get('data') or {} + results = data.get('web') or [] + + if filter_list: + from open_webui.retrieval.web.main import get_filtered_results + + results = get_filtered_results(results, filter_list) + + from open_webui.retrieval.web.main import SearchResult + + search_results = [] + for result in results[:count]: + url = get_firecrawl_result_url(result) + if not url: + continue + + metadata = result.get('metadata') or {} + search_results.append( + SearchResult( + link=url, + title=result.get('title') or metadata.get('title'), + snippet=result.get('description') or result.get('snippet') or metadata.get('description'), + ) + ) + + log.info(f'FireCrawl search results: {search_results}') + return search_results + except Exception as e: + log.error(f'Error in FireCrawl search: {e}') + return [] diff --git a/backend/open_webui/retrieval/web/google_pse.py b/backend/open_webui/retrieval/web/google_pse.py new file mode 100644 index 0000000000000000000000000000000000000000..bb0a8526586558782aa7e41c1c16ac9af7b75bb5 --- /dev/null +++ b/backend/open_webui/retrieval/web/google_pse.py @@ -0,0 +1,70 @@ +import logging +from typing import Optional + +import requests +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def search_google_pse( + api_key: str, + search_engine_id: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, + referer: Optional[str] = None, +) -> list[SearchResult]: + """Search using Google's Programmable Search Engine API and return the results as a list of SearchResult objects. + Handles pagination for counts greater than 10. + + Args: + api_key (str): A Programmable Search Engine API key + search_engine_id (str): A Programmable Search Engine ID + query (str): The query to search for + count (int): The number of results to return (max 100, as PSE max results per query is 10 and max page is 10) + filter_list (Optional[list[str]], optional): A list of keywords to filter out from results. Defaults to None. + + Returns: + list[SearchResult]: A list of SearchResult objects. + """ + url = 'https://www.googleapis.com/customsearch/v1' + + headers = {'Content-Type': 'application/json'} + if referer: + headers['Referer'] = referer + + all_results = [] + start_index = 1 # Google PSE start parameter is 1-based + + while count > 0: + num_results_this_page = min(count, 10) # Google PSE max results per page is 10 + params = { + 'cx': search_engine_id, + 'q': query, + 'key': api_key, + 'num': num_results_this_page, + 'start': start_index, + } + response = requests.request('GET', url, headers=headers, params=params) + response.raise_for_status() + json_response = response.json() + results = json_response.get('items', []) + if results: # check if results are returned. If not, no more pages to fetch. + all_results.extend(results) + count -= len(results) # Decrement count by the number of results fetched in this page. + start_index += 10 # Increment start index for the next page + else: + break # No more results from Google PSE, break the loop + + if filter_list: + all_results = get_filtered_results(all_results, filter_list) + + return [ + SearchResult( + link=result['link'], + title=result.get('title'), + snippet=result.get('snippet'), + ) + for result in all_results + ] diff --git a/backend/open_webui/retrieval/web/jina_search.py b/backend/open_webui/retrieval/web/jina_search.py new file mode 100644 index 0000000000000000000000000000000000000000..b3266c47d09f7ba7a723d137233ac658d874f4c7 --- /dev/null +++ b/backend/open_webui/retrieval/web/jina_search.py @@ -0,0 +1,48 @@ +import logging + +import requests +from open_webui.retrieval.web.main import SearchResult +from yarl import URL + +log = logging.getLogger(__name__) + + +def search_jina(api_key: str, query: str, count: int, base_url: str = '') -> list[SearchResult]: + """ + Search using Jina's Search API and return the results as a list of SearchResult objects. + Args: + api_key (str): The Jina API key + query (str): The query to search for + count (int): The number of results to return + base_url (str): Optional custom base URL for the Jina API + + Returns: + list[SearchResult]: A list of search results + """ + jina_search_endpoint = base_url if base_url else 'https://s.jina.ai/' + + headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': api_key, + 'X-Retain-Images': 'none', + } + + payload = {'q': query, 'count': count if count <= 10 else 10} + + url = str(URL(jina_search_endpoint)) + response = requests.post(url, headers=headers, json=payload) + response.raise_for_status() + data = response.json() + + results = [] + for result in data['data']: + results.append( + SearchResult( + link=result['url'], + title=result.get('title'), + snippet=result.get('content'), + ) + ) + + return results diff --git a/backend/open_webui/retrieval/web/kagi.py b/backend/open_webui/retrieval/web/kagi.py new file mode 100644 index 0000000000000000000000000000000000000000..e6ed57001169b41891f19cd57df4b450b932f055 --- /dev/null +++ b/backend/open_webui/retrieval/web/kagi.py @@ -0,0 +1,42 @@ +import logging +from typing import Optional + +import requests +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def search_kagi(api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None) -> list[SearchResult]: + """Search using Kagi's Search API and return the results as a list of SearchResult objects. + + The Search API will inherit the settings in your account, including results personalization and snippet length. + + Args: + api_key (str): A Kagi Search API key + query (str): The query to search for + count (int): The number of results to return + """ + url = 'https://kagi.com/api/v0/search' + headers = { + 'Authorization': f'Bot {api_key}', + } + params = {'q': query, 'limit': count} + + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + json_response = response.json() + search_results = json_response.get('data', []) + + results = [ + SearchResult(link=result['url'], title=result['title'], snippet=result.get('snippet')) + for result in search_results + if result['t'] == 0 + ] + + print(results) + + if filter_list: + results = get_filtered_results(results, filter_list) + + return results diff --git a/backend/open_webui/retrieval/web/main.py b/backend/open_webui/retrieval/web/main.py new file mode 100644 index 0000000000000000000000000000000000000000..3a8fed52dd58651f2cb7cd96fae8517b2e99cf84 --- /dev/null +++ b/backend/open_webui/retrieval/web/main.py @@ -0,0 +1,46 @@ +import validators + +from typing import Optional +from urllib.parse import urlparse + +from pydantic import BaseModel + +from open_webui.retrieval.web.utils import resolve_hostname +from open_webui.utils.misc import is_string_allowed + + +def get_filtered_results(results, filter_list): + if not filter_list: + return results + + filtered_results = [] + + for result in results: + url = result.get('url') or result.get('link', '') or result.get('href', '') + if not validators.url(url): + continue + + domain = urlparse(url).netloc + if not domain: + continue + + hostnames = [domain] + + try: + ipv4_addresses, ipv6_addresses = resolve_hostname(domain) + hostnames.extend(ipv4_addresses) + hostnames.extend(ipv6_addresses) + except Exception: + pass + + if is_string_allowed(hostnames, filter_list): + filtered_results.append(result) + continue + + return filtered_results + + +class SearchResult(BaseModel): + link: str + title: Optional[str] + snippet: Optional[str] diff --git a/backend/open_webui/retrieval/web/mojeek.py b/backend/open_webui/retrieval/web/mojeek.py new file mode 100644 index 0000000000000000000000000000000000000000..a094ef6fc80baef912db7ebcb74284e317c566ec --- /dev/null +++ b/backend/open_webui/retrieval/web/mojeek.py @@ -0,0 +1,33 @@ +import logging +from typing import Optional + +import requests +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def search_mojeek(api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None) -> list[SearchResult]: + """Search using Mojeek's Search API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A Mojeek Search API key + query (str): The query to search for + """ + url = 'https://api.mojeek.com/search' + headers = { + 'Accept': 'application/json', + } + params = {'q': query, 'api_key': api_key, 'fmt': 'json', 't': count} + + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + json_response = response.json() + results = json_response.get('response', {}).get('results', []) + print(results) + if filter_list: + results = get_filtered_results(results, filter_list) + + return [ + SearchResult(link=result['url'], title=result.get('title'), snippet=result.get('desc')) for result in results + ] diff --git a/backend/open_webui/retrieval/web/ollama.py b/backend/open_webui/retrieval/web/ollama.py new file mode 100644 index 0000000000000000000000000000000000000000..7ed19b91b12079941a17a2f0ade8ca0f0a5fa34b --- /dev/null +++ b/backend/open_webui/retrieval/web/ollama.py @@ -0,0 +1,52 @@ +import logging +from dataclasses import dataclass +from typing import Optional + +import requests +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def search_ollama_cloud( + url: str, + api_key: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, +) -> list[SearchResult]: + """Search using Ollama Search API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A Ollama Search API key + query (str): The query to search for + count (int): Number of results to return + filter_list (Optional[list[str]]): List of domains to filter results by + """ + log.info(f'Searching with Ollama for query: {query}') + + headers = {'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'} + payload = {'query': query, 'max_results': count} + + try: + response = requests.post(f'{url}/api/web_search', headers=headers, json=payload) + response.raise_for_status() + data = response.json() + + results = data.get('results', []) + log.info(f'Found {len(results)} results') + + if filter_list: + results = get_filtered_results(results, filter_list) + + return [ + SearchResult( + link=result.get('url', ''), + title=result.get('title', ''), + snippet=result.get('content', ''), + ) + for result in results + ] + except Exception as e: + log.error(f'Error searching Ollama: {e}') + return [] diff --git a/backend/open_webui/retrieval/web/perplexity.py b/backend/open_webui/retrieval/web/perplexity.py new file mode 100644 index 0000000000000000000000000000000000000000..8b3a9d3b08606ce4b98f79f7975cfbb4c0c2e169 --- /dev/null +++ b/backend/open_webui/retrieval/web/perplexity.py @@ -0,0 +1,100 @@ +import logging +from typing import Optional, Literal +import requests + +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +MODELS = Literal[ + 'sonar', + 'sonar-pro', + 'sonar-reasoning', + 'sonar-reasoning-pro', + 'sonar-deep-research', +] +SEARCH_CONTEXT_USAGE_LEVELS = Literal['low', 'medium', 'high'] + + +log = logging.getLogger(__name__) + + +def search_perplexity( + api_key: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, + model: MODELS = 'sonar', + search_context_usage: SEARCH_CONTEXT_USAGE_LEVELS = 'medium', +) -> list[SearchResult]: + """Search using Perplexity API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A Perplexity API key + query (str): The query to search for + count (int): Maximum number of results to return + filter_list (Optional[list[str]]): List of domains to filter results + model (str): The Perplexity model to use (sonar, sonar-pro) + search_context_usage (str): Search context usage level (low, medium, high) + + """ + + # Handle PersistentConfig object + if hasattr(api_key, '__str__'): + api_key = str(api_key) + + try: + url = 'https://api.perplexity.ai/chat/completions' + + # Create payload for the API call + payload = { + 'model': model, + 'messages': [ + { + 'role': 'system', + 'content': 'You are a search assistant. Provide factual information with citations.', + }, + {'role': 'user', 'content': query}, + ], + 'temperature': 0.2, # Lower temperature for more factual responses + 'stream': False, + 'web_search_options': { + 'search_context_usage': search_context_usage, + }, + } + + headers = { + 'Authorization': f'Bearer {api_key}', + 'Content-Type': 'application/json', + } + + # Make the API request + response = requests.request('POST', url, json=payload, headers=headers) + + # Parse the JSON response + json_response = response.json() + + # Extract citations from the response + citations = json_response.get('citations', []) + + # Create search results from citations + results = [] + for i, citation in enumerate(citations[:count]): + # Extract content from the response to use as snippet + content = '' + if 'choices' in json_response and json_response['choices']: + if i == 0: + content = json_response['choices'][0]['message']['content'] + + result = {'link': citation, 'title': f'Source {i + 1}', 'snippet': content} + results.append(result) + + if filter_list: + results = get_filtered_results(results, filter_list) + + return [ + SearchResult(link=result['link'], title=result['title'], snippet=result['snippet']) + for result in results[:count] + ] + + except Exception as e: + log.error(f'Error searching with Perplexity API: {e}') + return [] diff --git a/backend/open_webui/retrieval/web/perplexity_search.py b/backend/open_webui/retrieval/web/perplexity_search.py new file mode 100644 index 0000000000000000000000000000000000000000..9cbec049d9b50a7ddd1e4c58f1e2314e73d89330 --- /dev/null +++ b/backend/open_webui/retrieval/web/perplexity_search.py @@ -0,0 +1,70 @@ +import logging +from typing import Optional, Literal +import requests + +from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from open_webui.utils.headers import include_user_info_headers + +log = logging.getLogger(__name__) + + +def search_perplexity_search( + api_key: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, + api_url: str = 'https://api.perplexity.ai/search', + user=None, +) -> list[SearchResult]: + """Search using Perplexity API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A Perplexity API key + query (str): The query to search for + count (int): Maximum number of results to return + filter_list (Optional[list[str]]): List of domains to filter results + api_url (str): Custom API URL (defaults to https://api.perplexity.ai/search) + user: Optional user object for forwarding user info headers + + """ + + # Handle PersistentConfig object + if hasattr(api_key, '__str__'): + api_key = str(api_key) + + if hasattr(api_url, '__str__'): + api_url = str(api_url) + + try: + url = api_url + + # Create payload for the API call + payload = { + 'query': query, + 'max_results': count, + } + + headers = { + 'Authorization': f'Bearer {api_key}', + 'Content-Type': 'application/json', + } + + # Forward user info headers if user is provided + if user is not None: + headers = include_user_info_headers(headers, user) + + # Make the API request + response = requests.request('POST', url, json=payload, headers=headers) + # Parse the JSON response + json_response = response.json() + + # Extract citations from the response + results = json_response.get('results', []) + + return [ + SearchResult(link=result['url'], title=result['title'], snippet=result['snippet']) for result in results + ] + + except Exception as e: + log.error(f'Error searching with Perplexity Search API: {e}') + return [] diff --git a/backend/open_webui/retrieval/web/searchapi.py b/backend/open_webui/retrieval/web/searchapi.py new file mode 100644 index 0000000000000000000000000000000000000000..855269ef02274881d15b264161b8c81c2e7b6e5b --- /dev/null +++ b/backend/open_webui/retrieval/web/searchapi.py @@ -0,0 +1,46 @@ +import logging +from typing import Optional +from urllib.parse import urlencode + +import requests +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def search_searchapi( + api_key: str, + engine: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, +) -> list[SearchResult]: + """Search using searchapi.io's API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A searchapi.io API key + query (str): The query to search for + """ + url = 'https://www.searchapi.io/api/v1/search' + + engine = engine or 'google' + + payload = {'engine': engine, 'q': query, 'api_key': api_key} + + url = f'{url}?{urlencode(payload)}' + response = requests.request('GET', url) + + json_response = response.json() + log.info(f'results from searchapi search: {json_response}') + + results = sorted(json_response.get('organic_results', []), key=lambda x: x.get('position', 0)) + if filter_list: + results = get_filtered_results(results, filter_list) + return [ + SearchResult( + link=result['link'], + title=result.get('title'), + snippet=result.get('snippet'), + ) + for result in results[:count] + ] diff --git a/backend/open_webui/retrieval/web/searxng.py b/backend/open_webui/retrieval/web/searxng.py new file mode 100644 index 0000000000000000000000000000000000000000..0335bea9a3cf8d35e8ca79c54c1733f0b37a5852 --- /dev/null +++ b/backend/open_webui/retrieval/web/searxng.py @@ -0,0 +1,87 @@ +import logging +from typing import Optional + +import requests +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def search_searxng( + query_url: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, + **kwargs, +) -> list[SearchResult]: + """ + Search a SearXNG instance for a given query and return the results as a list of SearchResult objects. + + The function allows passing additional parameters such as language or time_range to tailor the search result. + + Args: + query_url (str): The base URL of the SearXNG server. + query (str): The search term or question to find in the SearXNG database. + count (int): The maximum number of results to retrieve from the search. + + Keyword Args: + language (str): Language filter for the search results; e.g., "all", "en-US", "es". Defaults to "all". + safesearch (int): Safe search filter for safer web results; 0 = off, 1 = moderate, 2 = strict. Defaults to 1 (moderate). + time_range (str): Time range for filtering results by date; e.g., "2023-04-05..today" or "all-time". Defaults to ''. + categories: (Optional[list[str]]): Specific categories within which the search should be performed, defaulting to an empty string if not provided. + + Returns: + list[SearchResult]: A list of SearchResults sorted by relevance score in descending order. + + Raise: + requests.exceptions.RequestException: If a request error occurs during the search process. + """ + + # Default values for optional parameters are provided as empty strings or None when not specified. + language = kwargs.get('language', 'all') + safesearch = kwargs.get('safesearch', '1') + time_range = kwargs.get('time_range', '') + categories = ''.join(kwargs.get('categories', [])) + + params = { + 'q': query, + 'format': 'json', + 'pageno': 1, + 'safesearch': safesearch, + 'language': language, + 'time_range': time_range, + 'categories': categories, + 'theme': 'simple', + 'image_proxy': 0, + } + + # Legacy query format + if '' in query_url: + # Strip all query parameters from the URL + query_url = query_url.split('?')[0] + + log.debug(f'searching {query_url}') + + response = requests.get( + query_url, + headers={ + 'User-Agent': 'Open WebUI (https://github.com/open-webui/open-webui) RAG Bot', + 'Accept': 'text/html', + 'Accept-Encoding': 'gzip, deflate', + 'Accept-Language': 'en-US,en;q=0.5', + 'Connection': 'keep-alive', + }, + params=params, + ) + + response.raise_for_status() # Raise an exception for HTTP errors. + + json_response = response.json() + results = json_response.get('results', []) + sorted_results = sorted(results, key=lambda x: x.get('score', 0), reverse=True) + if filter_list: + sorted_results = get_filtered_results(sorted_results, filter_list) + return [ + SearchResult(link=result['url'], title=result.get('title'), snippet=result.get('content')) + for result in sorted_results[:count] + ] diff --git a/backend/open_webui/retrieval/web/serpapi.py b/backend/open_webui/retrieval/web/serpapi.py new file mode 100644 index 0000000000000000000000000000000000000000..602f60d7a79fed73bbae79bf4d9d3e4ab1249705 --- /dev/null +++ b/backend/open_webui/retrieval/web/serpapi.py @@ -0,0 +1,46 @@ +import logging +from typing import Optional +from urllib.parse import urlencode + +import requests +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def search_serpapi( + api_key: str, + engine: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, +) -> list[SearchResult]: + """Search using serpapi.com's API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A serpapi.com API key + query (str): The query to search for + """ + url = 'https://serpapi.com/search' + + engine = engine or 'google' + + payload = {'engine': engine, 'q': query, 'api_key': api_key} + + url = f'{url}?{urlencode(payload)}' + response = requests.request('GET', url) + + json_response = response.json() + log.info(f'results from serpapi search: {json_response}') + + results = sorted(json_response.get('organic_results', []), key=lambda x: x.get('position', 0)) + if filter_list: + results = get_filtered_results(results, filter_list) + return [ + SearchResult( + link=result['link'], + title=result.get('title'), + snippet=result.get('snippet'), + ) + for result in results[:count] + ] diff --git a/backend/open_webui/retrieval/web/serper.py b/backend/open_webui/retrieval/web/serper.py new file mode 100644 index 0000000000000000000000000000000000000000..9f1a8e1b3a0731d7a50f10f38530f0e35c33489b --- /dev/null +++ b/backend/open_webui/retrieval/web/serper.py @@ -0,0 +1,37 @@ +import json +import logging +from typing import Optional + +import requests +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def search_serper(api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None) -> list[SearchResult]: + """Search using serper.dev's API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A serper.dev API key + query (str): The query to search for + """ + url = 'https://google.serper.dev/search' + + payload = json.dumps({'q': query}) + headers = {'X-API-KEY': api_key, 'Content-Type': 'application/json'} + + response = requests.request('POST', url, headers=headers, data=payload) + response.raise_for_status() + + json_response = response.json() + results = sorted(json_response.get('organic', []), key=lambda x: x.get('position', 0)) + if filter_list: + results = get_filtered_results(results, filter_list) + return [ + SearchResult( + link=result['link'], + title=result.get('title'), + snippet=result.get('snippet'), + ) + for result in results[:count] + ] diff --git a/backend/open_webui/retrieval/web/serply.py b/backend/open_webui/retrieval/web/serply.py new file mode 100644 index 0000000000000000000000000000000000000000..f245392b75d6020e2cfe0b9da131401adaf8c62e --- /dev/null +++ b/backend/open_webui/retrieval/web/serply.py @@ -0,0 +1,65 @@ +import logging +from typing import Optional +from urllib.parse import urlencode + +import requests +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def search_serply( + api_key: str, + query: str, + count: int, + hl: str = 'us', + limit: int = 10, + device_type: str = 'desktop', + proxy_location: str = 'US', + filter_list: Optional[list[str]] = None, +) -> list[SearchResult]: + """Search using serper.dev's API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A serply.io API key + query (str): The query to search for + hl (str): Host Language code to display results in (reference https://developers.google.com/custom-search/docs/xml_results?hl=en#wsInterfaceLanguages) + limit (int): The maximum number of results to return [10-100, defaults to 10] + """ + log.info('Searching with Serply') + + url = 'https://api.serply.io/v1/search/' + + query_payload = { + 'q': query, + 'language': 'en', + 'num': limit, + 'gl': proxy_location.upper(), + 'hl': hl.lower(), + } + + url = f'{url}{urlencode(query_payload)}' + headers = { + 'X-API-KEY': api_key, + 'X-User-Agent': device_type, + 'User-Agent': 'open-webui', + 'X-Proxy-Location': proxy_location, + } + + response = requests.request('GET', url, headers=headers) + response.raise_for_status() + + json_response = response.json() + log.info(f'results from serply search: {json_response}') + + results = sorted(json_response.get('results', []), key=lambda x: x.get('realPosition', 0)) + if filter_list: + results = get_filtered_results(results, filter_list) + return [ + SearchResult( + link=result['link'], + title=result.get('title'), + snippet=result.get('description'), + ) + for result in results[:count] + ] diff --git a/backend/open_webui/retrieval/web/serpstack.py b/backend/open_webui/retrieval/web/serpstack.py new file mode 100644 index 0000000000000000000000000000000000000000..28a49566458ec2e0d6f7967734954e7f4dcf0dcb --- /dev/null +++ b/backend/open_webui/retrieval/web/serpstack.py @@ -0,0 +1,42 @@ +import logging +from typing import Optional + +import requests +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def search_serpstack( + api_key: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, + https_enabled: bool = True, +) -> list[SearchResult]: + """Search using serpstack.com's and return the results as a list of SearchResult objects. + + Args: + api_key (str): A serpstack.com API key + query (str): The query to search for + https_enabled (bool): Whether to use HTTPS or HTTP for the API request + """ + url = f'{"https" if https_enabled else "http"}://api.serpstack.com/search' + + headers = {'Content-Type': 'application/json'} + params = { + 'access_key': api_key, + 'query': query, + } + + response = requests.request('POST', url, headers=headers, params=params) + response.raise_for_status() + + json_response = response.json() + results = sorted(json_response.get('organic_results', []), key=lambda x: x.get('position', 0)) + if filter_list: + results = get_filtered_results(results, filter_list) + return [ + SearchResult(link=result['url'], title=result.get('title'), snippet=result.get('snippet')) + for result in results[:count] + ] diff --git a/backend/open_webui/retrieval/web/sougou.py b/backend/open_webui/retrieval/web/sougou.py new file mode 100644 index 0000000000000000000000000000000000000000..b267374d797b0f53b4f99d96a8370d883a6b252c --- /dev/null +++ b/backend/open_webui/retrieval/web/sougou.py @@ -0,0 +1,51 @@ +import logging +import json +from typing import Optional, List + + +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def search_sougou( + sougou_api_sid: str, + sougou_api_sk: str, + query: str, + count: int, + filter_list: Optional[List[str]] = None, +) -> List[SearchResult]: + from tencentcloud.common.common_client import CommonClient + from tencentcloud.common import credential + from tencentcloud.common.exception.tencent_cloud_sdk_exception import ( + TencentCloudSDKException, + ) + from tencentcloud.common.profile.client_profile import ClientProfile + from tencentcloud.common.profile.http_profile import HttpProfile + + try: + cred = credential.Credential(sougou_api_sid, sougou_api_sk) + http_profile = HttpProfile() + http_profile.endpoint = 'tms.tencentcloudapi.com' + client_profile = ClientProfile() + client_profile.http_profile = http_profile + params = json.dumps({'Query': query, 'Cnt': 20}) + common_client = CommonClient('tms', '2020-12-29', cred, '', profile=client_profile) + results = [ + json.loads(page) for page in common_client.call_json('SearchPro', json.loads(params))['Response']['Pages'] + ] + sorted_results = sorted(results, key=lambda x: x.get('scour', 0.0), reverse=True) + if filter_list: + sorted_results = get_filtered_results(sorted_results, filter_list) + + return [ + SearchResult( + link=result.get('url'), + title=result.get('title'), + snippet=result.get('passage'), + ) + for result in sorted_results[:count] + ] + except TencentCloudSDKException as err: + log.error(f'Error in Sougou search: {err}') + return [] diff --git a/backend/open_webui/retrieval/web/tavily.py b/backend/open_webui/retrieval/web/tavily.py new file mode 100644 index 0000000000000000000000000000000000000000..6b52bbb45b9d29ea709dcdac35104336c80cb21b --- /dev/null +++ b/backend/open_webui/retrieval/web/tavily.py @@ -0,0 +1,49 @@ +import logging +from typing import Optional + +import requests +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def search_tavily( + api_key: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, + # **kwargs, +) -> list[SearchResult]: + """Search using Tavily's Search API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A Tavily Search API key + query (str): The query to search for + count (int): The maximum number of results to return + + Returns: + list[SearchResult]: A list of search results + """ + url = 'https://api.tavily.com/search' + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {api_key}', + } + data = {'query': query, 'max_results': count} + response = requests.post(url, headers=headers, json=data) + response.raise_for_status() + + json_response = response.json() + + results = json_response.get('results', []) + if filter_list: + results = get_filtered_results(results, filter_list) + + return [ + SearchResult( + link=result['url'], + title=result.get('title', ''), + snippet=result.get('content'), + ) + for result in results + ] diff --git a/backend/open_webui/retrieval/web/testdata/bing.json b/backend/open_webui/retrieval/web/testdata/bing.json new file mode 100644 index 0000000000000000000000000000000000000000..80324f3b40fa5f8504d027ca028ab103b21f45fd --- /dev/null +++ b/backend/open_webui/retrieval/web/testdata/bing.json @@ -0,0 +1,58 @@ +{ + "_type": "SearchResponse", + "queryContext": { + "originalQuery": "Top 10 international results" + }, + "webPages": { + "webSearchUrl": "https://www.bing.com/search?q=Top+10+international+results", + "totalEstimatedMatches": 687, + "value": [ + { + "id": "https://api.bing.microsoft.com/api/v7/#WebPages.0", + "name": "2024 Mexican Grand Prix - F1 results and latest standings ... - PlanetF1", + "url": "https://www.planetf1.com/news/f1-results-2024-mexican-grand-prix-race-standings", + "datePublished": "2024-10-27T00:00:00.0000000", + "datePublishedFreshnessText": "1 day ago", + "isFamilyFriendly": true, + "displayUrl": "https://www.planetf1.com/news/f1-results-2024-mexican-grand-prix-race-standings", + "snippet": "Nico Hulkenberg and Pierre Gasly completed the top 10. A full report of the Mexican Grand Prix is available at the bottom of this article. F1 results – 2024 Mexican Grand Prix", + "dateLastCrawled": "2024-10-28T07:15:00.0000000Z", + "cachedPageUrl": "https://cc.bingj.com/cache.aspx?q=Top+10+international+results&d=916492551782&mkt=en-US&setlang=en-US&w=zBsfaAPyF2tUrHFHr_vFFdUm8sng4g34", + "language": "en", + "isNavigational": false, + "noCache": false + }, + { + "id": "https://api.bing.microsoft.com/api/v7/#WebPages.1", + "name": "F1 Results Today: HUGE Verstappen penalties cause major title change", + "url": "https://www.gpfans.com/en/f1-news/1033512/f1-results-today-mexican-grand-prix-huge-max-verstappen-penalties-cause-major-title-change/", + "datePublished": "2024-10-27T00:00:00.0000000", + "datePublishedFreshnessText": "1 day ago", + "isFamilyFriendly": true, + "displayUrl": "https://www.gpfans.com/en/f1-news/1033512/f1-results-today-mexican-grand-prix-huge-max...", + "snippet": "Elsewhere, Mercedes duo Lewis Hamilton and George Russell came home in P4 and P5 respectively. Meanwhile, the surprise package of the day were Haas, with both Kevin Magnussen and Nico Hulkenberg finishing inside the points.. READ MORE: RB star issues apology after red flag CRASH at Mexican GP Mexican Grand Prix 2024 results. 1. Carlos Sainz [Ferrari] 2. Lando Norris [McLaren] - +4.705", + "dateLastCrawled": "2024-10-28T06:06:00.0000000Z", + "cachedPageUrl": "https://cc.bingj.com/cache.aspx?q=Top+10+international+results&d=2840656522642&mkt=en-US&setlang=en-US&w=-Tbkwxnq52jZCvG7l3CtgcwT1vwAjIUD", + "language": "en", + "isNavigational": false, + "noCache": false + }, + { + "id": "https://api.bing.microsoft.com/api/v7/#WebPages.2", + "name": "International Power Rankings: England flying, Kangaroos cruising, Fiji rise", + "url": "https://www.loverugbyleague.com/post/international-power-rankings-england-flying-kangaroos-cruising-fiji-rise", + "datePublished": "2024-10-28T00:00:00.0000000", + "datePublishedFreshnessText": "7 hours ago", + "isFamilyFriendly": true, + "displayUrl": "https://www.loverugbyleague.com/post/international-power-rankings-england-flying...", + "snippet": "LRL RECOMMENDS: England player ratings from first Test against Samoa as omnificent George Williams scores perfect 10. 2. Australia (Men) – SAME. The Kangaroos remain 2nd in our Power Rankings after their 22-10 win against New Zealand in Christchurch on Sunday. As was the case in their win against Tonga last week, Mal Meninga’s side weren ...", + "dateLastCrawled": "2024-10-28T07:09:00.0000000Z", + "cachedPageUrl": "https://cc.bingj.com/cache.aspx?q=Top+10+international+results&d=1535008462672&mkt=en-US&setlang=en-US&w=82ujhH4Kp0iuhCS7wh1xLUFYUeetaVVm", + "language": "en", + "isNavigational": false, + "noCache": false + } + ], + "someResultsRemoved": true + } +} diff --git a/backend/open_webui/retrieval/web/testdata/brave.json b/backend/open_webui/retrieval/web/testdata/brave.json new file mode 100644 index 0000000000000000000000000000000000000000..0cc72109efce3a8be3eaf50fc899806b8b04db7b --- /dev/null +++ b/backend/open_webui/retrieval/web/testdata/brave.json @@ -0,0 +1,998 @@ +{ + "query": { + "original": "python", + "show_strict_warning": false, + "is_navigational": true, + "is_news_breaking": false, + "spellcheck_off": true, + "country": "us", + "bad_results": false, + "should_fallback": false, + "postal_code": "", + "city": "", + "header_country": "", + "more_results_available": true, + "state": "" + }, + "mixed": { + "type": "mixed", + "main": [ + { + "type": "web", + "index": 0, + "all": false + }, + { + "type": "web", + "index": 1, + "all": false + }, + { + "type": "news", + "all": true + }, + { + "type": "web", + "index": 2, + "all": false + }, + { + "type": "videos", + "all": true + }, + { + "type": "web", + "index": 3, + "all": false + }, + { + "type": "web", + "index": 4, + "all": false + }, + { + "type": "web", + "index": 5, + "all": false + }, + { + "type": "web", + "index": 6, + "all": false + }, + { + "type": "web", + "index": 7, + "all": false + }, + { + "type": "web", + "index": 8, + "all": false + }, + { + "type": "web", + "index": 9, + "all": false + }, + { + "type": "web", + "index": 10, + "all": false + }, + { + "type": "web", + "index": 11, + "all": false + }, + { + "type": "web", + "index": 12, + "all": false + }, + { + "type": "web", + "index": 13, + "all": false + }, + { + "type": "web", + "index": 14, + "all": false + }, + { + "type": "web", + "index": 15, + "all": false + }, + { + "type": "web", + "index": 16, + "all": false + }, + { + "type": "web", + "index": 17, + "all": false + }, + { + "type": "web", + "index": 18, + "all": false + }, + { + "type": "web", + "index": 19, + "all": false + } + ], + "top": [], + "side": [] + }, + "news": { + "type": "news", + "results": [ + { + "title": "Google lays off staff from Flutter, Dart and Python teams weeks before its developer conference | TechCrunch", + "url": "https://techcrunch.com/2024/05/01/google-lays-off-staff-from-flutter-dart-python-weeks-before-its-developer-conference/", + "is_source_local": false, + "is_source_both": false, + "description": "Google told TechCrunch that Flutter will have new updates to share at I/O this year.", + "page_age": "2024-05-02T17:40:05", + "family_friendly": true, + "meta_url": { + "scheme": "https", + "netloc": "techcrunch.com", + "hostname": "techcrunch.com", + "favicon": "https://imgs.search.brave.com/N6VSEVahheQOb7lqfb47dhUOB4XD-6sfQOP94sCe3Oo/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGI5Njk0Yzlk/YWM3ZWMwZjg1MTM1/NmIyMWEyNzBjZDZj/ZDQyNmFlNGU0NDRi/MDgyYjQwOGU0Y2Qy/ZWMwNWQ2ZC90ZWNo/Y3J1bmNoLmNvbS8", + "path": "› 2024 › 05 › 01 › google-lays-off-staff-from-flutter-dart-python-weeks-before-its-developer-conference" + }, + "breaking": false, + "thumbnail": { + "src": "https://imgs.search.brave.com/gCI5UG8muOEOZDAx9vpu6L6r6R00mD7jOF08-biFoyQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly90ZWNo/Y3J1bmNoLmNvbS93/cC1jb250ZW50L3Vw/bG9hZHMvMjAxOC8x/MS9HZXR0eUltYWdl/cy0xMDAyNDg0NzQ2/LmpwZz9yZXNpemU9/MTIwMCw4MDA" + }, + "age": "3 days ago", + "extra_snippets": [ + "Ahead of Google’s annual I/O developer conference in May, the tech giant has laid off staff across key teams like Flutter, Dart, Python and others, according to reports from affected employees shared on social media. Google confirmed the layoffs to TechCrunch, but not the specific teams, roles or how many people were let go.", + "In a separate post on Reddit, another commenter noted the Python team affected by the layoffs were those who managed the internal Python runtimes and toolchains and worked with OSS Python. Included in this group were “multiple current and former core devs and steering council members,” they said.", + "Meanwhile, others shared on Y Combinator’s Hacker News, where a Python team member detailed their specific duties on the technical front and noted that, for years, much of the work was done with fewer than 10 people. Another Hacker News commenter said their early years on the Python team were spent paying down internal technical debt accumulated from not having a strong Python strategy.", + "CNBC reports that a total of 200 people were let go across Google’s “Core” teams, which included those working on Python, app platforms, and other engineering roles. Some jobs were being shifted to India and Mexico, it said, citing internal documents." + ] + } + ], + "mutated_by_goggles": false + }, + "type": "search", + "videos": { + "type": "videos", + "results": [ + { + "type": "video_result", + "url": "https://www.youtube.com/watch?v=b093aqAZiPU", + "title": "👩‍💻 Python for Beginners Tutorial - YouTube", + "description": "In this step-by-step Python for beginner's tutorial, learn how you can get started programming in Python. In this video, I assume that you are completely new...", + "age": "March 25, 2021", + "page_age": "2021-03-25T10:00:08", + "video": {}, + "meta_url": { + "scheme": "https", + "netloc": "youtube.com", + "hostname": "www.youtube.com", + "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v", + "path": "› watch" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/tZI4Do4_EYcTCsD_MvE3Jx8FzjIXwIJ5ZuKhwiWTyZs/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9i/MDkzYXFBWmlQVS9t/YXhyZXNkZWZhdWx0/LmpwZw" + } + }, + { + "type": "video_result", + "url": "https://www.youtube.com/watch?v=rfscVS0vtbw", + "title": "Learn Python - Full Course for Beginners [Tutorial] - YouTube", + "description": "This course will give you a full introduction into all of the core concepts in python. Follow along with the videos and you'll be a python programmer in no t...", + "age": "July 11, 2018", + "page_age": "2018-07-11T18:00:42", + "video": {}, + "meta_url": { + "scheme": "https", + "netloc": "youtube.com", + "hostname": "www.youtube.com", + "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v", + "path": "› watch" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/65zkx_kPU_zJb-4nmvvY-q5-ZZwzceChz-N00V8cqvk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9y/ZnNjVlMwdnRidy9t/YXhyZXNkZWZhdWx0/LmpwZw" + } + }, + { + "type": "video_result", + "url": "https://www.youtube.com/watch?v=_uQrJ0TkZlc", + "title": "Python Tutorial - Python Full Course for Beginners - YouTube", + "description": "Become a Python pro! 🚀 This comprehensive tutorial takes you from beginner to hero, covering the basics, machine learning, and web development projects.🚀 W...", + "age": "February 18, 2019", + "page_age": "2019-02-18T15:00:08", + "video": {}, + "meta_url": { + "scheme": "https", + "netloc": "youtube.com", + "hostname": "www.youtube.com", + "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v", + "path": "› watch" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/Djiv1pXLq1ClqBSE_86jQnEYR8bW8UJP6Cs7LrgyQzQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9f/dVFySjBUa1psYy9t/YXhyZXNkZWZhdWx0/LmpwZw" + } + }, + { + "type": "video_result", + "url": "https://www.youtube.com/watch?v=wRKgzC-MhIc", + "title": "[] and {} vs list() and dict(), which is better?", + "description": "Enjoy the videos and music you love, upload original content, and share it all with friends, family, and the world on YouTube.", + "video": {}, + "meta_url": { + "scheme": "https", + "netloc": "youtube.com", + "hostname": "www.youtube.com", + "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v", + "path": "› watch" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/Hw9ep2Pio13X1VZjRw_h9R2VH_XvZFOuGlQJVnVkeq0/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS93/UktnekMtTWhJYy9o/cWRlZmF1bHQuanBn" + } + }, + { + "type": "video_result", + "url": "https://www.youtube.com/watch?v=LWdsF79H1Pg", + "title": "print() vs. return in Python Functions - YouTube", + "description": "In this video, you will learn the differences between the return statement and the print function when they are used inside Python functions. We will see an ...", + "age": "June 11, 2022", + "page_age": "2022-06-11T21:33:26", + "video": {}, + "meta_url": { + "scheme": "https", + "netloc": "youtube.com", + "hostname": "www.youtube.com", + "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v", + "path": "› watch" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/ebglnr5_jwHHpvon3WU-5hzt0eHdTZSVGg3Ts6R38xY/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9M/V2RzRjc5SDFQZy9t/YXhyZXNkZWZhdWx0/LmpwZw" + } + }, + { + "type": "video_result", + "url": "https://www.youtube.com/watch?v=AovxLr8jUH4", + "title": "Python Tutorial for Beginners 5 - Python print() and input() Function ...", + "description": "In this Video I am going to show How to use print() Function and input() Function in Python. In python The print() function is used to print the specified ...", + "age": "August 28, 2018", + "page_age": "2018-08-28T20:11:09", + "video": {}, + "meta_url": { + "scheme": "https", + "netloc": "youtube.com", + "hostname": "www.youtube.com", + "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v", + "path": "› watch" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/nCoLEcWkKtiecprWbS6nufwGCaSbPH7o0-sMeIkFmjI/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9B/b3Z4THI4alVINC9o/cWRlZmF1bHQuanBn" + } + } + ], + "mutated_by_goggles": false + }, + "web": { + "type": "search", + "results": [ + { + "title": "Welcome to Python.org", + "url": "https://www.python.org", + "is_source_local": false, + "is_source_both": false, + "description": "The official home of the Python Programming Language", + "page_age": "2023-09-09T15:55:05", + "profile": { + "name": "Python", + "url": "https://www.python.org", + "long_name": "python.org", + "img": "https://imgs.search.brave.com/vBaRH-v6oPS4csO4cdvuKhZ7-xDVvydin3oe3zXYxAI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNTJjMzZjNDBj/MmIzODgwMGUyOTRj/Y2E5MjM3YjRkYTZj/YWI1Yzk1NTlmYTgw/ZDBjNzM0MGMxZjQz/YWFjNTczYy93d3cu/cHl0aG9uLm9yZy8" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "python.org", + "hostname": "www.python.org", + "favicon": "https://imgs.search.brave.com/vBaRH-v6oPS4csO4cdvuKhZ7-xDVvydin3oe3zXYxAI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNTJjMzZjNDBj/MmIzODgwMGUyOTRj/Y2E5MjM3YjRkYTZj/YWI1Yzk1NTlmYTgw/ZDBjNzM0MGMxZjQz/YWFjNTczYy93d3cu/cHl0aG9uLm9yZy8", + "path": "" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/GGfNfe5rxJ8QWEoxXniSLc0-POLU3qPyTIpuqPdbmXk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/cHl0aG9uLm9yZy9z/dGF0aWMvb3Blbmdy/YXBoLWljb24tMjAw/eDIwMC5wbmc", + "original": "https://www.python.org/static/opengraph-icon-200x200.png", + "logo": false + }, + "age": "September 9, 2023", + "cluster_type": "generic", + "cluster": [ + { + "title": "Downloads", + "url": "https://www.python.org/downloads/", + "is_source_local": false, + "is_source_both": false, + "description": "The official home of the Python Programming Language", + "family_friendly": true + }, + { + "title": "Macos", + "url": "https://www.python.org/downloads/macos/", + "is_source_local": false, + "is_source_both": false, + "description": "The official home of the Python Programming Language", + "family_friendly": true + }, + { + "title": "Windows", + "url": "https://www.python.org/downloads/windows/", + "is_source_local": false, + "is_source_both": false, + "description": "The official home of the Python Programming Language", + "family_friendly": true + }, + { + "title": "Getting Started", + "url": "https://www.python.org/about/gettingstarted/", + "is_source_local": false, + "is_source_both": false, + "description": "The official home of the Python Programming Language", + "family_friendly": true + } + ], + "extra_snippets": [ + "Calculations are simple with Python, and expression syntax is straightforward: the operators +, -, * and / work as expected; parentheses () can be used for grouping. More about simple math functions in Python 3.", + "The core of extensible programming is defining functions. Python allows mandatory and optional arguments, keyword arguments, and even arbitrary argument lists. More about defining functions in Python 3", + "Lists (known as arrays in other languages) are one of the compound data types that Python understands. Lists can be indexed, sliced and manipulated with other built-in functions. More about lists in Python 3", + "# Python 3: Simple output (with Unicode) >>> print(\"Hello, I'm Python!\") Hello, I'm Python! # Input, assignment >>> name = input('What is your name?\\n') >>> print('Hi, %s.' % name) What is your name? Python Hi, Python." + ] + }, + { + "title": "Python (programming language) - Wikipedia", + "url": "https://en.wikipedia.org/wiki/Python_(programming_language)", + "is_source_local": false, + "is_source_both": false, + "description": "Python is a high-level, general-purpose programming language. Its design philosophy emphasizes code readability with the use of significant indentation. Python is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), ...", + "page_age": "2024-05-01T12:54:03", + "profile": { + "name": "Wikipedia", + "url": "https://en.wikipedia.org/wiki/Python_(programming_language)", + "long_name": "en.wikipedia.org", + "img": "https://imgs.search.brave.com/0kxnVOiqv-faZvOJc7zpym4Zin1CTs1f1svfNZSzmfU/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNjQwNGZhZWY0/ZTQ1YWUzYzQ3MDUw/MmMzMGY3NTQ0ZjNj/NDUwMDk5ZTI3MWRk/NWYyNTM4N2UwOTE0/NTI3ZDQzNy9lbi53/aWtpcGVkaWEub3Jn/Lw" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "en.wikipedia.org", + "hostname": "en.wikipedia.org", + "favicon": "https://imgs.search.brave.com/0kxnVOiqv-faZvOJc7zpym4Zin1CTs1f1svfNZSzmfU/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNjQwNGZhZWY0/ZTQ1YWUzYzQ3MDUw/MmMzMGY3NTQ0ZjNj/NDUwMDk5ZTI3MWRk/NWYyNTM4N2UwOTE0/NTI3ZDQzNy9lbi53/aWtpcGVkaWEub3Jn/Lw", + "path": "› wiki › Python_(programming_language)" + }, + "age": "4 days ago", + "extra_snippets": [ + "Python is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), object-oriented and functional programming. It is often described as a \"batteries included\" language due to its comprehensive standard library.", + "Guido van Rossum began working on Python in the late 1980s as a successor to the ABC programming language and first released it in 1991 as Python 0.9.0. Python 2.0 was released in 2000. Python 3.0, released in 2008, was a major revision not completely backward-compatible with earlier versions. Python 2.7.18, released in 2020, was the last release of Python 2.", + "Python was invented in the late 1980s by Guido van Rossum at Centrum Wiskunde & Informatica (CWI) in the Netherlands as a successor to the ABC programming language, which was inspired by SETL, capable of exception handling and interfacing with the Amoeba operating system.", + "Python consistently ranks as one of the most popular programming languages, and has gained widespread use in the machine learning community." + ] + }, + { + "title": "Python Tutorial", + "url": "https://www.w3schools.com/python/", + "is_source_local": false, + "is_source_both": false, + "description": "W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, Python, SQL, Java, and many, many more.", + "page_age": "2017-12-07T00:00:00", + "profile": { + "name": "W3Schools", + "url": "https://www.w3schools.com/python/", + "long_name": "w3schools.com", + "img": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "w3schools.com", + "hostname": "www.w3schools.com", + "favicon": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8", + "path": "› python" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/EMfp8dodbJehmj0yCJh8317RHuaumsddnHI4bujvFcg/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dzNzY2hvb2xzLmNv/bS9pbWFnZXMvdzNz/Y2hvb2xzX2xvZ29f/NDM2XzIucG5n", + "original": "https://www.w3schools.com/images/w3schools_logo_436_2.png", + "logo": true + }, + "age": "December 7, 2017", + "extra_snippets": [ + "Well organized and easy to understand Web building tutorials with lots of examples of how to use HTML, CSS, JavaScript, SQL, Python, PHP, Bootstrap, Java, XML and more.", + "HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE", + "Python Variables Variable Names Assign Multiple Values Output Variables Global Variables Variable Exercises Python Data Types Python Numbers Python Casting Python Strings", + "Python Strings Slicing Strings Modify Strings Concatenate Strings Format Strings Escape Characters String Methods String Exercises Python Booleans Python Operators Python Lists" + ] + }, + { + "title": "Online Python - IDE, Editor, Compiler, Interpreter", + "url": "https://www.online-python.com/", + "is_source_local": false, + "is_source_both": false, + "description": "Build and Run your Python code instantly. Online-Python is a quick and easy tool that helps you to build, compile, test your python programs.", + "profile": { + "name": "Online-python", + "url": "https://www.online-python.com/", + "long_name": "online-python.com", + "img": "https://imgs.search.brave.com/kfaEvapwHxSsRObO52-I-otYFPHpG1h7UXJyUqDM2Ec/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGYxODdjNWQ0/NjZjZTNiMjk5NDY1/MWI5MTgyYjU3Y2Q3/MTI3NGM5MjUzY2Fi/OGQ3MTQ4MmIxMTQx/ZTcxNWFhMC93d3cu/b25saW5lLXB5dGhv/bi5jb20v" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "online-python.com", + "hostname": "www.online-python.com", + "favicon": "https://imgs.search.brave.com/kfaEvapwHxSsRObO52-I-otYFPHpG1h7UXJyUqDM2Ec/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGYxODdjNWQ0/NjZjZTNiMjk5NDY1/MWI5MTgyYjU3Y2Q3/MTI3NGM5MjUzY2Fi/OGQ3MTQ4MmIxMTQx/ZTcxNWFhMC93d3cu/b25saW5lLXB5dGhv/bi5jb20v", + "path": "" + }, + "extra_snippets": [ + "Build, run, and share Python code online for free with the help of online-integrated python's development environment (IDE). It is one of the most efficient, dependable, and potent online compilers for the Python programming language. It is not necessary for you to bother about establishing a Python environment in your local.", + "It is one of the most efficient, dependable, and potent online compilers for the Python programming language. It is not necessary for you to bother about establishing a Python environment in your local. Now You can immediately execute the Python code in the web browser of your choice.", + "It is not necessary for you to bother about establishing a Python environment in your local. Now You can immediately execute the Python code in the web browser of your choice. Using this Python editor is simple and quick to get up and running with. Simply type in the programme, and then press the RUN button!", + "Now You can immediately execute the Python code in the web browser of your choice. Using this Python editor is simple and quick to get up and running with. Simply type in the programme, and then press the RUN button! The code can be saved online by choosing the SHARE option, which also gives you the ability to access your code from any location providing you have internet access." + ] + }, + { + "title": "Python · GitHub", + "url": "https://github.com/python", + "is_source_local": false, + "is_source_both": false, + "description": "Repositories related to the Python Programming language - Python", + "page_age": "2023-03-06T00:00:00", + "profile": { + "name": "GitHub", + "url": "https://github.com/python", + "long_name": "github.com", + "img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "github.com", + "hostname": "github.com", + "favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw", + "path": "› python" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/POoaRfu_7gfp-D_O3qMNJrwDqJNbiDu1HuBpNJ_MpVQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9hdmF0/YXJzLmdpdGh1YnVz/ZXJjb250ZW50LmNv/bS91LzE1MjU5ODE_/cz0yMDAmYW1wO3Y9/NA", + "original": "https://avatars.githubusercontent.com/u/1525981?s=200&v=4", + "logo": false + }, + "age": "March 6, 2023", + "extra_snippets": ["Configuration for Python planets (e.g. http://planetpython.org)"] + }, + { + "title": "Online Python Compiler (Interpreter)", + "url": "https://www.programiz.com/python-programming/online-compiler/", + "is_source_local": false, + "is_source_both": false, + "description": "Write and run Python code using our online compiler (interpreter). You can use Python Shell like IDLE, and take inputs from the user in our Python compiler.", + "page_age": "2020-06-02T00:00:00", + "profile": { + "name": "Programiz", + "url": "https://www.programiz.com/python-programming/online-compiler/", + "long_name": "programiz.com", + "img": "https://imgs.search.brave.com/ozj4JFayZ3Fs5c9eTp7M5g12azQ_Hblgu4dpTuHRz6U/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMGJlN2U1YjVi/Y2M3ZDU5OGMwMWNi/M2Q3YjhjOTM1ZTFk/Y2NkZjE4NGQwOGIx/MTQ4NjI2YmNhODVj/MzFkMmJhYy93d3cu/cHJvZ3JhbWl6LmNv/bS8" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "programiz.com", + "hostname": "www.programiz.com", + "favicon": "https://imgs.search.brave.com/ozj4JFayZ3Fs5c9eTp7M5g12azQ_Hblgu4dpTuHRz6U/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMGJlN2U1YjVi/Y2M3ZDU5OGMwMWNi/M2Q3YjhjOTM1ZTFk/Y2NkZjE4NGQwOGIx/MTQ4NjI2YmNhODVj/MzFkMmJhYy93d3cu/cHJvZ3JhbWl6LmNv/bS8", + "path": "› python-programming › online-compiler" + }, + "age": "June 2, 2020", + "extra_snippets": [ + "Python Online Compiler Online R Compiler SQL Online Editor Online HTML/CSS Editor Online Java Compiler C Online Compiler C++ Online Compiler C# Online Compiler JavaScript Online Compiler Online GoLang Compiler Online PHP Compiler Online Swift Compiler Online Rust Compiler", + "# Online Python compiler (interpreter) to run Python online. # Write Python 3 code in this online editor and run it. print(\"Try programiz.pro\")" + ] + }, + { + "title": "Python Developer", + "url": "https://twitter.com/Python_Dv/status/1786763460992544791", + "is_source_local": false, + "is_source_both": false, + "description": "Python Developer", + "page_age": "2024-05-04T14:30:03", + "profile": { + "name": "X", + "url": "https://twitter.com/Python_Dv/status/1786763460992544791", + "long_name": "twitter.com", + "img": "https://imgs.search.brave.com/Zq483bGX0GnSgym-1P7iyOyEDX3PkDZSNT8m56F862A/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2MxOTUxNzhj/OTY1ZTQ3N2I0MjJk/MTY5NGM0MTRlYWVi/MjU1YWE2NDUwYmQ2/YTA2MDFhMDlkZDEx/NTAzZGNiNi90d2l0/dGVyLmNvbS8" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "twitter.com", + "hostname": "twitter.com", + "favicon": "https://imgs.search.brave.com/Zq483bGX0GnSgym-1P7iyOyEDX3PkDZSNT8m56F862A/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2MxOTUxNzhj/OTY1ZTQ3N2I0MjJk/MTY5NGM0MTRlYWVi/MjU1YWE2NDUwYmQ2/YTA2MDFhMDlkZDEx/NTAzZGNiNi90d2l0/dGVyLmNvbS8", + "path": "› Python_Dv › status › 1786763460992544791" + }, + "age": "20 hours ago" + }, + { + "title": "input table name? - python script - KNIME Extensions - KNIME Community Forum", + "url": "https://forum.knime.com/t/input-table-name-python-script/78978", + "is_source_local": false, + "is_source_both": false, + "description": "Hi, when running a python script node, I get the error seen on the screenshot Same happens with this code too: The script input is output from the csv reader node. How can I get the right name for that table? Best wishes, Dario", + "page_age": "2024-05-04T09:20:44", + "profile": { + "name": "Knime", + "url": "https://forum.knime.com/t/input-table-name-python-script/78978", + "long_name": "forum.knime.com", + "img": "https://imgs.search.brave.com/WQoOhAD5i6uEhJ-qXvlWMJwbGA52f2Ycc_ns36EK698/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTAxNzMxNjFl/MzJjNzU5NzRkOTMz/Mjg4NDU2OWUxM2Rj/YzVkOGM3MzIwNzI2/YTY1NzYxNzA1MDE5/NzQzOWU3NC9mb3J1/bS5rbmltZS5jb20v" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "article", + "meta_url": { + "scheme": "https", + "netloc": "forum.knime.com", + "hostname": "forum.knime.com", + "favicon": "https://imgs.search.brave.com/WQoOhAD5i6uEhJ-qXvlWMJwbGA52f2Ycc_ns36EK698/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTAxNzMxNjFl/MzJjNzU5NzRkOTMz/Mjg4NDU2OWUxM2Rj/YzVkOGM3MzIwNzI2/YTY1NzYxNzA1MDE5/NzQzOWU3NC9mb3J1/bS5rbmltZS5jb20v", + "path": " › knime extensions" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/DtEl38dcvuM1kGfhN0T5HfOrsMJcztWNyriLvtDJmKI/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9mb3J1/bS1jZG4ua25pbWUu/Y29tL3VwbG9hZHMv/ZGVmYXVsdC9vcmln/aW5hbC8zWC9lLzYv/ZTY0M2M2NzFlNzAz/MDg2MjkwMWY2YzJh/OWFjOWI5ZmEwM2M3/ZjMwZi5wbmc", + "original": "https://forum-cdn.knime.com/uploads/default/original/3X/e/6/e643c671e7030862901f6c2a9ac9b9fa03c7f30f.png", + "logo": false + }, + "age": "1 day ago", + "extra_snippets": [ + "Hi, when running a python script node, I get the error seen on the screenshot Same happens with this code too: The script input is output from the csv reader node. How can I get the right name for that table? …" + ] + }, + { + "title": "What does the Double Star operator mean in Python? - GeeksforGeeks", + "url": "https://www.geeksforgeeks.org/what-does-the-double-star-operator-mean-in-python/", + "is_source_local": false, + "is_source_both": false, + "description": "A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.", + "page_age": "2023-03-14T17:15:04", + "profile": { + "name": "GeeksforGeeks", + "url": "https://www.geeksforgeeks.org/what-does-the-double-star-operator-mean-in-python/", + "long_name": "geeksforgeeks.org", + "img": "https://imgs.search.brave.com/fhzcfv5xltx6-YBvJI9RZgS7xZo0dPNaASsrB8YOsCs/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjBhOGQ3MmNi/ZWE5N2EwMmZjYzA1/ZTI0ZTFhMGUyMTE0/MGM0ZTBmMWZlM2Y2/Yzk2ODMxZTRhYTBi/NDdjYTE0OS93d3cu/Z2Vla3Nmb3JnZWVr/cy5vcmcv" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "article", + "meta_url": { + "scheme": "https", + "netloc": "geeksforgeeks.org", + "hostname": "www.geeksforgeeks.org", + "favicon": "https://imgs.search.brave.com/fhzcfv5xltx6-YBvJI9RZgS7xZo0dPNaASsrB8YOsCs/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjBhOGQ3MmNi/ZWE5N2EwMmZjYzA1/ZTI0ZTFhMGUyMTE0/MGM0ZTBmMWZlM2Y2/Yzk2ODMxZTRhYTBi/NDdjYTE0OS93d3cu/Z2Vla3Nmb3JnZWVr/cy5vcmcv", + "path": "› what-does-the-double-star-operator-mean-in-python" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/GcR-j_dLbyHkbHEI3ffLMi6xpXGhF_2Z8POIoqtokhM/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9tZWRp/YS5nZWVrc2Zvcmdl/ZWtzLm9yZy93cC1j/b250ZW50L3VwbG9h/ZHMvZ2ZnXzIwMFgy/MDAtMTAweDEwMC5w/bmc", + "original": "https://media.geeksforgeeks.org/wp-content/uploads/gfg_200X200-100x100.png", + "logo": false + }, + "age": "March 14, 2023", + "extra_snippets": [ + "Difference between / vs. // operator in Python", + "Double Star or (**) is one of the Arithmetic Operator (Like +, -, *, **, /, //, %) in Python Language. It is also known as Power Operator.", + "The time complexity of the given Python program is O(n), where n is the number of key-value pairs in the input dictionary.", + "Inplace Operators in Python | Set 2 (ixor(), iand(), ipow(),…)" + ] + }, + { + "title": "r/Python", + "url": "https://www.reddit.com/r/Python/", + "is_source_local": false, + "is_source_both": false, + "description": "The official Python community for Reddit! Stay up to date with the latest news, packages, and meta information relating to the Python programming language. --- If you have questions or are new to Python use r/LearnPython", + "page_age": "2022-12-30T16:25:02", + "profile": { + "name": "Reddit", + "url": "https://www.reddit.com/r/Python/", + "long_name": "reddit.com", + "img": "https://imgs.search.brave.com/mAZYEK9Wi13WLDUge7XZ8YuDTwm6DP6gBjvz1GdYZVY/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2ZiNTU0M2Nj/MTFhZjRiYWViZDlk/MjJiMjBjMzFjMDRk/Y2IzYWI0MGI0MjVk/OGY5NzQzOGQ5NzQ5/NWJhMWI0NC93d3cu/cmVkZGl0LmNvbS8" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "reddit.com", + "hostname": "www.reddit.com", + "favicon": "https://imgs.search.brave.com/mAZYEK9Wi13WLDUge7XZ8YuDTwm6DP6gBjvz1GdYZVY/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2ZiNTU0M2Nj/MTFhZjRiYWViZDlk/MjJiMjBjMzFjMDRk/Y2IzYWI0MGI0MjVk/OGY5NzQzOGQ5NzQ5/NWJhMWI0NC93d3cu/cmVkZGl0LmNvbS8", + "path": "› r › Python" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/zWd10t3zg34ciHiAB-K5WWK3h_H4LedeDot9BVX7Ydo/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9zdHls/ZXMucmVkZGl0bWVk/aWEuY29tL3Q1XzJx/aDB5L3N0eWxlcy9j/b21tdW5pdHlJY29u/X2NpZmVobDR4dDdu/YzEucG5n", + "original": "https://styles.redditmedia.com/t5_2qh0y/styles/communityIcon_cifehl4xt7nc1.png", + "logo": false + }, + "age": "December 30, 2022", + "extra_snippets": [ + "r/Python: The official Python community for Reddit! Stay up to date with the latest news, packages, and meta information relating to the Python…", + "By default, Python allows you to import and use anything, anywhere. Over time, this results in modules that were intended to be separate getting tightly coupled together, and domain boundaries breaking down. We experienced this first-hand at a unicorn startup, where the eng team paused development for over a year in an attempt to split up packages into independent services.", + "Hello r/Python! It's time to share what you've been working on! Whether it's a work-in-progress, a completed masterpiece, or just a rough idea, let us know what you're up to!", + "Whether it's your job, your hobby, or your passion project, all Python-related work is welcome here." + ] + }, + { + "title": "GitHub - python/cpython: The Python programming language", + "url": "https://github.com/python/cpython", + "is_source_local": false, + "is_source_both": false, + "description": "The Python programming language. Contribute to python/cpython development by creating an account on GitHub.", + "page_age": "2022-10-29T00:00:00", + "profile": { + "name": "GitHub", + "url": "https://github.com/python/cpython", + "long_name": "github.com", + "img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "software", + "meta_url": { + "scheme": "https", + "netloc": "github.com", + "hostname": "github.com", + "favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw", + "path": "› python › cpython" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/BJbWFRUqgP-tKIyGK9ByXjuYjHO2mtYigUOEFNz_gXk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9vcGVu/Z3JhcGguZ2l0aHVi/YXNzZXRzLmNvbS82/MTY5YmJkNTQ0YzAy/NDg0MGU4NDdjYTU1/YTU3ZGZmMDA2ZDAw/YWQ1NDIzOTFmYTQ3/YmJjODg3OWM0NWYw/MTZhL3B5dGhvbi9j/cHl0aG9u", + "original": "https://opengraph.githubassets.com/6169bbd544c024840e847ca55a57dff006d00ad542391fa47bbc8879c45f016a/python/cpython", + "logo": false + }, + "age": "October 29, 2022", + "extra_snippets": [ + "You can pass many options to the configure script; run ./configure --help to find out more. On macOS case-insensitive file systems and on Cygwin, the executable is called python.exe; elsewhere it's just python.", + "Building a complete Python installation requires the use of various additional third-party libraries, depending on your build platform and configure options. Not all standard library modules are buildable or usable on all platforms. Refer to the Install dependencies section of the Developer Guide for current detailed information on dependencies for various Linux distributions and macOS.", + "To get an optimized build of Python, configure --enable-optimizations before you run make. This sets the default make targets up to enable Profile Guided Optimization (PGO) and may be used to auto-enable Link Time Optimization (LTO) on some platforms. For more details, see the sections below.", + "Copyright © 2001-2024 Python Software Foundation. All rights reserved." + ] + }, + { + "title": "5. Data Structures — Python 3.12.3 documentation", + "url": "https://docs.python.org/3/tutorial/datastructures.html", + "is_source_local": false, + "is_source_both": false, + "description": "This chapter describes some things you’ve learned about already in more detail, and adds some new things as well. More on Lists: The list data type has some more methods. Here are all of the method...", + "page_age": "2023-07-04T00:00:00", + "profile": { + "name": "Python documentation", + "url": "https://docs.python.org/3/tutorial/datastructures.html", + "long_name": "docs.python.org", + "img": "https://imgs.search.brave.com/F5Ym7eSElhGdGUFKLRxDj9Z_tc180ldpeMvQ2Q6ARbA/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTUzOTFjOGVi/YTcyOTVmODA3ODIy/YjE2NzFjY2ViMjhl/NzRlY2JhYTc5YjNm/ZjhmODAyZWI2OGUw/ZjU4NDVlNy9kb2Nz/LnB5dGhvbi5vcmcv" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "docs.python.org", + "hostname": "docs.python.org", + "favicon": "https://imgs.search.brave.com/F5Ym7eSElhGdGUFKLRxDj9Z_tc180ldpeMvQ2Q6ARbA/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTUzOTFjOGVi/YTcyOTVmODA3ODIy/YjE2NzFjY2ViMjhl/NzRlY2JhYTc5YjNm/ZjhmODAyZWI2OGUw/ZjU4NDVlNy9kb2Nz/LnB5dGhvbi5vcmcv", + "path": "› 3 › tutorial › datastructures.html" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/Y7GrMRF8WorDIMLuOl97XC8ltYpoOCqNwWF2pQIIKls/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9kb2Nz/LnB5dGhvbi5vcmcv/My9fc3RhdGljL29n/LWltYWdlLnBuZw", + "original": "https://docs.python.org/3/_static/og-image.png", + "logo": false + }, + "age": "July 4, 2023", + "extra_snippets": [ + "You might have noticed that methods like insert, remove or sort that only modify the list have no return value printed – they return the default None. [1] This is a design principle for all mutable data structures in Python.", + "We saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types — list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.", + "Python also includes a data type for sets. A set is an unordered collection with no duplicate elements. Basic uses include membership testing and eliminating duplicate entries. Set objects also support mathematical operations like union, intersection, difference, and symmetric difference.", + "Another useful data type built into Python is the dictionary (see Mapping Types — dict). Dictionaries are sometimes found in other languages as “associative memories” or “associative arrays”. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys." + ] + }, + { + "title": "Something wrong with python packages / AUR Issues, Discussion & PKGBUILD Requests / Arch Linux Forums", + "url": "https://bbs.archlinux.org/viewtopic.php?id=295466", + "is_source_local": false, + "is_source_both": false, + "description": "Big Python updates require Python packages to be rebuild. For some reason they didn't think a bump that made it necessary to rebuild half the official repo was a news post.", + "page_age": "2024-05-04T08:30:02", + "profile": { + "name": "Archlinux", + "url": "https://bbs.archlinux.org/viewtopic.php?id=295466", + "long_name": "bbs.archlinux.org", + "img": "https://imgs.search.brave.com/3au9oqkzSri_aLEec3jo-0bFgLuICkydrWfjFcC8lkI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNWNkODM1MWJl/ZmJhMzkzNzYzMDkz/NmEyMWMxNjI5MjNk/NGJmZjFhNTBlZDNl/Mzk5MzJjOGZkYjZl/MjNmY2IzNS9iYnMu/YXJjaGxpbnV4Lm9y/Zy8" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "bbs.archlinux.org", + "hostname": "bbs.archlinux.org", + "favicon": "https://imgs.search.brave.com/3au9oqkzSri_aLEec3jo-0bFgLuICkydrWfjFcC8lkI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNWNkODM1MWJl/ZmJhMzkzNzYzMDkz/NmEyMWMxNjI5MjNk/NGJmZjFhNTBlZDNl/Mzk5MzJjOGZkYjZl/MjNmY2IzNS9iYnMu/YXJjaGxpbnV4Lm9y/Zy8", + "path": "› viewtopic.php" + }, + "age": "1 day ago", + "extra_snippets": [ + "Traceback (most recent call last): File \"/usr/lib/python3.12/importlib/metadata/__init__.py\", line 397, in from_name return next(cls.discover(name=name)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ StopIteration During handling of the above exception, another exception occurred: Traceback (most recent call last): File \"/usr/bin/informant\", line 33, in sys.exit(load_entry_point('informant==0.5.0', 'console_scripts', 'informant')()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File \"/usr/bin/informant\", line 22, in importlib_load_entry_point for entry_point in distribution(dis" + ] + }, + { + "title": "Introduction to Python", + "url": "https://www.w3schools.com/python/python_intro.asp", + "is_source_local": false, + "is_source_both": false, + "description": "W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, Python, SQL, Java, and many, many more.", + "profile": { + "name": "W3Schools", + "url": "https://www.w3schools.com/python/python_intro.asp", + "long_name": "w3schools.com", + "img": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "w3schools.com", + "hostname": "www.w3schools.com", + "favicon": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8", + "path": "› python › python_intro.asp" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/EMfp8dodbJehmj0yCJh8317RHuaumsddnHI4bujvFcg/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dzNzY2hvb2xzLmNv/bS9pbWFnZXMvdzNz/Y2hvb2xzX2xvZ29f/NDM2XzIucG5n", + "original": "https://www.w3schools.com/images/w3schools_logo_436_2.png", + "logo": true + }, + "extra_snippets": [ + "Well organized and easy to understand Web building tutorials with lots of examples of how to use HTML, CSS, JavaScript, SQL, Python, PHP, Bootstrap, Java, XML and more.", + "HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE", + "Python Variables Variable Names Assign Multiple Values Output Variables Global Variables Variable Exercises Python Data Types Python Numbers Python Casting Python Strings", + "Python Strings Slicing Strings Modify Strings Concatenate Strings Format Strings Escape Characters String Methods String Exercises Python Booleans Python Operators Python Lists" + ] + }, + { + "title": "bug: AUR package wants to use python but does not find any preset version · Issue #1740 · asdf-vm/asdf", + "url": "https://github.com/asdf-vm/asdf/issues/1740", + "is_source_local": false, + "is_source_both": false, + "description": "Describe the Bug I am not sure why this is happening, I am trying to install tlpui from AUR and it fails, here are some logs to help: ==> Making package: tlpui 2:1.6.5-1 (Mi 10 apr 2024 23:19:15 +0...", + "page_age": "2024-05-04T06:45:04", + "profile": { + "name": "GitHub", + "url": "https://github.com/asdf-vm/asdf/issues/1740", + "long_name": "github.com", + "img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "software", + "meta_url": { + "scheme": "https", + "netloc": "github.com", + "hostname": "github.com", + "favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw", + "path": "› asdf-vm › asdf › issues › 1740" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/KrLW5s_2n4jyP8XLbc3ZPVBaLD963tQgWzG9EWPZlQs/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9vcGVu/Z3JhcGguZ2l0aHVi/YXNzZXRzLmNvbS81/MTE0ZTdkOGIwODM2/YmQ2MTY3NzQ1ZGI4/MmZjMGE3OGUyMjcw/MGFlY2ZjMWZkODBl/MDYzZTNiN2ZjOWNj/NzYyL2FzZGYtdm0v/YXNkZi9pc3N1ZXMv/MTc0MA", + "original": "https://opengraph.githubassets.com/5114e7d8b0836bd6167745db82fc0a78e22700aecfc1fd80e063e3b7fc9cc762/asdf-vm/asdf/issues/1740", + "logo": false + }, + "age": "1 day ago", + "extra_snippets": [ + "==> Starting build()... No preset version installed for command python Please install a version by running one of the following: asdf install python 3.8 or add one of the following versions in your config file at /home/ferret/.tool-versions python 3.11.0 python 3.12.1 python 3.12.3 ==> ERROR: A failure occurred in build(). Aborting...", + "-> error making: tlpui-exit status 4 -> Failed to install the following packages. Manual intervention is required: tlpui - exit status 4 ferret@FX505DT in ~ $ cat /home/ferret/.tool-versions nodejs 21.6.0 python 3.12.3 ferret@FX505DT in ~ $ python -V Python 3.12.3 ferret@FX505DT in ~ $ which python /home/ferret/.asdf/shims/python", + "Describe the Bug I am not sure why this is happening, I am trying to install tlpui from AUR and it fails, here are some logs to help: ==> Making package: tlpui 2:1.6.5-1 (Mi 10 apr 2024 23:19:15 +0300) ==> Retrieving sources... -> Found ..." + ] + }, + { + "title": "What are python.exe and python3.exe, and why do they appear to point to App Installer? | Windows 11 Forum", + "url": "https://www.elevenforum.com/t/what-are-python-exe-and-python3-exe-and-why-do-they-appear-to-point-to-app-installer.24886/", + "is_source_local": false, + "is_source_both": false, + "description": "I was looking at App execution aliases (Settings > Apps > Advanced app settings > App execution aliases) on my new computer -- my first Windows 11 computer. Why are python.exe and python3.exe listed as App Installer? I assume that App Installer refers to installation of Microsoft Store / UWP...", + "page_age": "2024-05-03T17:30:04", + "profile": { + "name": "Windows 11 Forum", + "url": "https://www.elevenforum.com/t/what-are-python-exe-and-python3-exe-and-why-do-they-appear-to-point-to-app-installer.24886/", + "long_name": "elevenforum.com", + "img": "https://imgs.search.brave.com/XVRAYMEj6Im8i7jV5RxeTwpiRPtY9IWg4wRIuh-WhEw/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZjk5MDZkMDIw/M2U1OWIwNjM5Y2U1/M2U2NzNiNzVkNTA5/NzA5OTI1ZTFmOTc4/MzU3OTlhYzU5OTVi/ZGNjNTY4MS93d3cu/ZWxldmVuZm9ydW0u/Y29tLw" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "elevenforum.com", + "hostname": "www.elevenforum.com", + "favicon": "https://imgs.search.brave.com/XVRAYMEj6Im8i7jV5RxeTwpiRPtY9IWg4wRIuh-WhEw/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZjk5MDZkMDIw/M2U1OWIwNjM5Y2U1/M2U2NzNiNzVkNTA5/NzA5OTI1ZTFmOTc4/MzU3OTlhYzU5OTVi/ZGNjNTY4MS93d3cu/ZWxldmVuZm9ydW0u/Y29tLw", + "path": " › windows support forums › apps and software" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/DVoFcE6d_-lx3BVGNS-RZK_lZzxQ8VhwZVf3AVqEJFA/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/ZWxldmVuZm9ydW0u/Y29tL2RhdGEvYXNz/ZXRzL2xvZ28vbWV0/YTEtMjAxLnBuZw", + "original": "https://www.elevenforum.com/data/assets/logo/meta1-201.png", + "logo": true + }, + "age": "2 days ago", + "extra_snippets": [ + "Why are python.exe and python3.exe listed as App Installer? I assume that App Installer refers to installation of Microsoft Store / UWP apps, but if that's the case, then why are they called python.exe and python3.exe? Or are python.exe and python3.exe simply serving as aliases / pointers pointing to App Installer, which is itself a Microsoft Store App?", + "Or are python.exe and python3.exe simply serving as aliases / pointers pointing to App Installer, which is itself a Microsoft Store App? I wish to soon install Python, along with an integrated development editor (IDE), on my machine, so that I can code in Python.", + "I wish to soon install Python, along with an integrated development editor (IDE), on my machine, so that I can code in Python. But is a Python interpreter already on my computer as suggested, if obliquely, by the presence of python.exe and python3.exe? I kind of doubt it." + ] + }, + { + "title": "How to Watermark Your Images Using Python OpenCV in ...", + "url": "https://medium.com/@daily_data_prep/how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1", + "is_source_local": false, + "is_source_both": false, + "description": "Medium is an open platform where readers find dynamic thinking, and where expert and undiscovered voices can share their writing on any topic.", + "page_age": "2024-05-03T14:05:06", + "profile": { + "name": "Medium", + "url": "https://medium.com/@daily_data_prep/how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1", + "long_name": "medium.com", + "img": "https://imgs.search.brave.com/qvE2kIQCiAsnPv2C6P9xM5J2VVWdm55g-A-2Q_yIJ0g/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTZhYmQ1N2Q4/NDg4ZDcyODIyMDZi/MzFmOWNhNjE3Y2E4/Y2YzMThjNjljNDIx/ZjllZmNhYTcwODhl/YTcwNDEzYy9tZWRp/dW0uY29tLw" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "medium.com", + "hostname": "medium.com", + "favicon": "https://imgs.search.brave.com/qvE2kIQCiAsnPv2C6P9xM5J2VVWdm55g-A-2Q_yIJ0g/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTZhYmQ1N2Q4/NDg4ZDcyODIyMDZi/MzFmOWNhNjE3Y2E4/Y2YzMThjNjljNDIx/ZjllZmNhYTcwODhl/YTcwNDEzYy9tZWRp/dW0uY29tLw", + "path": "› @daily_data_prep › how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1" + }, + "age": "2 days ago" + }, + { + "title": "Increment and Decrement Operators in Python?", + "url": "https://www.tutorialspoint.com/increment-and-decrement-operators-in-python", + "is_source_local": false, + "is_source_both": false, + "description": "Increment and Decrement Operators in Python - Python does not have unary increment/decrement operator (++/--). Instead to increment a value, usea += 1to decrement a value, use −a -= 1Example>>> a = 0 >>> >>> #Increment >>> a +=1 >>> >>> #Decrement >>> a -= 1 >>> >>> #value of a >>> a 0Python ...", + "page_age": "2023-08-23T00:00:00", + "profile": { + "name": "Tutorialspoint", + "url": "https://www.tutorialspoint.com/increment-and-decrement-operators-in-python", + "long_name": "tutorialspoint.com", + "img": "https://imgs.search.brave.com/Wt8BSkivPlFwcU5yBtf7YzuvTuRExyd_502cdABCS5c/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjcyYjAzYmVl/ODU4MzZiMjJiYTFh/MjJhZDNmNWE4YzA5/MDgyYTZhMDg3NTYw/M2NiY2NiZTUxN2I5/MjU1MWFmMS93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tLw" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "tutorialspoint.com", + "hostname": "www.tutorialspoint.com", + "favicon": "https://imgs.search.brave.com/Wt8BSkivPlFwcU5yBtf7YzuvTuRExyd_502cdABCS5c/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjcyYjAzYmVl/ODU4MzZiMjJiYTFh/MjJhZDNmNWE4YzA5/MDgyYTZhMDg3NTYw/M2NiY2NiZTUxN2I5/MjU1MWFmMS93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tLw", + "path": "› increment-and-decrement-operators-in-python" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/ddG5vyZGLVudvecEbQJPeG8tGuaZ7g3Xz6Gyjdl5WA8/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tL2ltYWdl/cy90cF9sb2dvXzQz/Ni5wbmc", + "original": "https://www.tutorialspoint.com/images/tp_logo_436.png", + "logo": true + }, + "age": "August 23, 2023", + "extra_snippets": [ + "Increment and Decrement Operators in Python - Python does not have unary increment/decrement operator (++/--). Instead to increment a value, usea += 1to decrement a value, use −a -= 1Example>>> a = 0 >>> >>> #Increment >>> a +=1 >>> >>> #Decrement >>> a -= 1 >>> >>> #value of a >>> a 0Python does not provide multiple ways to do the same thing", + "So what above statement means in python is: create an object of type int having value 1 and give the name a to it. The object is an instance of int having value 1 and the name a refers to it. The assigned name a and the object to which it refers are distinct.", + "Python does not provide multiple ways to do the same thing .", + "However, be careful if you are coming from a language like C, Python doesn’t have \"variables\" in the sense that C does, instead python uses names and objects and in python integers (int’s) are immutable." + ] + }, + { + "title": "Gumroad – How not to suck at Python / SideFX Houdini | CG Persia", + "url": "https://cgpersia.com/2024/05/gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html", + "is_source_local": false, + "is_source_both": false, + "description": "Info: This course is made for artists or TD (technical director) willing to learn Python to improve their workflows inside SideFX Houdini, get faster in production and develop all the tools you always wished you had.", + "page_age": "2024-05-03T08:35:03", + "profile": { + "name": "Cgpersia", + "url": "https://cgpersia.com/2024/05/gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html", + "long_name": "cgpersia.com", + "img": "https://imgs.search.brave.com/VjyaopAm-M9sWvM7n-KnGZ3T5swIOwwE80iF5QVqQPg/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYmE0MzQ4NmI2/NjFhMTA1ZDBiN2Iw/ZWNiNDUxNjUwYjdh/MGE5ZjQ0ZjIxNzll/NmVkZDE2YzYyMDBh/NDNiMDgwMy9jZ3Bl/cnNpYS5jb20v" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "cgpersia.com", + "hostname": "cgpersia.com", + "favicon": "https://imgs.search.brave.com/VjyaopAm-M9sWvM7n-KnGZ3T5swIOwwE80iF5QVqQPg/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYmE0MzQ4NmI2/NjFhMTA1ZDBiN2Iw/ZWNiNDUxNjUwYjdh/MGE5ZjQ0ZjIxNzll/NmVkZDE2YzYyMDBh/NDNiMDgwMy9jZ3Bl/cnNpYS5jb20v", + "path": "› 2024 › 05 › gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html" + }, + "age": "2 days ago", + "extra_snippets": [ + "Posted in: 2D, CG Releases, Downloads, Learning, Tutorials, Videos. Tagged: Gumroad, Python, Sidefx. Leave a Comment", + "01 – Python – Fundamentals Get the Fundamentals of python before starting the fun stuff ! 02 – Python Construction Part02 digging further into python concepts 03 – Houdini – Python Basics Applying some basic python in Houdini and starting to make tools !", + "02 – Python Construction Part02 digging further into python concepts 03 – Houdini – Python Basics Applying some basic python in Houdini and starting to make tools ! 04 – Houdini – Python Intermediate Applying some more advanced python in Houdini to make tools ! 05 – Houdini – Python Expert Using QtDesigner in combinaison with Houdini Python/Pyside to create advanced tools." + ] + }, + { + "title": "How to install Python: The complete Python programmer’s guide", + "url": "https://www.pluralsight.com/resources/blog/software-development/python-installation-guide", + "is_source_local": false, + "is_source_both": false, + "description": "An easy guide on how set up your operating system so you can program in Python, and how to update or uninstall it. For Linux, Windows, and macOS.", + "page_age": "2024-05-02T07:30:02", + "profile": { + "name": "Pluralsight", + "url": "https://www.pluralsight.com/resources/blog/software-development/python-installation-guide", + "long_name": "pluralsight.com", + "img": "https://imgs.search.brave.com/zvwQNSVu9-jR2CRlNcsTzxjaXKPlXNuh-Jo9-0yA1OE/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTNkNWQyNjk3/M2Q0NzYyMmUyNDc3/ZjYwMWFlZDI5YTI4/ODhmYzc2MDkzMjAy/MjNkMWY1MDE3NTQw/MzI5NWVkZS93d3cu/cGx1cmFsc2lnaHQu/Y29tLw" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "pluralsight.com", + "hostname": "www.pluralsight.com", + "favicon": "https://imgs.search.brave.com/zvwQNSVu9-jR2CRlNcsTzxjaXKPlXNuh-Jo9-0yA1OE/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTNkNWQyNjk3/M2Q0NzYyMmUyNDc3/ZjYwMWFlZDI5YTI4/ODhmYzc2MDkzMjAy/MjNkMWY1MDE3NTQw/MzI5NWVkZS93d3cu/cGx1cmFsc2lnaHQu/Y29tLw", + "path": " › blog › blog" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/xrv5PHH2Bzmq2rcIYzk__8h5RqCj6kS3I6SGCNw5dZM/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/cGx1cmFsc2lnaHQu/Y29tL2NvbnRlbnQv/ZGFtL3BzL2ltYWdl/cy9yZXNvdXJjZS1j/ZW50ZXIvYmxvZy9o/ZWFkZXItaGVyby1p/bWFnZXMvUHl0aG9u/LndlYnA", + "original": "https://www.pluralsight.com/content/dam/ps/images/resource-center/blog/header-hero-images/Python.webp", + "logo": false + }, + "age": "3 days ago", + "extra_snippets": [ + "Whether it’s your first time programming or you’re a seasoned programmer, you’ll have to install or update Python every now and then --- or if necessary, uninstall it. In this article, you'll learn how to do just that.", + "Some systems come with Python, so to start off, we’ll first check to see if it’s installed on your system before we proceed. To do that, we’ll need to open a terminal. Since you might be new to programming, let’s go over how to open a terminal for Linux, Windows, and macOS.", + "Before we dive into setting up your system so you can program in Python, let’s talk terminal basics and benefits.", + "However, let’s focus on why we need it for working with Python. We use a terminal, or command line, to:" + ] + } + ], + "family_friendly": true + } +} diff --git a/backend/open_webui/retrieval/web/testdata/google_pse.json b/backend/open_webui/retrieval/web/testdata/google_pse.json new file mode 100644 index 0000000000000000000000000000000000000000..15da9729cde30eee86d0302f662c5ca37d71f65b --- /dev/null +++ b/backend/open_webui/retrieval/web/testdata/google_pse.json @@ -0,0 +1,442 @@ +{ + "kind": "customsearch#search", + "url": { + "type": "application/json", + "template": "https://www.googleapis.com/customsearch/v1?q={searchTerms}&num={count?}&start={startIndex?}&lr={language?}&safe={safe?}&cx={cx?}&sort={sort?}&filter={filter?}&gl={gl?}&cr={cr?}&googlehost={googleHost?}&c2coff={disableCnTwTranslation?}&hq={hq?}&hl={hl?}&siteSearch={siteSearch?}&siteSearchFilter={siteSearchFilter?}&exactTerms={exactTerms?}&excludeTerms={excludeTerms?}&linkSite={linkSite?}&orTerms={orTerms?}&dateRestrict={dateRestrict?}&lowRange={lowRange?}&highRange={highRange?}&searchType={searchType}&fileType={fileType?}&rights={rights?}&imgSize={imgSize?}&imgType={imgType?}&imgColorType={imgColorType?}&imgDominantColor={imgDominantColor?}&alt=json" + }, + "queries": { + "request": [ + { + "title": "Google Custom Search - lectures", + "totalResults": "2450000000", + "searchTerms": "lectures", + "count": 10, + "startIndex": 1, + "inputEncoding": "utf8", + "outputEncoding": "utf8", + "safe": "off", + "cx": "0473ef98502d44e18" + } + ], + "nextPage": [ + { + "title": "Google Custom Search - lectures", + "totalResults": "2450000000", + "searchTerms": "lectures", + "count": 10, + "startIndex": 11, + "inputEncoding": "utf8", + "outputEncoding": "utf8", + "safe": "off", + "cx": "0473ef98502d44e18" + } + ] + }, + "context": { + "title": "LLM Search" + }, + "searchInformation": { + "searchTime": 0.445959, + "formattedSearchTime": "0.45", + "totalResults": "2450000000", + "formattedTotalResults": "2,450,000,000" + }, + "items": [ + { + "kind": "customsearch#result", + "title": "The Feynman Lectures on Physics", + "htmlTitle": "The Feynman \u003cb\u003eLectures\u003c/b\u003e on Physics", + "link": "https://www.feynmanlectures.caltech.edu/", + "displayLink": "www.feynmanlectures.caltech.edu", + "snippet": "This edition has been designed for ease of reading on devices of any size or shape; text, figures and equations can all be zoomed without degradation.", + "htmlSnippet": "This edition has been designed for ease of reading on devices of any size or shape; text, figures and equations can all be zoomed without degradation.", + "cacheId": "CyXMWYWs9UEJ", + "formattedUrl": "https://www.feynmanlectures.caltech.edu/", + "htmlFormattedUrl": "https://www.feynman\u003cb\u003electures\u003c/b\u003e.caltech.edu/", + "pagemap": { + "metatags": [ + { + "viewport": "width=device-width, initial-scale=1.0" + } + ] + } + }, + { + "kind": "customsearch#result", + "title": "Video Lectures", + "htmlTitle": "Video \u003cb\u003eLectures\u003c/b\u003e", + "link": "https://www.reddit.com/r/lectures/", + "displayLink": "www.reddit.com", + "snippet": "r/lectures: This subreddit is all about video lectures, talks and interesting public speeches. The topics include mathematics, physics, computer…", + "htmlSnippet": "r/\u003cb\u003electures\u003c/b\u003e: This subreddit is all about video \u003cb\u003electures\u003c/b\u003e, talks and interesting public speeches. The topics include mathematics, physics, computer…", + "formattedUrl": "https://www.reddit.com/r/lectures/", + "htmlFormattedUrl": "https://www.reddit.com/r/\u003cb\u003electures\u003c/b\u003e/", + "pagemap": { + "cse_thumbnail": [ + { + "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTZtOjhfkgUKQbL3DZxe5F6OVsgeDNffleObjJ7n9RllKQTSsimax7VIaY&s", + "width": "192", + "height": "192" + } + ], + "metatags": [ + { + "og:image": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png", + "theme-color": "#000000", + "og:image:width": "256", + "og:type": "website", + "twitter:card": "summary", + "twitter:title": "r/lectures", + "og:site_name": "Reddit", + "og:title": "r/lectures", + "og:image:height": "256", + "bingbot": "noarchive", + "msapplication-navbutton-color": "#000000", + "og:description": "This subreddit is all about video lectures, talks and interesting public speeches.\n\nThe topics include mathematics, physics, computer science, programming, engineering, biology, medicine, economics, politics, social sciences, and any other subjects!", + "twitter:image": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png", + "apple-mobile-web-app-status-bar-style": "black", + "twitter:site": "@reddit", + "viewport": "width=device-width, initial-scale=1, viewport-fit=cover", + "apple-mobile-web-app-capable": "yes", + "og:ttl": "600", + "og:url": "https://www.reddit.com/r/lectures/" + } + ], + "cse_image": [ + { + "src": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png" + } + ] + } + }, + { + "kind": "customsearch#result", + "title": "Lectures & Discussions | Flint Institute of Arts", + "htmlTitle": "\u003cb\u003eLectures\u003c/b\u003e & Discussions | Flint Institute of Arts", + "link": "https://flintarts.org/events/lectures", + "displayLink": "flintarts.org", + "snippet": "It will trace the intricate relationship between jewelry, attire, and the expression of personal identity, social hierarchy, and spiritual belief systems that ...", + "htmlSnippet": "It will trace the intricate relationship between jewelry, attire, and the expression of personal identity, social hierarchy, and spiritual belief systems that ...", + "cacheId": "jvpb9DxrfxoJ", + "formattedUrl": "https://flintarts.org/events/lectures", + "htmlFormattedUrl": "https://flintarts.org/events/\u003cb\u003electures\u003c/b\u003e", + "pagemap": { + "cse_thumbnail": [ + { + "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS23tMtAeNhJbOWdGxShYsmnyzFdzOC9Hb7lRykA9Pw72z1IlKTkjTdZw&s", + "width": "447", + "height": "113" + } + ], + "metatags": [ + { + "og:image": "https://flintarts.org/uploads/images/page-headers/_headerImage/nightshot.jpg", + "og:type": "website", + "viewport": "width=device-width, initial-scale=1", + "og:title": "Lectures & Discussions | Flint Institute of Arts", + "og:description": "The Flint Institute of Arts is the second largest art museum in Michigan and one of the largest museum art schools in the nation." + } + ], + "cse_image": [ + { + "src": "https://flintarts.org/uploads/images/page-headers/_headerImage/nightshot.jpg" + } + ] + } + }, + { + "kind": "customsearch#result", + "title": "Mandel Lectures | Mandel Center for the Humanities ... - Waltham", + "htmlTitle": "Mandel \u003cb\u003eLectures\u003c/b\u003e | Mandel Center for the Humanities ... - Waltham", + "link": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html", + "displayLink": "www.brandeis.edu", + "snippet": "Past Lectures · Lecture 1: \"Invisible Music: The Sonic Idea of Black Revolution From Captivity to Reconstruction\" · Lecture 2: \"Solidarity in Sound: Grassroots ...", + "htmlSnippet": "Past \u003cb\u003eLectures\u003c/b\u003e · \u003cb\u003eLecture\u003c/b\u003e 1: "Invisible Music: The Sonic Idea of Black Revolution From Captivity to Reconstruction" · \u003cb\u003eLecture\u003c/b\u003e 2: "Solidarity in Sound: Grassroots ...", + "cacheId": "cQLOZr0kgEEJ", + "formattedUrl": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html", + "htmlFormattedUrl": "https://www.brandeis.edu/mandel-center-humanities/mandel-\u003cb\u003electures\u003c/b\u003e.html", + "pagemap": { + "cse_thumbnail": [ + { + "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQWlU7bcJ5pIHk7RBCk2QKE-48ejF7hyPV0pr-20_cBt2BGdfKtiYXBuyw&s", + "width": "275", + "height": "183" + } + ], + "metatags": [ + { + "og:image": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba", + "twitter:card": "summary_large_image", + "viewport": "width=device-width,initial-scale=1,minimum-scale=1", + "og:title": "Mandel Lectures in the Humanities", + "og:url": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html", + "og:description": "Annual Lecture Series", + "twitter:image": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba" + } + ], + "cse_image": [ + { + "src": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba" + } + ] + } + }, + { + "kind": "customsearch#result", + "title": "Brian Douglas - YouTube", + "htmlTitle": "Brian Douglas - YouTube", + "link": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", + "displayLink": "www.youtube.com", + "snippet": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it.", + "htmlSnippet": "Welcome to Control Systems \u003cb\u003eLectures\u003c/b\u003e! This collection of videos is intended to supplement a first year controls class, not replace it.", + "cacheId": "NEROyBHolL0J", + "formattedUrl": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", + "htmlFormattedUrl": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", + "pagemap": { + "hcard": [ + { + "fn": "Brian Douglas", + "url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg" + } + ], + "cse_thumbnail": [ + { + "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR7G0CeCBz_wVTZgjnhEr2QbiKP7f3uYzKitZYn74Mi32cDmVxvsegJoLI&s", + "width": "225", + "height": "225" + } + ], + "imageobject": [ + { + "width": "900", + "url": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj", + "height": "900" + } + ], + "person": [ + { + "name": "Brian Douglas", + "url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg" + } + ], + "metatags": [ + { + "apple-itunes-app": "app-id=544007664, app-argument=https://m.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?referring_app=com.apple.mobilesafari-smartbanner, affiliate-data=ct=smart_app_banner_polymer&pt=9008", + "og:image": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj", + "twitter:app:url:iphone": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", + "twitter:app:id:googleplay": "com.google.android.youtube", + "theme-color": "rgb(255, 255, 255)", + "og:image:width": "900", + "twitter:card": "summary", + "og:site_name": "YouTube", + "twitter:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", + "twitter:app:url:ipad": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", + "al:android:package": "com.google.android.youtube", + "twitter:app:name:googleplay": "YouTube", + "al:ios:url": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", + "twitter:app:id:iphone": "544007664", + "og:description": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it. My goal is to take specific concepts in controls and expand on them in order to provide an intuitive understanding which will ultimately make you a better controls engineer. \n\nI'm glad you made it to my channel and I hope you find it useful.\n\nShoot me a message at controlsystemlectures@gmail.com, leave a comment or question and I'll get back to you if I can. Don't forget to subscribe!\n \nTwitter: @BrianBDouglas for engineering tweets and announcement of new videos.\nWebpage: http://engineeringmedia.com\n\nHere is the hardware/software I use: http://www.youtube.com/watch?v=m-M5_mIyHe4\n\nHere's a list of my favorite references: http://bit.ly/2skvmWd\n\n--Brian", + "al:ios:app_store_id": "544007664", + "twitter:image": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj", + "twitter:site": "@youtube", + "og:type": "profile", + "twitter:title": "Brian Douglas", + "al:ios:app_name": "YouTube", + "og:title": "Brian Douglas", + "og:image:height": "900", + "twitter:app:id:ipad": "544007664", + "al:web:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?feature=applinks", + "al:android:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?feature=applinks", + "fb:app_id": "87741124305", + "twitter:app:url:googleplay": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", + "twitter:app:name:ipad": "YouTube", + "viewport": "width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no,", + "twitter:description": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it. My goal is to take specific concepts in controls and expand on them in order to provide an intuitive understanding which will ultimately make you a better controls engineer. \n\nI'm glad you made it to my channel and I hope you find it useful.\n\nShoot me a message at controlsystemlectures@gmail.com, leave a comment or question and I'll get back to you if I can. Don't forget to subscribe!\n \nTwitter: @BrianBDouglas for engineering tweets and announcement of new videos.\nWebpage: http://engineeringmedia.com\n\nHere is the hardware/software I use: http://www.youtube.com/watch?v=m-M5_mIyHe4\n\nHere's a list of my favorite references: http://bit.ly/2skvmWd\n\n--Brian", + "og:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", + "al:android:app_name": "YouTube", + "twitter:app:name:iphone": "YouTube" + } + ], + "cse_image": [ + { + "src": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj" + } + ] + } + }, + { + "kind": "customsearch#result", + "title": "Lecture - Wikipedia", + "htmlTitle": "\u003cb\u003eLecture\u003c/b\u003e - Wikipedia", + "link": "https://en.wikipedia.org/wiki/Lecture", + "displayLink": "en.wikipedia.org", + "snippet": "Lecture ... For the academic rank, see Lecturer. A lecture (from Latin: lēctūra 'reading') is an oral presentation intended to present information or teach people ...", + "htmlSnippet": "\u003cb\u003eLecture\u003c/b\u003e ... For the academic rank, see \u003cb\u003eLecturer\u003c/b\u003e. A \u003cb\u003electure\u003c/b\u003e (from Latin: lēctūra 'reading') is an oral presentation intended to present information or teach people ...", + "cacheId": "d9Pjta02fmgJ", + "formattedUrl": "https://en.wikipedia.org/wiki/Lecture", + "htmlFormattedUrl": "https://en.wikipedia.org/wiki/Lecture", + "pagemap": { + "metatags": [ + { + "referrer": "origin", + "og:image": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/26/ADFA_Lecture_Theatres.jpg/1200px-ADFA_Lecture_Theatres.jpg", + "theme-color": "#eaecf0", + "og:image:width": "1200", + "og:type": "website", + "viewport": "width=device-width, initial-scale=1.0, user-scalable=yes, minimum-scale=0.25, maximum-scale=5.0", + "og:title": "Lecture - Wikipedia", + "og:image:height": "799", + "format-detection": "telephone=no" + } + ] + } + }, + { + "kind": "customsearch#result", + "title": "Mount Wilson Observatory | Lectures", + "htmlTitle": "Mount Wilson Observatory | \u003cb\u003eLectures\u003c/b\u003e", + "link": "https://www.mtwilson.edu/lectures/", + "displayLink": "www.mtwilson.edu", + "snippet": "Talks & Telescopes: August 24, 2024 – Panel: The Triumph of Hubble ... Compelling talks followed by picnicking and convivial stargazing through both the big ...", + "htmlSnippet": "Talks & Telescopes: August 24, 2024 – Panel: The Triumph of Hubble ... Compelling talks followed by picnicking and convivial stargazing through both the big ...", + "cacheId": "wdXI0azqx5UJ", + "formattedUrl": "https://www.mtwilson.edu/lectures/", + "htmlFormattedUrl": "https://www.mtwilson.edu/\u003cb\u003electures\u003c/b\u003e/", + "pagemap": { + "metatags": [ + { + "viewport": "width=device-width,initial-scale=1,user-scalable=no" + } + ], + "webpage": [ + { + "image": "http://www.mtwilson.edu/wp-content/uploads/2016/09/Logo.jpg", + "url": "https://www.facebook.com/WilsonObs" + } + ] + } + }, + { + "kind": "customsearch#result", + "title": "Lectures | NBER", + "htmlTitle": "\u003cb\u003eLectures\u003c/b\u003e | NBER", + "link": "https://www.nber.org/research/lectures", + "displayLink": "www.nber.org", + "snippet": "Results 1 - 50 of 354 ... Among featured events at the NBER Summer Institute are the Martin Feldstein Lecture, which examines a current issue involving economic ...", + "htmlSnippet": "Results 1 - 50 of 354 \u003cb\u003e...\u003c/b\u003e Among featured events at the NBER Summer Institute are the Martin Feldstein \u003cb\u003eLecture\u003c/b\u003e, which examines a current issue involving economic ...", + "cacheId": "CvvP3U3nb44J", + "formattedUrl": "https://www.nber.org/research/lectures", + "htmlFormattedUrl": "https://www.nber.org/research/\u003cb\u003electures\u003c/b\u003e", + "pagemap": { + "cse_thumbnail": [ + { + "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTmeViEZyV1YmFEFLhcA6WdgAG3v3RV6tB93ncyxSJ5JPst_p2aWrL7D1k&s", + "width": "310", + "height": "163" + } + ], + "metatags": [ + { + "og:image": "https://www.nber.org/sites/default/files/2022-06/NBER-FB-Share-Tile-1200.jpg", + "og:site_name": "NBER", + "handheldfriendly": "true", + "viewport": "width=device-width, initial-scale=1.0", + "og:title": "Lectures", + "mobileoptimized": "width", + "og:url": "https://www.nber.org/research/lectures" + } + ], + "cse_image": [ + { + "src": "https://www.nber.org/sites/default/files/2022-06/NBER-FB-Share-Tile-1200.jpg" + } + ] + } + }, + { + "kind": "customsearch#result", + "title": "STUDENTS CANNOT ACCESS RECORDED LECTURES ... - Solved", + "htmlTitle": "STUDENTS CANNOT ACCESS RECORDED LECTURES ... - Solved", + "link": "https://community.canvaslms.com/t5/Canvas-Question-Forum/STUDENTS-CANNOT-ACCESS-RECORDED-LECTURES/td-p/190358", + "displayLink": "community.canvaslms.com", + "snippet": "Mar 19, 2020 ... I believe the issue is that students were not invited. Are you trying to capture your screen? If not, there is an option to just record your web ...", + "htmlSnippet": "Mar 19, 2020 \u003cb\u003e...\u003c/b\u003e I believe the issue is that students were not invited. Are you trying to capture your screen? If not, there is an option to just record your web ...", + "cacheId": "wqrynQXX61sJ", + "formattedUrl": "https://community.canvaslms.com/t5/Canvas...LECTURES/td-p/190358", + "htmlFormattedUrl": "https://community.canvaslms.com/t5/Canvas...\u003cb\u003eLECTURES\u003c/b\u003e/td-p/190358", + "pagemap": { + "cse_thumbnail": [ + { + "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRUqXau3N8LfKgSD7OJOvV7xzGarLKRU-ckWXy1ZQ1p4CLPsedvLKmLMhk&s", + "width": "310", + "height": "163" + } + ], + "metatags": [ + { + "og:image": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png", + "og:type": "article", + "article:section": "Canvas Question Forum", + "article:published_time": "2020-03-19T15:50:03.409Z", + "og:site_name": "Instructure Community", + "article:modified_time": "2020-03-19T13:55:53-07:00", + "viewport": "width=device-width, initial-scale=1.0, user-scalable=yes", + "og:title": "STUDENTS CANNOT ACCESS RECORDED LECTURES", + "og:url": "https://community.canvaslms.com/t5/Canvas-Question-Forum/STUDENTS-CANNOT-ACCESS-RECORDED-LECTURES/m-p/190358#M93667", + "og:description": "I can access and see my recorded lectures but my students can't. They have an error message when they try to open the recorded presentation or notes.", + "article:author": "https://community.canvaslms.com/t5/user/viewprofilepage/user-id/794287", + "twitter:image": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png" + } + ], + "cse_image": [ + { + "src": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png" + } + ] + } + }, + { + "kind": "customsearch#result", + "title": "Public Lecture Series - Sam Fox School of Design & Visual Arts", + "htmlTitle": "Public \u003cb\u003eLecture\u003c/b\u003e Series - Sam Fox School of Design & Visual Arts", + "link": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series", + "displayLink": "samfoxschool.wustl.edu", + "snippet": "The Sam Fox School's Spring 2024 Public Lecture Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like ...", + "htmlSnippet": "The Sam Fox School's Spring 2024 Public \u003cb\u003eLecture\u003c/b\u003e Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like ...", + "cacheId": "B-cgQG0j6tUJ", + "formattedUrl": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series", + "htmlFormattedUrl": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series", + "pagemap": { + "cse_thumbnail": [ + { + "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQSmHaGianm-64m-qauYjkPK_Q0JKWe-7yom4m1ogFYTmpWArA7k6dmk0sR&s", + "width": "307", + "height": "164" + } + ], + "website": [ + { + "name": "Public Lecture Series - Sam Fox School of Design & Visual Arts — Washington University in St. Louis" + } + ], + "metatags": [ + { + "og:image": "https://dvsp0hlm0xrn3.cloudfront.net/assets/default_og_image-44e73dee4b9d1e2c6a6295901371270c8ec5899eaed48ee8167a9b12f1b0f8b3.jpg", + "og:type": "website", + "og:site_name": "Sam Fox School of Design & Visual Arts — Washington University in St. Louis", + "viewport": "width=device-width, initial-scale=1.0", + "og:title": "Public Lecture Series - Sam Fox School of Design & Visual Arts — Washington University in St. Louis", + "csrf-token": "jBQsfZGY3RH8NVs0-KVDBYB-2N2kib4UYZHYdrShfTdLkvzfSvGeOaMrRKTRdYBPRKzdcGIuP7zwm9etqX_uvg", + "csrf-param": "authenticity_token", + "og:description": "The Sam Fox School's Spring 2024 Public Lecture Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like social equity, resilient cities, and the impact of emerging technologies on contemporary life. Speakers include artists, architects, designers, and critics of the highest caliber, widely recognized for their research-based practices and multidisciplinary approaches to their fields." + } + ], + "cse_image": [ + { + "src": "https://dvsp0hlm0xrn3.cloudfront.net/assets/default_og_image-44e73dee4b9d1e2c6a6295901371270c8ec5899eaed48ee8167a9b12f1b0f8b3.jpg" + } + ] + } + } + ] +} diff --git a/backend/open_webui/retrieval/web/testdata/searchapi.json b/backend/open_webui/retrieval/web/testdata/searchapi.json new file mode 100644 index 0000000000000000000000000000000000000000..fa3d1c3d74097aaef3d975ff6af845b6c1370b17 --- /dev/null +++ b/backend/open_webui/retrieval/web/testdata/searchapi.json @@ -0,0 +1,357 @@ +{ + "search_metadata": { + "id": "search_VW19X7MebbAtdMwoQe68NbDz", + "status": "Success", + "created_at": "2024-08-27T13:43:20Z", + "request_time_taken": 0.6, + "parsing_time_taken": 0.72, + "total_time_taken": 1.32, + "request_url": "https://www.google.com/search?q=chatgpt&oq=chatgpt&gl=us&hl=en&ie=UTF-8", + "html_url": "https://www.searchapi.io/api/v1/searches/search_VW19X7MebbAtdMwoQe68NbDz.html", + "json_url": "https://www.searchapi.io/api/v1/searches/search_VW19X7MebbAtdMwoQe68NbDz" + }, + "search_parameters": { + "engine": "google", + "q": "chatgpt", + "device": "desktop", + "google_domain": "google.com", + "hl": "en", + "gl": "us" + }, + "search_information": { + "query_displayed": "chatgpt", + "total_results": 1010000000, + "time_taken_displayed": 0.37, + "detected_location": "United States" + }, + "knowledge_graph": { + "kgmid": "/g/11khcfz0y2", + "knowledge_graph_type": "Kp3 verticals", + "title": "ChatGPT", + "type": "Software", + "description": "ChatGPT is a chatbot and virtual assistant developed by OpenAI and launched on November 30, 2022. Based on large language models, it enables users to refine and steer a conversation towards a desired length, format, style, level of detail, and language.", + "source": { + "name": "Wikipedia", + "link": "https://en.wikipedia.org/wiki/ChatGPT" + }, + "developer": "OpenAI, Microsoft", + "developer_links": [ + { + "text": "OpenAI", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=OpenAI&si=ACC90nwLLwns5sISZcdzuISy7t-NHozt8Cbt6G3WNQfC9ekAgItjbuK5dmA2L3ta2Ero3Ypd_sib6W4Pr5sCi7O_W3yzdqxwyrjzsYeYOtNg2ogL1xVq9TKwgD48tL7rygfkRfNyy4k-R5yQgywoFukoCUths6NdRX69gl50cvd6dpZcMzVelCxT7mxXlRchl6XkueG326znDiZL-ODNOysdnCc4XoeAQUFtbaVjja6Vc7WkQF4X8rUdbDKPVU9WyLOV765d8Y777kMI7-nXGGyD7xXJX5E3HA%3D%3D&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAHoECD0QAg" + }, + { + "text": "Microsoft", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=Microsoft&si=ACC90nyvvWro6QmnyY1IfSdgk5wwjB1r8BGd_IWRjXqmKPQqm-SdjhIP74XAMBYys4zy1Z9yzXEom04F9Qy-tMOt2d-L6jIC5cXse6I528G870-4sF-DZYAPj0F1HoGTUOqpWuP7jbEPm3w_-mCH0wVgBHBGCgxRrCaUn8_k2-aga9V9JD6hkq2kM8zVmERCqCM8rqo3bNfbPdJ-baTq4w8Pkxdan3K--CfOtXX--lTjJtO6BnfG2RdpY_jBfy3uZZ7DeAE4-P4rvKuty6UL6le4dqqDt-kLQA%3D%3D&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAXoECD0QAw" + } + ], + "initial_release_date": "November 30, 2022", + "programming_language": "Python", + "programming_language_links": [ + { + "text": "Python", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=Python&si=ACC90nyvvWro6QmnyY1IfSdgk5wwjB1r8BGd_IWRjXqmKPQqmwbtPHPEcZi5JOYKaqe_iu1m4TVPotntrDVKbuXCkoFhx-K-Dp6PbewOILPFWjhDofHha-WRuSQCgY7LnBkzXtVH7pxiRdHONv3wpVsflGBg_EdTHCxOnyWt1nDgBmCjsfchXU7DKtJq159-V0-seE_cp7VV&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAHoECDYQAg" + } + ], + "engine": "GPT-4; GPT-4o; GPT-4o mini", + "engine_links": [ + { + "text": "GPT-4", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=GPT-4&stick=H4sIAAAAAAAAAONgVuLVT9c3NMy2TI_PNUtOX8TK6h4QomsCAKiBOxkZAAAA&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAHoECDUQAg" + }, + { + "text": "GPT-4o", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=GPT-4o&stick=H4sIAAAAAAAAAONgVuLVT9c3NCyryEg3rMooWMTK5h4QomuSDwC3NAfvGgAAAA&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAXoECDUQAw" + }, + { + "text": "GPT-4o", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=GPT-4o&stick=H4sIAAAAAAAAAONgVuLVT9c3NCyryEg3rMooWMTK5h4QomuSDwC3NAfvGgAAAA&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAnoECDUQBA" + } + ], + "license": "Proprietary", + "platform": "Cloud computing platforms", + "platform_links": [ + { + "text": "Cloud computing", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=Cloud+computing&stick=H4sIAAAAAAAAAONgVuLSz9U3MKqMt8w1XsTK75yTX5qikJyfW1BakpmXDgB-4JvxIAAAAA&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAHoECDcQAg" + } + ], + "stable_release": "July 18, 2024; 40 days ago", + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALoAAAC6CAMAAAAu0KfDAAAAaVBMVEX///8AAAD7+/uysrJ5eXmoqKiYmJhZWVn4+PiVlZXw8PDj4+P09PTr6+vNzc2vr6/V1dWMjIxmZmY7OzvExMSCgoIgICBMTEygoKBUVFQUFBQxMTG4uLhDQ0O+vr7b29snJydubm4LCwtts+PWAAAPGElEQVR4nM1d6YKiMAxWBKlcIocoooDv/5CrjjpNmrTlcNz82x2Oz5KmX46mi8VMIoI42ZbZ+VRfl9e6891108aRM9fjPyVOmHhZvVSl2hVp8G10Gom3uyMB+yldf0i+jZCRcOdfeeAPOfUr8W2YqoTZ3oD7IfXJ+zZSKE7Y2+B+SvMfKX24oWYmL8fL/2JvPM3cpKV2w2+DvkvgmiYnJefLt3EvFsl5BPC77L6tNNuRwG/Sf1VpxFqHrd53WoN5TL+HPNgxoPb9ZnspiiRJiqLx2CU2/9rqGpUkoNMhDQN50RRxeMlo7MV/hHxfxszlTUUY/y5ZOEIEaXJZeQfvkrRRJD4/fR0CuX/QUZTEPamfyCsr+D9VuUrC6KPQ1RlaHUxvTGgVUxWp9JLP8bQVft11Z2HsxKWioBJy6g8fMp6J8u0LOx0NXEvsd/SfAB/76DVH+3sLa+y3ydPOPmk38A373YB720HUoZx53Urg4+uD/diEh2EEedkdOHM7SpCia00iFM92mkriz7hwHeCj7bWlPY0hyMvrYS7kYQce7Frf14/B/ZBqJqWB60pmqS3hwcrvZqSbZbamwEKcW6ubxErnBea+65al2x9VpvDGPodbBTS9totNJBmr5P4hScM4iKIoiOMwLdbMb9xPxx4CG5HZ3OL0DPBrVqjBSEc0NPpmKvQLGAqL6RN4NO668thpEmwoIzrRSArgNJjtIjeEy2ylXceiA+YaN5nmVQXgWUZenTChjvxi/F7hocN3WdoEUkTRy49aGy4PSsZkrK04oULyltk4++6EG0ScrnoAYsvQlcyazB7wrSOiN6L1VMK3061GIs1p4Mchsy3BDxlqZsRlp+jdTXSWNmV8ObMXCKXFSjMsTHxxyS9f8ZMm2jFDbqfksmCFt6ZMNwl9hnyU7C0rxpuo4hFOTwCnem2tMoKLcN1oOn2Hw0VQzyOXlBg+z7ezMk7BfPibnOgFomWU/LweHZ5I4bhbsfdooyGqZ+rXxx41n+/e64TVZNEAGGeL+aIuCbJQQYAtt+on0zx7+CW3xutTveuukkbnzKxBxeRQFvyEpnFoDK67Qr1C+rpuMxX3TVLwSIOR2Zr8MUxgBKle+1Kv5CIpisTCZoCg2Vl7qRJTvEsuT3XsH12o35oZDGJR3u6qs8aoUS14um6iFoS25KtQ/u14sihU6WY/Ddnd+MUs697kNjtgfdH4CQrrufu1An62FbpHjVrvDHxDDoRdS8PcAwFaPsSphgXz9f3JzgDoe8OqJwq8Aug/EXg3sxzeVqIeI3db9XY99MzgwCdEsNq/6FTek/0tbkXFtGX/CipaQz+t9EMebshFt3Y13mcsKzFD/VL0wNN7AG2hl4almnBbXu/SzA/Z+NKenoOMnEQZ7KD7BrpC2ABJui2nNXJQhE65IuZ3lp5kBd3XmgonNGZkzikNPpYvohZUlCU6yl9mOvRwbRM03dFWXr6EoGARHJQKPGQy9ANill1PY89JlZdnyFp9C0xSofjqROgpZjl+G4XMjM0JjZBVuVS88wgm85H5nARdyQzsf/jNhQkznRWtkXlVr3wWmODq0V+10F9ZPRp6jIOm53fuLMKhqffIIlMla8RRsY7gk17xLxsNXQ2abmRYbUlHJrstANhKf8rx0gHsjwJutMIkuFg2Q2bZKRhf0pdVXvZlThg6sOmVok7jRl30yCB2BFcRDY29Pv5i1EF3ALNQF6wx0APM4zsuPlzSwYRl+RpCHXTgjqpzeAR0pRTj5PI0oaUDhMvTU+Vl6FjXZWRXRdNHQFcKYPQRMMEUWi2zh8rL5g9lUkHcRbU+g6EHO8Vkn3Z6Nhxvaa2p7x9Lng4Z1IlCfhPFiIdB90i6YkpXxkzgr96BehAXrqbyhNpTDGgAdKEW+7wfbSiOCo+0ytfyyrUBxgAEdU/UQ+2hc5mB55AZUnErbcTwIZA5xvIcwRxgEPRgg7yJHP3bqPIen3L/EcgL25z90xDomRo03cQxqltaVoaYZ6gvzkMhAUC9SBfRCnqHE83VwwSHyn8b/MBWVxDhQ7MOqDr5NCvoGOGbhTQYPFuJ+hRNNQeyf3JBdzUT9Fyu3IoPSOX3nh58wGXTsCMh20Y6TzYYOnYyUxzi8Q1WXrnhR5Cqg4AkHRMfCL1ScxlOgrTm6usDpXTmGMUchS6MOwI6lyVUUg6uQeWJSD9iMKKX/ka/dgD0nM/mC6zBV6PKK8QGUhgAnaCNg6AzkZSnqCqvDZTejJ/CKEF9wowKczSVcIsE2T1TbkBZ0sDrAXR64ttBr7cWFQwRrjjpDMn/Ffa8JZVx5B9GB4GtoB8tSy8ivOBc9dsicS5UJoiyXSfZlyX9skOOAic/t2oTrDjXIvm4cnApnwDdMjHtUMxWO71TuCacfq+dicNMgr7M1xp9Q+5L+f5GgDnSWYPPQ79nLPl74Fyt33QA6BJp2D8DPeuRsTnz2xxgru3toAIviax4/Qz0w6JBH+DKlqA4cG16aTvgX93fQV8TdPjMqTysoXgbQjmOXFOeL4COVXIKdMKh85nEK4wDvtYlMIHJxU1+Ps7TT4N+3w2BwNOVnAGw7i9yHsgqV1E3AiZxhgvIVOg3dotTBCQdATa8e/1vaboPjQzYNDQd+iJeo5GnWCCwJvUrhglKSciEMNqQfFr/XjQDdCX0pCQA7gJSO2+yBSg9GZRVUkLvgZkF+kIUQGuoWDwIi/gvSwRcADWr8bgRs/6unRP6AuUYqTvJ8hJYWM9UbihB8J+GBrNBl5WStHNAr99TsgeYmAILsUamIL/vDZ0NuqyTtP8h6/XbB4YFJRmHot0gn+W4fa8VfwAdxHVfhgKVCLA7GhwcQqyzF5I/gC57Fr/7NxLA4qiE0ku4BPkfQJfTu9c3YxHQsOq6FERY5f8MOigv+bXhqOK60nlcLRUR/AvoMs+UaCAq+tOWXFMJ8sFu9QjovXSJbMJRlEy/j0k0OD4yOJgxArqs1XJCDK5LN+Ohmat3wRHBoSGkEdDlS0BZqrLfb6MPZ6Y4QT4scDcCumwdXRB9VFJQuAgEI1G4wZBw6UToPRimQAms6kon7xLjEGLObxXDQeo5R/2GRN3uuTdkC2Ncv2idGhgBXWZoJRojKneWM6WTL2l9BMomITMOuuxoohwEk+A3dYHwsJU3psFGQpe/sCaZJwtdOvkrMa7G0CcfR0OXlQIpMp/lPhqMvFITokn5jocu3w790FBXGXE0JcgtE+3H10o2HHokPwdVC+jrIkwd6mzKG26+ycsCD4cu76rP4UgyhXtv6UzZQlNRSb4JpjBHeZaimIu5/ZtvUHl9Kc/DUk2ALisFtI2OzHuZuVUbVF5TQHX6YTjjoYPtVdA2Br30p/Vix2zGMyXI6bK196o8Hrr8RU/QwAAK09xLJ+lS//3WlCAnigXfC8No6LFsBXy40IAdwQ/bw5ZO6hPkBXZeS8mSjYa+krUAhcdC2R78GMJ4S6uuLkEe4sZwR/BDx0IH6ow5Xii/8mXDudLJPbexfY3LKVAHgbHQQaKxQwSPhM6XTi6JnUTigoDvXXzRSOgBMHl43BjoCyXT9hIfd4lL0NyoCTdrJHS45mA7AXatgEWfVXmwraLF+zd9aivsOOhwX4MSOAE+EhouMmp0k/PmpcnOQUkJ0anvUdB78GTlW4I5rKz43Eb35y7DAq8BXH/AUdChI5Gpm5NkekPsBW6Y6sNzGCmNRfiCujHQYS8EakuGzFCpYk2FGb5HGNmVo6ZJxAjo6IO7RKgKzGLyvUzpJJS9vqXKYOgx9APIXcqAr9PeqDD0dri/NtRv3xwKPUBvJNdCQFg5Zu6smM04P2LqH/w7Kyyh442Ge/Kx5vKShxDM8CVnQ62uXM1gB11JojD+gsxXzpr4RULvJOo2eu81Xsk4bKBHa/wizgDI5rPWRezEheAGpq0YiEJbQE975R0c24blJdrZFmAfvDO0VIlxbyUjdLFRFPPIf1bZDLGtEp7ilNLHrA3tfoju4QboolF9tFwTQQQv2JgSW+mr8Kwz7GEIGmJi4x8LtgCEK2L163SDCUqs9sY+Rk5zV999aWqpQvmJSn9M0AaCsmGGrpTgt/Idvt4S31vU6HNIbUl650rzU9NC3Zma8oCr52gOTbfJIJ5t6JHQmdqHCWBH66m4BdOJuj6rX0q/i01jW14C03hDmk6r4mCH742DYhnaE0ZseutCyjCpX2u7o0NQdNSVa4rwuINo2kAIdEjy0d3ShOLwPYXpTYVjCZJUls1LUa+148jzdpQI2FPOXAM2ZkPcTewbjqMsYTXi7AKHyGL+AGeD3IIxMB2xTZ0X9JDhPU9D5rSTfMcPA+3CdO6wjooxut++XepDxIEZcm0Ok8qonDaWJxn8Cm65pq3rwVIwRk6fOY5UffEbvadIiqp3hjzMryiVnE+p2VZHP9Ki6ysvGNdPUe3o2FspPKfkp9I010A82Z3SBjJV8xKG7N1N4gsdprma3Cc8vaadf0Zwj8zTBylW3Kpv7h8IjVpvsegPxH71efDCo07ouYtnMYYpKCSbfLIDnf/NCketGIkKrrv9tbfSWvC9LDiiETvTqivzkjaMgyASURCHbeIxinLvbm9HPiBtmuOcQh0h8nu3LMs+04XxjpYnPcJt1KY+A3aSakN0Btnbnh6EdnFOcxHeEms76mnF/uQgqG/dXEfHOLtxR6ic7L86WrlnPJGTa2+lk8r+/bhtN73hdaTESsTSIPWAI7IE/qozH5FH9RvlxVRoJYuDI8amAxgGi5MOOQ/U3hOPlO13sx7v9JSwN/XL+RXXjj4JpROzLho6Cfwa5+lYqWyOj01VNZx8WgwrUeGVloa+NE23eK0+abZDqWj0YbJ10eJflUTzpdzVffwIdwS9y0zLqE4cEUVtUjTbbXNJQiGEs0gIVaorjryGZGGW2trzb4Q5VqEvwhgU9wdhwkTztEekfFTY/sHHndc8Tmu9f6cNe2qVJkLzcUm1J93tO/0huQMOA/yAcGEMGzGf3fBZUXepWMqgCNWHpBl1mPW3TAuUdvgR4gMI8mdFXIYd3F4bqgr+VJQEvE76EQHRj8q6s1Kb/cDg95+I2Bopcu3bHF/8FUnWuDeyLP6u+YRTMZfE6aWkjGXXr5L/dcB/RURxu1q7WV5fl9f6VPW7bRIHs/Gsf2zY1viSH96vAAAAAElFTkSuQmCC" + }, + "organic_results": [ + { + "position": 1, + "title": "ChatGPT | OpenAI", + "link": "https://openai.com/chatgpt/", + "source": "OpenAI", + "domain": "openai.com", + "displayed_link": "https://openai.com › chatgpt", + "snippet": "ChatGPT helps you get answers, find inspiration and be more productive. It is free to use and easy to try. Just ask and ChatGPT can help with writing, ...", + "snippet_highlighted_words": ["ChatGPT", "ChatGPT"], + "sitelinks": { + "expanded": [ + { + "title": "Introducing ChatGPT", + "link": "https://openai.com/index/chatgpt/", + "snippet": "We've trained a model called ChatGPT which interacts in a ..." + }, + { + "title": "Download ChatGPT", + "link": "https://openai.com/chatgpt/download/", + "snippet": "Download ChatGPT Use ChatGPT your way. Talk to type or have a ..." + }, + { + "title": "Pricing", + "link": "https://openai.com/chatgpt/pricing/", + "snippet": "Pricing · $25per user / month billed annually · $30per user / month ..." + }, + { + "title": "“What is ChatGPT?” article", + "link": "https://help.openai.com/en/articles/6783457-what-is-chatgpt", + "snippet": "How does ChatGPT work? ChatGPT is fine-tuned from ..." + }, + { + "title": "For Teams", + "link": "https://openai.com/chatgpt/team/", + "snippet": "ChatGPT simplifies lead qualification and forecasting ..." + } + ] + }, + "favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAAAAABWESUoAAABQElEQVR4Ac3PIYyDMBiG4VefPDtxEj0xM39qZl40mcPhMzONOjWNrqxA4UgmqklweBQKVfFdGhbSZZvfY5qmb35++DAbO4XQF7xjpN42s1oyXtlr2gN4SRpynnTaANtesy1tkOOR8aoAJ12J6ngmGkknCqn5gv0y8Jv03eYy+PEAu07jCQ66sDqqpohBCVb2PMtvSbeoxRJcLlIFVFKVBuOwBDdNxkzjEbKbVDwHvgZw8j+Qq2fVhhjkxB2g7JwqKJMRhUqo5Lol8OTxMbSsehXw45e9ao+J92EkGaFbBscxLqnbPRhYOVXr/53L+wTVaUDmNZ+tLNyDWgdWl3gxo7otHMYY5DYdwLc6gB18tVLBSVJD6qr6fsoBVt7wyCm4PxfiRyBTx5N8kCQP8DtrzysZrebG9ZLhnaILYbIbPss/4c/row+G/FAAAAAASUVORK5CYII=" + }, + { + "position": 2, + "title": "ChatGPT", + "link": "https://chatgpt.com/", + "source": "ChatGPT", + "domain": "chatgpt.com", + "displayed_link": "https://chatgpt.com", + "snippet": "ChatGPT helps you get answers, find inspiration and be more productive. It is free to use and easy to try. Just ask and ChatGPT can help with writing, learning,", + "snippet_highlighted_words": ["ChatGPT", "ChatGPT"], + "favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAAAAABXZoBIAAABGElEQVR4Aa3SIWzCUBSF4d8rDA6LnMfiMPjU18xiJjHPCxzBVePqaqsrK6sqK5qgnmjybzShzQKb4tjv3mvuwX/yHhya9i8cDgCXlziwKm99TnIM5RN+rlQvkO5Z97+wP1FpAbkadwwzWgAOW4L2rcppxoZLjc2i1xMEzZYzblMrbBILzpaQV0wYqUfcbNNk3+kZPibsaEek1oqjxj3DA6W8Y5uobs7kuggTphvNOKWq6/HQlQl70sF4oNaS2NNaMzxQ4Krt9rBPliMW82akubKqDFSuR9x9TiiF8QsybfnBLtDNePhQm3ifSOyAyhlvpKoZy0pzsuiM2kKSwlWNhKd/FiHsFsXtVrB5XbAAEHyN2jTv7+1TvgE1rn+XcUk3JAAAAABJRU5ErkJggg==" + }, + { + "position": 3, + "title": "OpenAI", + "link": "https://openai.com/", + "source": "OpenAI", + "domain": "openai.com", + "displayed_link": "https://openai.com", + "snippet": "ChatGPT on your desktop. Chat about email, screenshots, files, and anything on your screen. Chat about email, screenshots, files ...", + "snippet_highlighted_words": ["ChatGPT"], + "sitelinks": { + "inline": [ + { + "title": "ChatGPT", + "link": "https://openai.com/chatgpt/" + }, + { + "title": "Introducing ChatGPT", + "link": "https://openai.com/index/chatgpt/" + }, + { + "title": "Download ChatGPT", + "link": "https://openai.com/chatgpt/download/" + }, + { + "title": "ChatGPT for teams", + "link": "https://openai.com/chatgpt/team/" + } + ] + }, + "favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAAAAABWESUoAAABQElEQVR4Ac3PIYyDMBiG4VefPDtxEj0xM39qZl40mcPhMzONOjWNrqxA4UgmqklweBQKVfFdGhbSZZvfY5qmb35++DAbO4XQF7xjpN42s1oyXtlr2gN4SRpynnTaANtesy1tkOOR8aoAJ12J6ngmGkknCqn5gv0y8Jv03eYy+PEAu07jCQ66sDqqpohBCVb2PMtvSbeoxRJcLlIFVFKVBuOwBDdNxkzjEbKbVDwHvgZw8j+Qq2fVhhjkxB2g7JwqKJMRhUqo5Lol8OTxMbSsehXw45e9ao+J92EkGaFbBscxLqnbPRhYOVXr/53L+wTVaUDmNZ+tLNyDWgdWl3gxo7otHMYY5DYdwLc6gB18tVLBSVJD6qr6fsoBVt7wyCm4PxfiRyBTx5N8kCQP8DtrzysZrebG9ZLhnaILYbIbPss/4c/row+G/FAAAAAASUVORK5CYII=" + }, + { + "position": 4, + "title": "ChatGPT - Apps on Google Play", + "link": "https://play.google.com/store/apps/details?id=com.openai.chatgpt&hl=en_US", + "source": "Google Play", + "domain": "play.google.com", + "displayed_link": "https://play.google.com › store › apps › details › id=com...", + "snippet": "With the official ChatGPT app, get instant answers and inspiration wherever you are. This app is free and brings you the newest model improvements from ...", + "snippet_highlighted_words": ["ChatGPT"], + "rich_snippet": { + "detected_extensions": { + "rating": 4.8, + "reviews": 3113820 + }, + "extensions": ["Rating: 4.8", "3,113,820 votes", "Free", "Android", "Business/Productivity"] + }, + "favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADNklEQVR4AcXUA4wdURiG4VPbtu32mmvUtm3bts2gnNq2bbtBbSzn3n79M8nZrvdcZZO8yUTzPUMGIFmLOlBJTXhtqcPUUaoDxRwp16aObORuHcNpxmwnUjM5/uICam0PyKza2miJSmoGOlH0HlIGjwPUm9tOqrXdD6qt9RANEb39VEGPAXym/5YMK9ehxu5ahKgbH4I3m0rjdoDfBEj+EwDDyrUiiO9UR7ffAd8pMvzHAyJ3Ivr74R7AtD8SIRATURO1ttWBakuCCN4BqrDLAApRiAmAcflm1NilJUSwCAJqqcm8nBs7pRu4y+gCIAoRqdwJ07LtdCfUSSLUlEZqjBQbevwctVvX1QUA7zd8pkYIIfh41o2d0Xh7ICJOpAFOsic0ZHYBIIZQKynjaLQtCPLJVKCrRyQhaAjUYaqIMCBxxA5CqAgRRIjmUMapzBu7oHG0cZmPx2welU4AkARi6T7U3KWHanuAgshCV952hy/sJ1MmNs77Q5mcAHBEuIKwLD6Mmrv1yCS1QMftfsApJjLO+0A1cxjAEb5TwxRE/uX30PmkFrjAgJPRn7lQklMAXwL4TAtB8UVA927PIA8sBNxikC+mhHzMgwA+7j0tFEWXAN1GXkGksSzkMpXxdWBZ/L3LYLvMRBHfqLbCAD7uNT0MJRYDfYedQ4S5FOymonjvbcD7Slb86Fsa9psM9itJIuxUsPhLGG282BJg8ODjgL4QbKbieO+nxyc/NT55a/CughXfu5dLCrGGyir2GcYzPoTGYSiESFNJGtfhk69aSUH4qPG+YoKIc1Q58R+R+Hj8iJ5lYbuUArazKd/QkL9Tv2Lfqb9hpfGiSYzHh3hXxjv05/Di/e23GB8TB/Bxy4xw5YUbMfAwjRdJajw6Yun7KpaM37uWY/YbBDjpIICP023HxH47AV0ehJtLi4wfp0pR7H01M6OvwnGA/9Rf0v/xHTSeF2GWMviQ+PhzykoxJVcA1eZBKh5jvGxi47+oHnzULYCacyFN7is0Po9KRzG3Am7XbzopxFoeoZZyCY0foIrwIbcDZFPxzN+9qy2JZ/whZeADngJEP0k76gB1kOooOuwKIFn7B3LHHIJtp64TAAAAAElFTkSuQmCC", + "thumbnail": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIiAdHx8kKDQsJCYxJx8fLT0tMTU3Ojo6Iys/RD84QzQ5OjcBCgoKDQwNGg8PGjclHyU3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N//AABEIAFwAXAMBIgACEQEDEQH/xAAbAAABBQEBAAAAAAAAAAAAAAAAAgMEBQYBB//EADQQAAIBAwMDAgQEBAcAAAAAAAECAwAEEQUSIQYTMSJBFCMyUTNhgZFCUnHSFTRTVGJysf/EABkBAQADAQEAAAAAAAAAAAAAAAABAgMEBf/EACURAAICAAQFBQAAAAAAAAAAAAABAhEDEhMhIjFRUoFBYcHh8P/aAAwDAQACEQMRAD8A8sooorc8s0+l6ppbaRp1tqF3qNlNplxNNH8CvqnEgHh8jY4K43c8Grew6u0sx2UGoid4LWGwEfyEcxyRKwlbJ859PPuB7cVzo3pe21jpW9ke2jkvLh3W2nPcIhCBM5K8KctkL5cAiq/o/TkudMubmDT4b++F5FC0c8DzLbwMGJk7aEE+oBc+35VxT03m9vk64uaSLxuutNhuFltxMwWe0nkjW3C/EMgdJDksSDyjAknOwA481BseoOn9JtoILY3F40XxO6SSBkE3ciZQHXuHLZI3MNvHjxVje6Z05odvIur21msTXt1GQI5ZZWARCixSAjbgt5aqS+Syvun9FJttF0qW9inkmue1IvMcu0KuC2Mjzke3tVIxw5ck6+izc16ogw9RiebUZb+GKIS6K2m20VtGQkY3KVHJJxw3OTUrovqXT+m7aZ5rWe4uLi4jEoRwii3UHI5B3ZJOV4zgc1anpHSptJ0w28q3Dqly872MyyTXrJGj9uMcgEEsPGcAZGTWS6m0yPR9Xks4XkZBHG+2bHcjLIG2PjjcM4Nax0sS4L9RlJzjxMuX6pt7GwsbDR4I2WGK4tnuriI94QvMzAL6scoVzkHnPNUfUl7DqXUGo39tu7NzcvKm8YOCcjIqtoreOFGLtGcsSUlTCiiitDMKKKKAUruoIVmAJBIBxzXUZw2ULA4wSpxxXFXJp1Fz44pQboTtJ+piR/Wu7RgDJOPHNPrH9h+ppez/AJAUKOZFAIIKswKnKkHwfypDK2SSc5OSfvUwxk/Y006UolTsi0U4y5596boWCiiigCiilL5oBaj2qRGv7UzH71IHipM5sft0E1xFE0ixK7qpkbwgJxk/kK37QaN0wz20+LcveSJI17Zx3Us8KKgwFyBGjlnIb7Ac159BKYZ4pgquY3D7WGQ2DnB/Kt4uqaFrqqZjGiw3Bl7er3mwW0bnLrAEX5gGOFY/YBearKzTArfqUvV2i2mlJA9tHcW0jyvG1vcSrIXUYKyoygZRgft5BrNMM/1rQ9U6/barEttYxz9lby4ue5ORnMjcKij6UAA48kk5rO5qY3Rni1n4RiQc5phxzmpUnJqO3ihaLG6KKKFgpS+aTXV80A8nvV/0qmktqZm16VFsIYyzRszZlYkKFAX1H6t3A/h54rPKcGtBp+mG9jYW6WG+NYsLcSyK8pePecYYDjn9BRiMW5WWT2WiW+rW9vHcWVyqafKQ7XHyZrlWkCdxgRtBAU+QPHIzUqXTem7oRRteWlnc7i0ht70GH8SFSuXBPh3YHwNp+oc1XHprUBjNnpWSSAPjGOcDP+pjxSX6fu07bG303Y8ywlhJL6WL7PBbPk1XyaqL7S5TR+lBG8IvY3f6e82pQowy0ByM+n0q0wzyDtYecYhDSOmorSV/8UW5lNs5hBuo4w8mzcDjymGyu1/q8ik3fTEkd0sNqtjL3ACm8TKSTkY4c8cYz4JKjyRTMfTV60gR4NJjJGctcv8AoOH8nn9qjyS4vtMw58VHPipd2UKwSJGI+5HuKqSQDuYcZJPsPeoh8VcwSoRRRRQkKKKKAUDUxb3KqJbaCRlULuYNkgDA8EewA/SoVGaAnC6j/wBlbfs/91KF2i+LO3Hvxv8A7qghjXd1BbLCbUWnk7txDFLJ/PI0jN+5amjdRY/yVr+z/wB1Qy1czmlC2PXE5nZSURFVdqqg4AyT/wCkn9aYJozXKAKKKKAKKK7QFpHoVydCk1iXK2wC9rau7eS5Q5/lA2nk/dceeIkGn39yiPbWN3Mj52NFAzBsecEDnHvWv6mJXobTFHhfg88fVut3fn+ngY9sZzgYXply9j0dp93B+NbyzXClmYhnVJtmRnwp5AGBkknOTmmbY6NNZqMX8FeB4ozaXIeb8Jey2ZP+ox6v0rq2N42zbZ3LdyQxJiFjuceVHHLDB4816DDqD3g0SSeJGkkgGXDOCO8q274O70+hARjGGyacj6gur97dZooQLzUZ7GXZvHywJgNvq9LfPf1DngfnlmZKwY9TzcW1wySOtvMUjYI7CMkIxOACccHPsa7LZ3cIlM1pcRiFgkpeJl7bHwGyOCfsa9CXqK5srW+1OC3txcfFx3JBDFC8sy7sjdz+CmPcc85NNa/rVxFpms2sccOyxmis4S4MmUKxElwxIc/KXlgfJ+9TmZGjGuZidG05tV1KKxVzG82VVthbDAEjIHOOOT7eccUzf2c+n3b2l2mydApdc5xuUMB+xFWel302rdaWd/cduOa81OJn7Uaqql5BnC4x7++c+TkkmpnX57mq2U2MGWxRio8D5ki8Z5/hzySck1N7lHFZbMxRRRUmR//Z" + }, + { + "position": 5, + "title": "ChatGPT on the App Store - Apple", + "link": "https://apps.apple.com/us/app/chatgpt/id6448311069", + "source": "Apple", + "domain": "apps.apple.com", + "displayed_link": "https://apps.apple.com › app › chatgpt", + "snippet": "This official app is free, syncs your history across devices, and brings you the newest model improvements from OpenAI. With ChatGPT in your pocket ...", + "snippet_highlighted_words": ["ChatGPT"], + "rich_snippet": { + "detected_extensions": { + "rating": 4.9, + "reviews": 1026513 + }, + "extensions": ["Rating: 4.9", "1,026,513 reviews", "Free", "iOS", "Business/Productivity"] + }, + "favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAC5UlEQVR4Aa1XQ3hkQRjc+ynX2OZtbfu+tm3b1nlt27a9O4qNS5xxbdd+cTKvXydT31fJoPuvmvf6/ejw86dBlX6CwwQXCq6t5cLaz/xV4+ld6F8r9NdgsCAjIwf5+UUoLCwBydf8jN+JNQbBddzjDQM+gocErRSyWm2QgWu4lntq9/q01UAfwYKCgmK43W6ognu4lzEE+6oamCboLC0tR3vBGIwlOF2vgZm5uQWoqamBXrhcLpw5cxZ79uxFKxCxrGBMxpYZ6Eu33KAXNDp+/AQEBgbzv8Y6Kxi7+e1ofuAKVS/7zp27KE7i6dNnem5HAbVaM3CYh0YF/PWRkdEUpxHoQe3BPNTcQJCgTc9pT0tLh8VigdPpBLFv3368evVKBC7A16/fkJmZKX06qCXo39jAej67Wnjx4iVGjBiJ0NBwBAeHYsCAgTh48BCuXLmCKVOmIioqBrwS4eGRGDduPMxmMzyBWtRsbMCglWSePXuOkJAwCuhmnz79YLVaPSUrGjDWGQhgCvWEyspKdOrURUk8JiYO799/0Exg1KQ2DQxjHveEO3fuKomTPBcyUJPaNLCQxcQTNm3arGzAYDBABmoK7UU0sE7rAC5dukxJPCgoRPy6DMhATWpLDWzbtl35Cty//0DBgOQW3LhxU9nAsGEj4HA4dN0CySHkwvy6bKfECRMmISsrS34IZY8hMXnyFAZV5rFjx6WPoa5E9PnzZ2XxpKQUlJaWaiUik1IqXrBgkZKB06fPwBOKiv4fwA3Ni5FdK3NVVFSgd+++usRnzJilXIzII7JynJOTAxaa7t17Yt68+bh37z6+fPmKCxcuYvToMejVqzdWrVrNMi0rx4cVGxIFKDQkCi2ZAhRaMklTavWqeF6epCltxuneasvLyurb8lmqg0lfLw4m/dozmh0RtBUV6R/NuJZ7avf6eGs4ZeIwMoVmZrYcTvkZv+MarlUZTlUZIDi8diRfX8uFtZ8FqMb7Bx+2VJbBTrlcAAAAAElFTkSuQmCC" + }, + { + "position": 6, + "title": "What is ChatGPT and why does it matter? Here's what you ...", + "link": "https://www.zdnet.com/article/what-is-chatgpt-and-why-does-it-matter-heres-everything-you-need-to-know/", + "source": "ZDNET", + "domain": "www.zdnet.com", + "displayed_link": "https://www.zdnet.com › ... › Artificial Intelligence", + "snippet": "ChatGPT is an AI chatbot with natural language processing (NLP) that allows you to have human-like conversations to complete various tasks. The ...", + "snippet_highlighted_words": ["ChatGPT"], + "date": "Jun 17, 2024", + "favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAMAAABF0y+mAAAAMFBMVEXQ/0rQ/0vM+0oDAxDV/0yixjxlfCpTZiV/nDK75USTtTg7SR/F8Uiu1UAfJhhyjC65DF56AAAAAXRSTlP3Yz+/2QAAANRJREFUKJHdkkGSxCAIRaOAAore/7YDJNPVSVfPAeYtdPEK5FMex1/U8pV/JutMatwVH9JaouTH1ok3SWsEjWGNBXve5JQ5qS+XJLJB8V3iZK8AZhBEAb5JBVjNEFPaU65tIibR1saiZ2XgbzpDH1H2ZtWttB316SSpZ5TeWymNtSfK501X2wFWo23mVR7DE49f2VYrkdPONVaEXmq9JHVIGUQSl/ia1jxFyOYT2YeUletTIyJ5SEIf4WoLfJNCE73EhJKoJHv9hHjFyQNzeefp8jvHD3ZbC4DWezICAAAAAElFTkSuQmCC" + } + ], + "inline_images": { + "images": [ + { + "title": "upload.wikimedia.org/wikipedia/commons/e/ef/ChatGP...", + "source": { + "name": "en.wikipedia.org", + "link": "https://en.wikipedia.org/wiki/ChatGPT" + }, + "original": { + "link": "https://upload.wikimedia.org/wikipedia/commons/e/ef/ChatGPT-Logo.svg", + "height": 800, + "width": 800, + "size": "1KB" + }, + "thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALoAAAC6CAMAAAAu0KfDAAAAaVBMVEX///8AAAD7+/uysrJ5eXmoqKiYmJhZWVn4+PiVlZXw8PDj4+P09PTr6+vNzc2vr6/V1dWMjIxmZmY7OzvExMSCgoIgICBMTEygoKBUVFQUFBQxMTG4uLhDQ0O+vr7b29snJydubm4LCwtts+PWAAAPGElEQVR4nM1d6YKiMAxWBKlcIocoooDv/5CrjjpNmrTlcNz82x2Oz5KmX46mi8VMIoI42ZbZ+VRfl9e6891108aRM9fjPyVOmHhZvVSl2hVp8G10Gom3uyMB+yldf0i+jZCRcOdfeeAPOfUr8W2YqoTZ3oD7IfXJ+zZSKE7Y2+B+SvMfKX24oWYmL8fL/2JvPM3cpKV2w2+DvkvgmiYnJefLt3EvFsl5BPC77L6tNNuRwG/Sf1VpxFqHrd53WoN5TL+HPNgxoPb9ZnspiiRJiqLx2CU2/9rqGpUkoNMhDQN50RRxeMlo7MV/hHxfxszlTUUY/y5ZOEIEaXJZeQfvkrRRJD4/fR0CuX/QUZTEPamfyCsr+D9VuUrC6KPQ1RlaHUxvTGgVUxWp9JLP8bQVft11Z2HsxKWioBJy6g8fMp6J8u0LOx0NXEvsd/SfAB/76DVH+3sLa+y3ydPOPmk38A373YB720HUoZx53Urg4+uD/diEh2EEedkdOHM7SpCia00iFM92mkriz7hwHeCj7bWlPY0hyMvrYS7kYQce7Frf14/B/ZBqJqWB60pmqS3hwcrvZqSbZbamwEKcW6ubxErnBea+65al2x9VpvDGPodbBTS9totNJBmr5P4hScM4iKIoiOMwLdbMb9xPxx4CG5HZ3OL0DPBrVqjBSEc0NPpmKvQLGAqL6RN4NO668thpEmwoIzrRSArgNJjtIjeEy2ylXceiA+YaN5nmVQXgWUZenTChjvxi/F7hocN3WdoEUkTRy49aGy4PSsZkrK04oULyltk4++6EG0ScrnoAYsvQlcyazB7wrSOiN6L1VMK3061GIs1p4Mchsy3BDxlqZsRlp+jdTXSWNmV8ObMXCKXFSjMsTHxxyS9f8ZMm2jFDbqfksmCFt6ZMNwl9hnyU7C0rxpuo4hFOTwCnem2tMoKLcN1oOn2Hw0VQzyOXlBg+z7ezMk7BfPibnOgFomWU/LweHZ5I4bhbsfdooyGqZ+rXxx41n+/e64TVZNEAGGeL+aIuCbJQQYAtt+on0zx7+CW3xutTveuukkbnzKxBxeRQFvyEpnFoDK67Qr1C+rpuMxX3TVLwSIOR2Zr8MUxgBKle+1Kv5CIpisTCZoCg2Vl7qRJTvEsuT3XsH12o35oZDGJR3u6qs8aoUS14um6iFoS25KtQ/u14sihU6WY/Ddnd+MUs697kNjtgfdH4CQrrufu1An62FbpHjVrvDHxDDoRdS8PcAwFaPsSphgXz9f3JzgDoe8OqJwq8Aug/EXg3sxzeVqIeI3db9XY99MzgwCdEsNq/6FTek/0tbkXFtGX/CipaQz+t9EMebshFt3Y13mcsKzFD/VL0wNN7AG2hl4almnBbXu/SzA/Z+NKenoOMnEQZ7KD7BrpC2ABJui2nNXJQhE65IuZ3lp5kBd3XmgonNGZkzikNPpYvohZUlCU6yl9mOvRwbRM03dFWXr6EoGARHJQKPGQy9ANill1PY89JlZdnyFp9C0xSofjqROgpZjl+G4XMjM0JjZBVuVS88wgm85H5nARdyQzsf/jNhQkznRWtkXlVr3wWmODq0V+10F9ZPRp6jIOm53fuLMKhqffIIlMla8RRsY7gk17xLxsNXQ2abmRYbUlHJrstANhKf8rx0gHsjwJutMIkuFg2Q2bZKRhf0pdVXvZlThg6sOmVok7jRl30yCB2BFcRDY29Pv5i1EF3ALNQF6wx0APM4zsuPlzSwYRl+RpCHXTgjqpzeAR0pRTj5PI0oaUDhMvTU+Vl6FjXZWRXRdNHQFcKYPQRMMEUWi2zh8rL5g9lUkHcRbU+g6EHO8Vkn3Z6Nhxvaa2p7x9Lng4Z1IlCfhPFiIdB90i6YkpXxkzgr96BehAXrqbyhNpTDGgAdKEW+7wfbSiOCo+0ytfyyrUBxgAEdU/UQ+2hc5mB55AZUnErbcTwIZA5xvIcwRxgEPRgg7yJHP3bqPIen3L/EcgL25z90xDomRo03cQxqltaVoaYZ6gvzkMhAUC9SBfRCnqHE83VwwSHyn8b/MBWVxDhQ7MOqDr5NCvoGOGbhTQYPFuJ+hRNNQeyf3JBdzUT9Fyu3IoPSOX3nh58wGXTsCMh20Y6TzYYOnYyUxzi8Q1WXrnhR5Cqg4AkHRMfCL1ScxlOgrTm6usDpXTmGMUchS6MOwI6lyVUUg6uQeWJSD9iMKKX/ka/dgD0nM/mC6zBV6PKK8QGUhgAnaCNg6AzkZSnqCqvDZTejJ/CKEF9wowKczSVcIsE2T1TbkBZ0sDrAXR64ttBr7cWFQwRrjjpDMn/Ffa8JZVx5B9GB4GtoB8tSy8ivOBc9dsicS5UJoiyXSfZlyX9skOOAic/t2oTrDjXIvm4cnApnwDdMjHtUMxWO71TuCacfq+dicNMgr7M1xp9Q+5L+f5GgDnSWYPPQ79nLPl74Fyt33QA6BJp2D8DPeuRsTnz2xxgru3toAIviax4/Qz0w6JBH+DKlqA4cG16aTvgX93fQV8TdPjMqTysoXgbQjmOXFOeL4COVXIKdMKh85nEK4wDvtYlMIHJxU1+Ps7TT4N+3w2BwNOVnAGw7i9yHsgqV1E3AiZxhgvIVOg3dotTBCQdATa8e/1vaboPjQzYNDQd+iJeo5GnWCCwJvUrhglKSciEMNqQfFr/XjQDdCX0pCQA7gJSO2+yBSg9GZRVUkLvgZkF+kIUQGuoWDwIi/gvSwRcADWr8bgRs/6unRP6AuUYqTvJ8hJYWM9UbihB8J+GBrNBl5WStHNAr99TsgeYmAILsUamIL/vDZ0NuqyTtP8h6/XbB4YFJRmHot0gn+W4fa8VfwAdxHVfhgKVCLA7GhwcQqyzF5I/gC57Fr/7NxLA4qiE0ku4BPkfQJfTu9c3YxHQsOq6FERY5f8MOigv+bXhqOK60nlcLRUR/AvoMs+UaCAq+tOWXFMJ8sFu9QjovXSJbMJRlEy/j0k0OD4yOJgxArqs1XJCDK5LN+Ohmat3wRHBoSGkEdDlS0BZqrLfb6MPZ6Y4QT4scDcCumwdXRB9VFJQuAgEI1G4wZBw6UToPRimQAms6kon7xLjEGLObxXDQeo5R/2GRN3uuTdkC2Ncv2idGhgBXWZoJRojKneWM6WTL2l9BMomITMOuuxoohwEk+A3dYHwsJU3psFGQpe/sCaZJwtdOvkrMa7G0CcfR0OXlQIpMp/lPhqMvFITokn5jocu3w790FBXGXE0JcgtE+3H10o2HHokPwdVC+jrIkwd6mzKG26+ycsCD4cu76rP4UgyhXtv6UzZQlNRSb4JpjBHeZaimIu5/ZtvUHl9Kc/DUk2ALisFtI2OzHuZuVUbVF5TQHX6YTjjoYPtVdA2Br30p/Vix2zGMyXI6bK196o8Hrr8RU/QwAAK09xLJ+lS//3WlCAnigXfC8No6LFsBXy40IAdwQ/bw5ZO6hPkBXZeS8mSjYa+krUAhcdC2R78GMJ4S6uuLkEe4sZwR/BDx0IH6ow5Xii/8mXDudLJPbexfY3LKVAHgbHQQaKxQwSPhM6XTi6JnUTigoDvXXzRSOgBMHl43BjoCyXT9hIfd4lL0NyoCTdrJHS45mA7AXatgEWfVXmwraLF+zd9aivsOOhwX4MSOAE+EhouMmp0k/PmpcnOQUkJ0anvUdB78GTlW4I5rKz43Eb35y7DAq8BXH/AUdChI5Gpm5NkekPsBW6Y6sNzGCmNRfiCujHQYS8EakuGzFCpYk2FGb5HGNmVo6ZJxAjo6IO7RKgKzGLyvUzpJJS9vqXKYOgx9APIXcqAr9PeqDD0dri/NtRv3xwKPUBvJNdCQFg5Zu6smM04P2LqH/w7Kyyh442Ge/Kx5vKShxDM8CVnQ62uXM1gB11JojD+gsxXzpr4RULvJOo2eu81Xsk4bKBHa/wizgDI5rPWRezEheAGpq0YiEJbQE975R0c24blJdrZFmAfvDO0VIlxbyUjdLFRFPPIf1bZDLGtEp7ilNLHrA3tfoju4QboolF9tFwTQQQv2JgSW+mr8Kwz7GEIGmJi4x8LtgCEK2L163SDCUqs9sY+Rk5zV999aWqpQvmJSn9M0AaCsmGGrpTgt/Idvt4S31vU6HNIbUl650rzU9NC3Zma8oCr52gOTbfJIJ5t6JHQmdqHCWBH66m4BdOJuj6rX0q/i01jW14C03hDmk6r4mCH742DYhnaE0ZseutCyjCpX2u7o0NQdNSVa4rwuINo2kAIdEjy0d3ShOLwPYXpTYVjCZJUls1LUa+148jzdpQI2FPOXAM2ZkPcTewbjqMsYTXi7AKHyGL+AGeD3IIxMB2xTZ0X9JDhPU9D5rSTfMcPA+3CdO6wjooxut++XepDxIEZcm0Ok8qonDaWJxn8Cm65pq3rwVIwRk6fOY5UffEbvadIiqp3hjzMryiVnE+p2VZHP9Ki6ysvGNdPUe3o2FspPKfkp9I010A82Z3SBjJV8xKG7N1N4gsdprma3Cc8vaadf0Zwj8zTBylW3Kpv7h8IjVpvsegPxH71efDCo07ouYtnMYYpKCSbfLIDnf/NCketGIkKrrv9tbfSWvC9LDiiETvTqivzkjaMgyASURCHbeIxinLvbm9HPiBtmuOcQh0h8nu3LMs+04XxjpYnPcJt1KY+A3aSakN0Btnbnh6EdnFOcxHeEms76mnF/uQgqG/dXEfHOLtxR6ic7L86WrlnPJGTa2+lk8r+/bhtN73hdaTESsTSIPWAI7IE/qozH5FH9RvlxVRoJYuDI8amAxgGi5MOOQ/U3hOPlO13sx7v9JSwN/XL+RXXjj4JpROzLho6Cfwa5+lYqWyOj01VNZx8WgwrUeGVloa+NE23eK0+abZDqWj0YbJ10eJflUTzpdzVffwIdwS9y0zLqE4cEUVtUjTbbXNJQiGEs0gIVaorjryGZGGW2trzb4Q5VqEvwhgU9wdhwkTztEekfFTY/sHHndc8Tmu9f6cNe2qVJkLzcUm1J93tO/0huQMOA/yAcGEMGzGf3fBZUXepWMqgCNWHpBl1mPW3TAuUdvgR4gMI8mdFXIYd3F4bqgr+VJQEvE76EQHRj8q6s1Kb/cDg95+I2Bopcu3bHF/8FUnWuDeyLP6u+YRTMZfE6aWkjGXXr5L/dcB/RURxu1q7WV5fl9f6VPW7bRIHs/Gsf2zY1viSH96vAAAAAElFTkSuQmCC" + }, + { + "title": "Introducing ChatGPT | OpenAI", + "source": { + "name": "OpenAI", + "link": "https://openai.com/index/chatgpt/" + }, + "original": { + "link": "https://images.ctfassets.net/kftzwdyauwt9/40in10B8KtAGrQvwRv5cop/8241bb17c283dced48ea034a41d7464a/chatgpt_diagram_light.png?w=3840&q=90&fm=webp", + "height": 1153, + "width": 1940, + "size": "93KB" + }, + "thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAABtCAMAAAAlHltpAAAAwFBMVEX////P6t38/Pz4+Pj09PTe8Ofr6+vw8PDJyczExMTk5OTY2NjMzMz1+vjM6dvV1dW7u7ve3t61tbWtra2lpaXg4OQAAACfn5/W7eGNjY3p9O+VlZV3d3fI4dSCgoJiYmKpvLO1yb9ra2tXV1dJSUmisqrB2MzR0ddBQUFrbXU0NDSPkJaIlo9venSQn5fb9+klKCZJUExZYl5+i4N3eIJQUVs5PEdARlOUlKWioa9YW2esrLk3PU7BwMkiKT6BgomhiaRPAAAOkElEQVR4nO1ci3bbNhKlAAQwgKnxjBmWsmI5kRPXjpPdJn2k7eb//2oHpB58SSalxGnPKWxRMs3BXAKDwZ0BqCw7upDjRZP0aeJZZhzWIfgxqtOBGU4IcdPlK+DEcIrazRHaM+mtzbk6QrICriAEa/R0+Qo4lTL6QO0R2jNGmVBHdXslQxiljgg6XZqstXNO2BHaTyjf2cZTc/MKRqsqQte176+//o8mGSftu6Cbt/XJwQattXFeq9peQsXmE6tU7O+MYCxEb3xU0ctgJUQ8eluClYHm1ke7T7bSrKWWBUhbcCm9jNLiG75kJDH4YIU0uSn3AmcSFcgQAS/lEquQNkgwylqw3gMOv0LKPeMH0fqwyBG+LfAYvQ0IOs8Ru8HbyUPhDgFXKJ8HlFIIFj/KcuFCIUtpZF7YuUv/C3uBU8Sdm9xDLhcuiduYW2lhEfJFrsocdJSFHlZPGSOEEsFwoKBrwherXuk3EzT9+xDwTJAkj1fhOCNECTzS9Gkti5XRA6bChMMLKFaQFLokzBQjwooEAq+hGT0wGBxHM2u54mqgp1slbq/DqCrEG85afVn7ZEX5I66iPTj1+sgSiiRJJR4keGP3NHi2yMuFl3M/L6O6dvMcXubF3JcF/ixMXOSxiMUwhkqzmdt5EXUBi6K4xv4tc/kyLopwvdhnYU3gIl4XKDMPZSzLIs7RWENRxqIIMA9zWdp5sPmeCiwoabhUAQSP3IJZGKmdwrFpJFgADSYOD4+6ry0Pkimv8DojrTPAU4UgwR32d7WNo2JrrDFGC5A4zgwi8EblORqLVQb7b2+LH1++sx8/QfVp0hVX0RqcidppZRwoUMbjm8EfCdxQMKCctsFzeMTwpqk+TbqycftygePJXnv/amHn87iwMF+8Qn+6mIcFvI45vtky5otwDA3cp/o06ZrqVIVWvxldf6LVKbDoqiOvT5JDTnGy6tOk/9E2/n3Kv8CfuvwL/BsU8myw/Fj/81HxH4bFn40EflD74fLsfFNmWLZ/nP84Dviw+Gz2wzjgbfFNDbPzHx4H/mK2Kcv7+3fvt381gG9ZL2neTA3r2UbjbLV6uNpKn++At3ln52Z22mezq8uO9pHA3z/M3i9nbdFac+QepMSAK3CwVgPyQgzdgLaBP6DqIeDIQjBe00YmKRvanKQB/N3Fh/+8u2yIpyBCMMGV4FoIxgTlXDFOQQjFG6IvVsvZuyHgyEu91kYjZIUvBA/GOoBOiz+8mN1f9oFToxXeKVIq5FY6ANdmAPjl/X8v3r172xLHYBmM93i/MSAVxpv23uRBY6xqDW3c8+X91WoI+MGyA37182onfj5k4wO1NbRjd93ctIFjjMeZIgwwWhSCc8YdtjgVPMWcjc56/35nZA3gQjptkH1qriwwAIZ/boncDvj72Yud+A44QUIrMfhQGMhoJ+Ue4Jezhu4N8IqVZSlIJpwwTgSe4DvRm9XN1dXyCg/4drPEw3LZMhVXJSSQNYNHQ5Ua7X1rp89m56vlcrVKgisUT9KryyZwa71Kxm25dLAH+Orn1f1qdXX/sFoPkxq4tXjLvspjRK6jt1HGuBO9vFo+oNIK/VV1WN60TaXipSkaFylSrzhoo8WvUCWCrwSX6cNVE3iG7YV9nnJCFasdBH5+ieUGf2bLZos7Zwg4rpmyDvs5FaX4VvQGJZaXeLy8XKYKzm9uWsCpUk6hoQgzkA14NkPpVC5vZrV4qqU5OEFzivEOViFMLyRfA8e7Xl5drlLzNYEfLC/Qfz9gk2OjLx+wux8esOOvmsCVjyCDDcEMhCjPZlcP99hPxQ2qXd3cr36+XN0vG8BZSnQZrCKitYiu+Bp46unKVpfL8cDPN6OjktgOj0aLcxzJGj2qGshhPJs1xTdvTVMRhGMNPDkF1rvzjfbmRLR1DYfLj7PB8qI1S04Xnzzlt8qLx3EjTRostc4R9G5YvOrqMezwgPiE0ksQPCWtJc+P1/SmV9koMXGX7y6Ej7CVnga8p318ORL43ZdPZ5vP4hf761Z6EnD62/TFmnV5/ntXdJxm+fl2m3qkX7582UpPa/Gzx6/ZU+gfvcrGqdQN82Swo+/ThsjRDY793IP0uAwDA+m3YrnGSKPwc+2v/+bBMpEAWtfsnDLGEqGpG28ScB0lHJs87K/hjtLMEbYaunK9sqyqg6jXPZCpDZsEd0i2J4DdFWWVkKYNYBRw6pAO7AWeYbgUeB68dmmtLpee24GWTSs99KgWFzILBVPQOvk1JiDFnMXQhUjuBHJ5ACEHAF4jzw4vj9HiM35x4TNodeRIr3LWCCP5jgLWplItADfTBUOVMgd5bhuraGRs6wudkeu3DunzdOD+9vOWLNDb4rYN3EifHE0qPh1UAOhVkdaiDEZXWzh//IYG5bhyGmN8JJeaO8dxHGjHjeIN98dMVrye/4TebTrwePvnVor9mn9qAafSSLRukB40hm5Oc4Duci55ZWlW5hjmvd2eo9lZivKD9D4EBxjFYQyo8dZDem8I2+yni4sLLltYxwGnoRFJqnLrm3bukGy2DJBsZziNYjzg/XB8a5w8S2EyVywNfSZSdJ+oPVOJ2TcuEyDLwqv2hphRflxxjO1qk7RoCqnHPdkCJ05wlwI3jBZBpBjQDKxDaxWsDG3tZ2gqhDBFGWcUQ/yMoYnwPiQusebORp5RLa6Ucaq2Oup0Arm2wdpUMDYHnFykDGjcIG3keR84KT2aRXsrw1mGguByn1JBEidlGaT2Q76e96aRccBBm7X/r+JqBM52wDPBFE6n1aQqhGCEiqF13YBmq9u2f5YpnCJU6k7hNMXOwhGihqYvknd3Tk3340zKLa4TucpZ2g5DKxSPrGBb2Tv1aO2H9l2cDNzakNKrEg6vmRN/ROj2XNFNyVLyZ13eVJt/pgHvTjlnGWicZtHf6H2bBihnhCUrzFjKU4mGG3tc3fNNefNm+/H5zquMLr2Yc0RgATHyKK2x6ACQEsF25E7SrLpxyMTQ7Y/O5aMiIsZ1Jgh6TEFFo88maX5+GnDy+1Tgypq0ZpBWDLhPmdkdl5ikuZen+tamosApQDqD9EUgl9GA/HOTpp2iuQ/lVK9ygupJV/dyBPUERI+pK3s64GnfUttYauDohTUDB84oCsoZZAUWu/fxCicA75H38cCJZcJk7ZmiBq65jBwplHWel947nFOQfTTmusau3Za+CcCHM1nIJQW6+LQAQwiStEyIvvUanr3FAEo2zWVzFU4NaSM3slONU3i1+7FJEHXi2lrinSHrbmy9PBm4WKiQI5v3XuTRh7RPNcTugho6og8XAZliFzhRCp0sQztRe6IxhZGNcCmuwU9IkTdVjwfO/hpkh4xXbBXjpkwZZPFaCNUlDxjxqTKGjDX/UQOPUoE3Hm934m6x8cD5b90z7RupMyLDdg9kjgGUM81k2LrFGRGMK41BzDTHcrKpKGlyJKvYcviGQ8ta6G/NdDr/8OEn14r8vrcf50aakFbwmUFLxAlKgulD0tY5097PPRF49+rRwIngvUl7imLobgP/9uywLiaYbvKw0uwkkgEwJuzJA7OIKlhAv0LrTxuNJ65IjAbOXC8fWbtDjN1T+G4HM5tZ9uXuT27uPv7PubvbX7S7u/u0dh9TVySOobVZSpfiYTDpSVp/9Ur55ROn+cdPCg+f0+Hl2uKeqMXDK070RTu3P0ozk3p9oNtDLf00g9PwPC9Je5L4zrR2M3Fsk1+kXyVBcgfKgG8/jnCS5knAlbUd13CWyWikMukxGB64SFujVOh64vmCZPAa5S+ayL8CcAI7PPXzV2zwyR/R86ZnGU4c0qUERRA27XXBEK2XQqEgXdXiLelpbqHLRTYrEr7gZR5sDBaQq4WY99PMQysSI22cafAxZ9OTntuyBzin3JGUSNNccZpSdK63updlL5FFxPaKxFkWNZIfT3FKVmnbibPaM4eEvu21/bVj5qcjvMoWeJcybmy8P3H19rgLBXm07X0lZ5n31rOAh6CiZU5JHAd4B52HEqocLxyxeLWtoEsuaxuXENLAx6jHCul8/cRZ916shPTIS8uAJ5As1evs8bLYan91pSvgSMp0WsDFl5K8WlLRHddAX1uSlSXi/9A4e5aeqGQCOTFnzIgqw68EGyAeIu919hTgvWc2189IjKjH2fWKRBMA2nhexjJYi28LXuQL8D7PfV88HpGtPVTqPVlpg2jQWhgm08NLw5dqHpMltxoumQrvjYahJ3nFcYn9daGqm8paL15p4OiBkwV7iaR+WLoIxoWideqp8iouhk6Prb0Ky+pHCGnG+xsPNyWluF3bCkYB1zj008vJ0PTkf/8UnBZgNUbjou2SJmiWAb3SonXqKYAbinF4WsNVvOkSJ2j2Od5zeUrM2Z1Px+XHTXrY0YDAaaCpOh0o3yLYj0RpiCZv72F4qphzqLJ0EOCNoR7DeK6dSHtzcQrpOBBIO9oDcbYZmD7FisSeUre4Q0qMPMFXcae1QSL4DsErIs/ky4zBdcPNfusViUOV7SCkvddkvShP+7wJjJUMef9Q0nOkrq9uKmkPtWNKpLVdJbhSUpi0H6OdxAQZooU2m//OmSyVvgIAXSVyI29zGzTGIdaa3LYoWV5gCPWqBbUmWQGUokpxgfesFU1v477742TgBFkZzURyLmklPn2oHrBgbd+XjKTD5itxDAJSBBRjETxEnfuy9AMhUK+4p1sD6klPEm8/K+72fa3DSNVjLjK3dzj1poO7vSPZ83SopSuSVX/fhko7TdKowLE9vMC97+H8o8oo4Lf+kzEf45/g5vkncB/D7brtalobvbGLvCx8ASEvdRl9HgZshah9xPGbAfefP2fEfv5M8XDLMnl7KxrAM8qIMBjK0LT4w4jBscEGV1V68dwJZZyRVptfdge+AdAMlln6IoTThsyU8hUGJ5U++rRKGOIRX0N0tOrTpOtgWRlhPCOcdAnONyzfNZA4SfVp0v8Cn676NOl/KvBTy/8BsqneOZjJbOsAAAAASUVORK5CYII=" + }, + { + "title": "What Is ChatGPT? Everything You Need to Know | TechTarget", + "source": { + "name": "TechTarget", + "link": "https://www.techtarget.com/whatis/definition/ChatGPT" + }, + "original": { + "link": "https://cdn.ttgtmedia.com/rms/onlineimages/chatgpt_screenshot-f_mobile.jpg", + "height": 252, + "width": 559, + "size": "12KB" + }, + "thumbnail": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIiAdHx8kKDQsJCYxJx8fLT0tMTU3Ojo6Iys/RD84QzQ5OjcBCgoKDQwNGg8PGjclHyU3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N//AABEIAFwAzAMBIgACEQEDEQH/xAAcAAEAAgMBAQEAAAAAAAAAAAAAAwQBAgcIBgX/xABBEAABAgQCBAoGBwkBAAAAAAABAAIDBBESITETFEFRBQciNmF0gaGy0SMkMnGR8AZSVGKSscEVQkNEU3KCouEz/8QAFwEBAQEBAAAAAAAAAAAAAAAAAAEDAv/EABsRAQACAgMAAAAAAAAAAAAAAAABEQJBEyFR/9oADAMBAAIRAxEAPwDmMJzGua6JB0gDiXC4tuFMq7FvEfBdDc2HLOa40tcYpNOynzVTwp2CxkNroLXFuZNRd0Lcz8scpUD/ACd5rpH51rli11cu5fp/tCU+xj8bvNYbPy7X3GXBF1aVNKUySh3LiN5hQutx/EugLn3Es+76DQ3NwBm4/jX3d7t6ipkUN7t6Xu3qD8v6RjhayD+x7rqu0ltnRT2u1X6TdkPRmCHWi68HPsVbhThaHwaIZjMivMS6mjAOWeZG9WNchAML4rWXtDmhxpgrpljEcmUxPfjJbPZNdLUpmWuz+PuWSJ63B0tXH913ZtWNbhUrp2U31CxrsE5TEM7PaCjVmk9X2pbH7rsO9bUm/rQMsqHP5oo9egCtZhmGeIWwnINK6wyn9wQbMbOXN0joFv7wa0+asqmJ2C6lJhhrliMUM7BH8xD/ABBBcRUxOQjSkdhqaDEJrsEgETDCD94ILiKprcM5RmYGhxQTkEkDTsqcsRigtooDEo4NL8TlgjYhcKgmnSEE6KG929L3b0EyKG929L3b0HkDaibUVQQ5IhyQegeJHmFB61H8a+9XwXEjzCg9aj+NfeooiIgrzkjKTwaJyXhxg2tt4rSua0miIdGMjNggMwrCuoAraqzbnAgMix2YZQoV1e4olRdohFLhc2ba4bfQHE9HetdNgQZlhycPVzln8VtpH3O9YmwTiGmBliMsMUue1xrMTZG7QfraisOmDUgTbWkF2BgHGmzsWNMQL9ba3AAky5zph+q2cYrWD1maJBpXQAn4UWXOeORrMy0jC7QZmvu6UGrYjza0zUK53s+rkCtUEyCQTNtpQV9XOa2D3gubrM0SRWur5dyGI4EERpugOWr16fq9iDR8UtFHzjeSaOpLkrbWKHGbZS3CkE49KzV5qNYmwbqD0GXdj71qYrqYzM20DGplqfm1AEY0I1xl2dTLmtKblu1zosQMhTLCa1oYOGGeKwHuMeG3WJoH6pg0DqZ40VxgLWAOcXEZk7UBgcGARHBzt4FFsiICIiAiIg8g7UTaiIIckRB6C4kGk/QGFT7XH8a++scvg+IzmDC63H8a6CoqKxyWOUqIIrHJY5SogiscmjO4KVEEVjkscpUQRWO6Esd0KVEEVjk0blKiCKx3QljlKiCKxyWOUqIIrHJY5SogiscljlKiDx1tRDnmE7QqgidoTtCD0JxGcwYXW4/jXQVz7iN5hQutx/GugqKIiICIiAiIgIiICIiAiIgIiICIiAiIgIiIPIkKNNNhQmQ4QcxsVzmcgGrqCvvw2LcxZkEuMlDx3yopl7lFCZGOjdDjNYXOIb6W20gZ9HvVh8KfcHF06x1uJ9caTls5W7BVEUzGjhphR5aFCNP6AaezBWODvo5wzwnK61wfwfEjy5cWh7XsFSMwAXAn4L858R8SmkiOfTAXEmitS3CvCUnA0EpwhNQIOJ0cOK5ranPAKTelh3jiM5gwqfao/jXQVz7iM5gwutx/GugoCIiAiIgIiICItHutpgTiBgg3RVROA/wY46NEcFnWxWmijYiv/mUFlFrDde0OoRXY4UK2QEREBERAREQEREHkBjZctbpIr2uLzfRlQ1v640UhgyVR6287/Qf9ViRlIMcyjYjT6WK8OIOwB1PyCscOcGS0jLh8APuMcs5Tq4Cvkqj8mO2CxwECI6I2mJcy0hRoiD0JxGcwYXW4/jXQVz7iM5gwutx/GugqKIiINIhOFBXL81m1qpPjRNO5t2AJwoFpp4to5QrX6oQfoWtS1qpse90MOL8SDsHksh78eX3DyQW7Wpa35KqGI8QwQ7Gh2BHOeGe33DyQW7W/JS1vyVUc54pyzj0DyS9+PL7h5ILdrfkpa1VL309vZuHklz6e3/qPJBbtalrVWufaTecxsHktQ593tn4DyQWYnJY4saHOAwBNKrZip3vuIv2DYPJSQXvMQAuqN1AgtIiICIiD/9k=" + }, + { + "title": "ChatGPT Tutorial - A Crash Course on Chat GPT for Beginners", + "source": { + "name": "YouTube", + "link": "https://m.youtube.com/watch?v=JTxsNm9IdYU" + }, + "original": { + "link": "https://i.ytimg.com/vi/JTxsNm9IdYU/maxresdefault.jpg", + "height": 720, + "width": 1280, + "size": "134KB" + }, + "thumbnail": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRIDD6PSH-o5_a4uY4vMZypbGD47mIWLL6VsTXNuADpOw&s" + }, + { + "title": "Introducing ChatGPT and Whisper APIs | OpenAI", + "source": { + "name": "OpenAI", + "link": "https://openai.com/index/introducing-chatgpt-and-whisper-apis/" + }, + "original": { + "link": "https://images.ctfassets.net/kftzwdyauwt9/44fefabe-41f8-4dbf-d80656c1f876/8dec20d14a894ae52ae07449452a89c5/introducing-chatgpt-and-whisper-apis.jpg?w=3840&q=90&fm=webp", + "height": 2048, + "width": 2048, + "size": "93KB" + }, + "thumbnail": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ_IQLO0924Gl1jYnj0yWaeKwSWj8tbTbk0Jc6cAvQv6A&s" + } + ] + }, + "inline_videos": [ + { + "position": 1, + "title": "2 MINUTES AGO: OpenAI Just Released the Most Powerful ...", + "link": "https://www.youtube.com/watch?v=7idowVzHZ9g", + "source": "YouTube", + "channel": "AI Uncovered", + "date": "1 day ago", + "image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIiAdHx8kKDQsJCYxJx8fLT0tMTU3Ojo6Iys/RD84QzQ5OjcBCgoKDQwNGg8PGjclHyU3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N//AABEIAFMAlAMBIgACEQEDEQH/xAAbAAABBQEBAAAAAAAAAAAAAAAAAgMEBQYHAf/EADgQAAEDAgQEAwYFBAIDAAAAAAECAwQAEQUGEiETMUFRFCKRBzJSYXGBFiNCobEVwdHwYsIzcpL/xAAZAQADAQEBAAAAAAAAAAAAAAAAAQIDBAX/xAAkEQACAQMEAgIDAAAAAAAAAAAAAQIDERIEFCExQVEyUhMi8P/aAAwDAQACEQMRAD8A4hatfh0cQsneIt+ZNdUb90pNgPpe9ZK1b7GUpZyhggQk6lRUkC3cmrghNmGWfOSefWrPBITsyU220nU4s2HyqHIirYcSh4WVbUodq6N7PILDbfiVutF5z9OoXSKmbxRcVdmsy9lmPEZYWHEeLadQ5ckcwb2+lbDEcNYxXDXI0+CH2lo82koIHzBJqHChx1XdWVKIOrTq2va3L6Vo4jbQQOGAFWt9rk/3NTTlKxFVPLo4U1HeyvjsjBJQUWffjqXbds/4rF5qiJjYw7oACHPMK7D7ZMMUIcHFkjzRpAbWQP0L2/kiuVZqPHahyD7xBSa18CTuZu1FhS7V5ppFCKLUu1FqAG6LVYYThMrFpRYiBsFKdS3HXAhDabgXJPzIHck1rMTyFHjQnHhNfiFh7hKemMlTT3TiflhRaRflr5gg/KkBgrUGpEyK7ClvxJKdD7DimnE3vZSSQR6imKAE2ry1LtXhFIBs0V6edFADlq6zliO3jDGFYVOUlbsRJWCAf/Gi10knqL27WFcq03BHeuhZYmON4iiY2gFC4irrH6VFCwR63/f7VdpoaipQk/KMnmV0KxaQ5YFOvl/anYmHScQjeJRhxaQlN0rYQbk/QG/3t0NTsHjxpmKOonpCm1kix610nL2VsEhJ48dpSjzCFLNqzqSSfJpTi2uCg9mmMTfGjCZTjq9Q1ILir+Ujoe24NXftBxjEIuIogwn5qefkiAhSu+43pgltjPMF5ASHF3SQkAWFdDxHL0PGwhcgLStN9LrStKhfmL9qxU/CNZU18mcMxjHYy8NmwJQnmWWtSHVylqBIIO4OxFUmJL4mFM9w8f4NdazrkXDYOXcZmcZ+TKEVS0LeXq0afNYdq488rVhbXzdP8VvTfZhUK/TRal2pIUg7BSfWtDMTavLU62W9aC4RwyoX35jrWjcdyih2MpuM+pPHCXkOKc0qbsvzAhQOq5QLf8b9TQBGyXLjRcQlNzC1wpEUoSh5zhtqcSpK0BS/07p2PK9r7XrqOJ4uP6a/ILr8JbkNLwmLUXYsY2IToUkFJUoqAIbvq0m4AukczjO5W8JEEqOpuVpWX1qLpa1EHSLBVykG3Ig8ue9lrdym9FRHaW83dbxSTxVcG5c0KUNVjtwvdHRV6kZTZilR5+PYhMhpUGH5C3EahYm5uTbpc3Nul7VWkVqVOZRD7auG4I916kK4vFH5t03OrSRw9tt786J7uTlQJpgNSG5BQkx+KpZso2JSPNbbcEnnsR1FO4jKkV5anPKQSFCw570m6SbBQ9aAGiN6KcKd6KAJATWkyxP4ALDly2pKuXMG19vrVAE1Mgu8BwLtyJP7H/NUibnSM25djYXlmHLw0hxyyH+MOuqx/g03lXGy80QsgHqO1Ly8XsdyO0yErddgFUc6VAHQmykEi4vsSLC/u1jIji4GIutFZa1AlJIvY1nWjdXNKErNlw5GzJiGb+PhkY2bXqQ4SAlKQN73+9dxwkYgzhpVibjKpJUb8H3bdK4DgmISXpC+Ni05p5J8vBZuL9q6rluVOEUlzEVSo36eOyULH71y85HdJJU73K/2sY0uLlp+I0fzpygzf/gd1ftcfeuNSUcOJGa+qj+1bPPuKJxbHUx2llTUYlPy19fSw/esfiBCpFh7qEgCu5RsjznK/JZZAjMSs74IxKbS4yuWnUhYuFWBIB+4FTcbzJmnEyYWMKeELxSboVBS0kEL2GoIB/eq1nCXWsD/AKwh9xp9EkJabQhSV7AHWFdLX5j1vXuIZmx7EovhcQxiZJY1JVw3XCRcG4P2NSuehvjs6NIw/BEe0DNL7GLLdxAwZeuCYJSlv8oXs5qsbbdOtVfs3gt4LlV7Hp0Rh9jEZAiv8ZaUlqELh1xIO58xFwOib1lGW8VfQ7jjeJOmTKSUSFg/mELcDVib7hW//wA2pUjBZk6W1hkueVIgsjhiQkaWWiTewCiAAdI576vlV/ikYvU0le76EJexfImap8GBK4TzTvh1rLSF8RrUFJPmBG40m471Z+1nGsRmZpxLCZD6VQIUq8dkNIToOgdQLn3jzNUU9udMxxqLPmLdl3aYLrhuUGwGn56SbfanJECTPdenYrPUh9xpLzjklPmUpWqyTdQN7I6A/TuKDY3WgvJq8oSX8KyTBk4fIdgmXizzc2XHgJlOBKWroTpIO17epNTnGcUi5rxGevNjjHDwVmW7OXhDalllShZHCuACL3vz6VlMKGOYP4ZvBsYmRfGrQHEtakJBKAvVa5CrJ5nY7WqLMexFx/GXsQxqUuRwgh9RBX4loqSEgkq2SSUm1qX45Aq9N9P+6LjN2bWnxgb+F4orEMXw9x1xeJrw5EfZVtKOHuDbfnUjPWa8bfy5gEdcxJaxTCuJMSGGxxVcQi99O2wHK1Y7FMLXhojFxSjx29YBQBbl2Ub8/lUaRKkSW47ch9x1EZvhspUbhtF76R2FzSxsXGakrogqTvRTpG9FMZKCaUBagKR8Q9a91I+JPrTuSarDJMqB7PsTlwnVtOR57B1oNj5goH/p6VSz8YRinEXKCI8xl0J8SE+RwG+6kjkduY2+laRDkKFk6RgUh1BkyQZMgJWDoUQNCfqEpST8ya5vIbdYWWyUqSuytuRHT6VMmOJ1PJ2cWMJU23MYRu4ga0eZCwVDe49auM953bdirZwIfmqA1vkW0BVx5e58tcnixvCJZlNJIeT5hrKVJ325W++/7VdsxC/lSRjC30lbs9KeFsNKUoI1fcqT6VMUkVKWXJDgWSpxw7htBJJ6kmoqSOMlaxcagVD7082tKYjoCk3UpI59N6Z1J+JPrWhJuH5MdmGHw07LugLLigG0aCCT1KieXasJptT/AImVYNeJVwdGyL7W5elN3T3HrUQp4dl1KmdhPmta5ta1r16tS1qKlrUokWJUq5tXupPcetF09x61pczEWN73N73vQsrWSVrUonmVG96Xt3HrXm3cUXAQVLUEhS1kJFkgqJsOwpNj3PID7U5t3FFh8qLgIVqUEpUtRCRZIJuEj5Ugop2vDSGMFO9FOkb0UcAWwy5F7D0pwZdhgXUAB8xVO5jcsYopDDyXGS6EoSEixF+htepuaMQiKirhNuqL4WNQCdtuhP8AjtXbudPZ2gLF+ycMuQ+iU+lLGXInwj0qtyliDq1Ox5LwLTbadF7DTbaw/wB6VF/EU2JPkFRQ+2XCEpJ8oSD+m3y60bqhZPAMX7L38NxPgHpXv4aiXvoTf6UziuML/orUiEoMyHSk6SQVJTvew/3aoUDNS3ElmWhCDwyEvpvsq3Mj69qb1ND6CxZPcyvFUbpVoPyFM/h0Mq8yEOt9wLEVFy5mFDDLzeJuurULrStR1bW9369qaxnMAnwGjFU5FdS9uhLm5FtjcVD1FFq6hyPFlzDyjPkKKDhz4cYKhJQU2KEb6fryGwvXv4bifCn0qEvM6WUwklhtwuNpU6pKvcv2+fWokrHkRsdXLZU5IZU0EBsnSBy5fz9zRHUU49xuLFlwctRPhT6V5+GYnwj0qnnY6nEXIrjJei8F7zDXcKTtvtbfblTsPNYTPkeMKlRN+EEI8wsdvUd6vc0b/AMWWYyzE+FPpQcsRLck+lZ5OY5cfEX3m3i8yteyHL2032sOhtT+OZi8ayz4B2RH0qPERfSTysbj70t3Qt8B4P2XH4YifCn0rw5Yi/Cn0qtjZpkCE+X20KeSkFoi4B3A3/mohzDNkty9UgMktAtpTtZQKb2PzGo091Q+gsZF0csR+gTSDlljsKoVZhnKVHUXCCz71iQHf/YU/NzC/KhAIcLD6XQfyyRqTY/36UbnTc/oPGXsthldk9vWiq05slAJDbSOQvqvuaKe40n1DGRnlc6DubkkmiivKND0gWrznz3oooAdC1kC61GwsLnpSLUUVohBbY0miipGKVva9JtRRSYhaORpCqKKb6GAFOJSNNeUU4gB3NulJIsdq9opMBFza19qORoopAKtfnRRRQB//9k=" + }, + { + "position": 2, + "title": "OpenAI Secretly Released a NEW ChatGPT Model and It’s ...", + "link": "https://www.youtube.com/watch?v=uh4baKXL6K4", + "source": "YouTube", + "channel": "Unveiling AI News", + "date": "21 hours ago", + "image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIiAdHx8kKDQsJCYxJx8fLT0tMTU3Ojo6Iys/RD84QzQ5OjcBCgoKDQwNGg8PGjclHyU3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N//AABEIAFMAlAMBIgACEQEDEQH/xAAbAAABBQEBAAAAAAAAAAAAAAAGAAIDBAUBB//EADYQAAIBAwMCAwYFAwQDAAAAAAECAwAEEQUSIQYxE0FRFCIyYXGBQpGhscEjYnIHFlLwFTOC/8QAGgEAAgMBAQAAAAAAAAAAAAAAAgQAAQMFBv/EACkRAAICAQQBAQgDAAAAAAAAAAABAhEDBBIhMUFRBRMiMnGh0fAzYZH/2gAMAwEAAhEDEQA/APJJDU0EWxdxHvH9KjRd8yqe2c1eVUz75GcE4IOPvVisSuFmnbbCNi+cjfwPOro0SAKpuJp5GYZBDADFMaW4RhIjbSoGCF5+lQzXFyyLJk7QCBxwKzcmNRh6l9NLsYoxzOAT73vZ2/PtWXr+lXFmFmWRJrRvgdDyP8h5VH7dMMNubag4GfOprXVpFlXeVCtw4PIP1qk5BNR8GDSrYu1t0nYQhRGeVB8h6VWZUP4VrQAogkHI71Nv3L+9OdUHkBUR2jtjmoQaeK7mmkj1pZqFDs13NNzXQahKHU4VHkeop4PlUBZIlWI6rqyjuQPvViLnGKsykWVHFKur2pVZhZy0GZ2YjIRSTWtpVq093vlGQVzgeX3qvp8aRl1fksvPzq1FO8NyFQ4JA3DyrOY1iXJr2FkuqTMkUAeNfiHY961LHpcSPiVO3ljgfLFaX+nyNEJGdC2Rgue3/eaML24trRN8rxx57FiBSk2zpQS9Dzu+6QgEp8BAuTnFYOodMojf+sJx5DvXo8+rWk91EltLFK/Jbw2zxig3U+o7GS9KmRs84jA+GghKW7g1lGFcgBrVsbbw0f4kZ0J+QwR+9ZdG3VEcLWceoRHKOwV/kw4/Yj8qGJdqjLDj6U+ujnNU6NnoLwvatT8bZj2Ndu/Hf2iHt9s0Ua9eWqWfU+t6PLFa3630cMscWAVkWRx4ifJ1OT/cG9RQZ07pdvrV7NbyTeAqW7Sh9owCCAM/LmteDo6EK/tbXIljtopHijMakOzOpGWIGBt9aJRbFsmqxY3UnyEUmuy/7n1prvULgW9rokbwtAVLRuy2xYpnjcTnP3pt/NPd2hu+mZJJNYuLG1ZJiEW7kh3TiUjb+PcIgdvO0D50N2PSdpfoZIbmdI1M0bB9pKyqyhF4453Dt9qoDp6NuoItJFwy7YBJcuVzsOzewA88dqm1lLVYm2r67DZm1Q6fqH/iGH+4Rb2IvjbFQxk3T7s44zs8Pd8+/NZ/UGnNrWnXVlosFvc6hFfwyXcdoUCh2tkWRlxxs8UPkjgH5UO2mhadqonOk3VwxS2aURXCqhVwwABb4cEHPB4q1a9IJJqiWlzcPHELOKWWQbW2ySEAKMcEZPf0FTayS1eKN26oKdRmvbq7E3Rc1v4LancnUZEZPDP9TCGX1h2dvw/F51WsrrTLTQ5XvIra4tJNOWKYQKB7rahcAtGDyCBhl+goXTRNOOmNLLJereJdCyaPamwTEHz77cj61Yl6as/EaO3mvGaC8jtZ5niXw2ZmCttI5GNw796m1lPVY7oLbu0a0jjtun7nxtSigsUkn0/aZntNjbmhyfNthODn4c8ZoU6zt1t+oHxKsjSwxSyMqovvlBuyEJXdnvg4zmp7jpbTba7tYLiW+j9puTbojpHuzkAP/gc/WsS/itbe9lhsjMY42KkygAlgSD28qumgI6iGX5RL2pUl7UqgA57hormEICxLDgdznjFalxYXFjOBcJtZuQQQQR9RxWJLKYNQgm4/pujjPyOf4r2a70uG7hezigVLe3gz4oH485GP1/KsMsttHR0+NStmjpaBNJg8GP4YwQvqaFtZ0bVtWc3F47JHvwkCjOU+fzPpRhp06qiKQFAUDAq9K6CMkNxilV6j9eAV6K6Xj0yaW7kQqWO1UfnA+lDvVfS9rFrk0iRYE+JUOcDPmKM11SUQzzRWs08aDEaQ4y7feg3qXqK4lu7eG5szA4ALLkNtPOeRwfKri3douSXTMq/0uReltTjUDaoWYDPYq2Tj7bqCISJYSjeXH2r0m5vFktJYsZSSJlI9cggivOLeMRQ734JH6U1ibcbEsySlwR2t1PYtcCEgGaFoJMjPunGf2rVHU+pOreP7NcBoo4XE8AcMqElcg+eSeay7lOzj71CnB+tapsWljhLmSNW01nUhIbayESe0XUcqwxRhV8RSNoA7AZAqZoeoINUutXMEi3UFxiZ8AgO/G3b55zjAz3rP02ZLXU7O5lzshnSRsDJwGBOKMZurrIxs1tBK0rSxTOGUAOyuvz/4oPvV8eWL5VOM0sWO77/foZl6nU2Utn0+KFLlGtY4YERUGTuYAA+6xxnn0pA9WIY7CGJ4ZGhTa0JVS0cQ2j3wcYGeefOren6tp2mXLPbNdzpc3guJTJGAYwA3A55OW703TtZ03TYI7FDPJbLHPumkt1b3pNoA2ZwVGOcnmpuj6gSxZ1H+L7P9/Jlay+ueDctqEKxo10k8rJt4lKkL2PmM01+qNSkBz7MHdkeSRYFDSshDKWI7nIq9Lf6JPZXdlLczRpNNHKrwWKoAVUgjYDjzHNDLhRIwjJZATtJGCRUYeOEZqpw6/r6FuK+nTUhqAI9oE3jZxxuznt6ZrrStNK8r/E7Fmx6mqi1OlUaOKRaU8V2o1PFKrMqHX8W9NwHbg/SvQen+v9Pj6e8HU5ZE1CKPw8bCRNgYU5H65oGicSrg43eYqrPZ85i7elBKKkuRnHlcHwe0Wd0Lq0hni4EqBgD5ZqxNI7QHc+0fib0FB/RWpSSaH4UwO6zbw2/w/C37j/5oklmM0e2Mggjn6UjOO10dCGTdGyOPVLmS0C2emS+Go90uVTI9QM80Ma1c3nhqj6csabi3vSKzE/nRncQXE1qqRssYxgMPKgnWNN8CUFr1pWPbK4/mih2G5raZssjHTpnlG0tGw257ZGP5oRun3NsXsO/1rf1y4MVuIA2Xc+XoKH1QscDvTkVSOdOVsfF78RB8uKrlMHB8qvbQiY8hVZhuJNEDuGqM4rd6esDeXCxRwmWZyFjT1JrGjWiroy6Wy1a2uJDiNJV8QkZ9wnDfoTS+p+Q7PsRKWouraTr/AAK4Ok7DwzHMZHm8MHxrYo6FyiuFVce8u10y7Oi5YAH1G+qNBGmxLlTtkTfGzxGJxg4Ksp7EEHzI7EE16GIfBgilC3YggYiMowZZv6aoqnbu8b3UXbhV4zuweSLdfT2/hCyhlWV7Zn4TO2BTt/pD1wwY8cDdgUq4qNNHoIZZ5d0Zcpp3x1S+3g8wlXaxFMAqece+ajxXQj0eJytKbo4KlSmAU5aIyZODxXKaDxXagFFgqVOVODU0MhdirDnHepZI6jhikaZfCRnb0UVZjGYWf6fLMdUulWMtAYMSt5Kcjbn68/rRLc2E0Mu62baP+PlTdBiGn9OaaYlCrPPmdh+JyWXB+mBW867/AC7UrlXxD+GXwgdf6xqULm3KhPQt/FDmpXM+4vPNub5Uaa1bvcTLGF4HnjtWVL02kRFxqHNuoyEB5kPkPp61UOXQU5cGHf29vP0/psdxhblzLIr+ag7cZ+WAPzoc9me3ciZcN5fT5Vt6tO1xfAD4IxgAeX/eKsxFvZ9pOR5ZFN7aQlKdvgF5Pe4HambKJ1MUhxcW0cnzK811tL0+Ye4skJ9VbI/I1e0H3gMhMCrNrO8DZU/WtWTQZMZt545f7T7pP8frWdNZzQNtmiZD/cKCcFJUzbBrJ4JqcHTLttrl5a7/AGWeSHeMP4Tsu4ehweapXN7JLHs4C/Ko/CrnhVktPBeDo5PbuqyRcXLvsqMuTmm7KuGKmGOtqOX7y3ZV20sVYMdMK1CbhlKnYpVC7N5UV5kVhkFgDW94McERSJAi/KlSrSBzW+At6ekaPpU7Dx4xGCM8cetasBy7JxtC9sV2lWWXyO4W6RRviVKbSRuYg4rI1QYgcc/F60qVTH0iZHywQMaeOfdHxH96vxRJ4ZG3ilSrfwJ2yMxpub3R3rqoo8qVKrBseow3HFTFiwKNyvoe1KlVA2Dd2ipcyoi4UOQBUJA9KVKszdDSBTGA9KVKqDRGwFRMBSpVDSJGaVKlUCP/2Q==" + }, + { + "position": 3, + "title": "OpenAI's ChatGPT Does Research… And Breaks Itself!", + "link": "https://www.youtube.com/watch?v=iC-wRBsAhEs", + "source": "YouTube", + "channel": "Two Minute Papers", + "date": "2 days ago", + "image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIiAdHx8kKDQsJCYxJx8fLT0tMTU3Ojo6Iys/RD84QzQ5OjcBCgoKDQwNGg8PGjclHyU3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N//AABEIAFMAlAMBIgACEQEDEQH/xAAcAAACAgMBAQAAAAAAAAAAAAAEBQMGAAECBwj/xAA+EAACAQMCBAMEBwYEBwAAAAABAgMABBEFEgYTITEiQVFhcZGhBxSBkrHB0RUjMjNCYhZScoNzgpOiwuHw/8QAGwEAAgMBAQEAAAAAAAAAAAAAAgQBAwUGAAf/xAAsEQACAgEDAgUDBAMAAAAAAAABAgADEQQhMRJBBRMiUZEUMkJxgcHRFVJh/9oADAMBAAIRAxEAPwCsoGA7CpEVicEYqYReA49KVcLcNrrPCfEGry313HNpUIeKKMjbIdpOGz18vKsjT1+bnBjdjdMtUMMkSqGBHTvW74MY+/lVd/w8q/R6eJWu7s3AvBbC3HVSCwGfXPWrA3AGkW93ZaJrHEl1bcQ3kQaOKGLfDGTnCsfPqCO65x5dK16mIG8TdczvQI2+uwHPXePxr2b93PbmOTBBGCDXz7BwVcpofEk8t3cftfQ7pYjbKw5cqHb4s9+oLEe4e2rZN9HPI4n03Sm1O75F1Zyzyy5GVaMqGA8sZdPnRs+YKpiWXVtOOnz+XKY+FvyoWMBuxBrx+4eKaRooruZld2EZzk7euCfsq2fQ8qniG7gvGaWPl7cFjjIJpivXZPSRFtR4KyoLQdj+/wA+0vG01mKf6lovKHNtQzIe69ytJmjwadSxXGRMW2l6mw0hxmtFaLtrdp5UjUdWOBTC80KWFC8TCVQOuO4+yvNYqnBM8tLupZRsIjK1rFTvESpA6Ejv6VHBbmONUZy5H9R7mizA6RjOd5xtruAYnjPtphbaXc3C7ooyV9ewqW60e4swkj7WXIyVPagNicZlqUWfdjaJp9Eiutd+v3CgrGPAD6+tb4k1KTTNNkngTc4GAPStcR65HprxQIMzSMAB6VDxbHv0RjjJIpGbQE8gvbu6vbl7iZ2Z2PU5rKbQaHcSJvOEyegNZVfmCNDT2EcRqQRG3THTzrX0Za/HofB3FTRahb2uptEjWSSOm+RwrY2q38XXHTBqG6iaUjaxBrjTuFrSTxSRgBcE4JGBkD19SKxabtPRX1AnJ5EZdLXbBG0eanxZLq/0Y51TUrafWY9RR1gYokhVXBB2Ljp7cUz1OXhzX+KtJ4vbiXT7O2gWKSezuJAs4eMlgoXueuB09Omc1ltwTojACeBnX/iMCPnUXEXAeiW1gs1skmSem5yfzprS6qu84QyqyspzBuCOL9MvvpE4ludQnhtdM1eIFGupBGDyiqoDnzKljin/APjmwuOHOIbuW7t11K2kvINPRpVDyxvgoUHcjO0f8tU3ROEdOur6GKRDtZgDgn9avMf0b8PyZ2hmwcEiRunzp0iUg5nhVpLy4Z9jpGyrtGe/2Vc/onVjq0rLnwqCce+rDrv0aWGm2s10h3JuyAScgfGj/o90e00m4Nyi/wA4FCMk9B1qAMHMse4sip2E9UiYMoHspRqWlGW6VoEAEn8XoDTOGZWAx06VKTt3OWJU+XpRq7IciL2VrauGgtjp0VtGAUVpAc78daG1KeWyYcoArID38jRSXqOTtIwDjrQOsTLM0caEHbknFWVgs+WlVxWuohNojZMmu7eyluZOXCwVz2LdhT2z0pOWHuM5P9PpRjCG1DTOyJEo6ZAG2rrNSMECK0aJshmk6BYkVFwAowBVY4u1vkW4SDDAyBCR65pPf65Nc67Fy5GWFiVCA9MVH9VF1ZOs7MMTluvc9aVTB3mlapT0mL+INJm1HVLV41GEALOewqXiC4/dxwA5XHWmd3eRW9vzLqQQxAeZ6mkvEKtfaUt1YMAAuV6dxUvkjaeoKq4LcRI80aNhnUH0JrKFsNKjltw902+UnqSa3VPkmOHXL7SRIx6L8abaNbm6uWtBtDTxMiEt/WPEv/copWq0fpzPFdRSRna6OGU+hHauRFgVgTxHCpIwJcIbO7EEcvIZkKg7o/EPlQPFFyg0+ODcOZ32nvTqRJGiGqaWziKYlpokPWN/6unpnv8AHsaovFcg1K/2uxklzgsp6gYrV0CJTewGcYz+o/5FrA1qiTcMZXVLYv0BbOTR1ldS2XE9xcLeMbOSQl0HUA1XdOE1ruVnZtgO0kYrvS5S9vnPdjWh4heaVDVyjTVdZw0vGp6iNZ0e4hiULMpyFJ/iANc8LoW0tjJECgYjcO6mkulIHSUOzYC5yKN0DUZYbF4IlyGDsW9Kq0Ore/qDdpOopFeMRsutpHNyUJO04z61ZBzJbH9ywLMK8hvtTa2k3Rgc1mPX0x7Ptoix4y1G16R3BA8wUU/lW/VpLLUDjAmLdrK6XKEEz0G4tZosGQEZ86YaZZhBzpMEnsPSqHHx9dSJsnWCQepUg01suNkmIh2pGzdh5H7aK3T3hNx8Sqi/TeZnJ/eXOWQL2NI+KZkfTo0H8bSqo9maHXVZZbWWdXQFThapWq8Wm6cxiIPErZVm7k+uB+FZ1hRB6pvaSi3UP6BxC9VENvqtkkJBKthqMvbyU21x9RTmXCHaAe2aqh1pWkDmJd4OQdgPWi4OIShYgJ4jlspjPwxVH1AAOOZoWeE3MROuKbeXUbG3iJHPQeMZ86MtbiKLRYrORxzQgXHqaBF8L7fJtCndggUs1SV0MLIcESDrSFPiFxvFbgRW/RisEdxGdlp11HEw8IBckVlF20VzLCr81uvsrdboVscTJNiA4zFSrR1iv75ffXSabJ5zQfeo20shHKpe5gAz18VfPnbI2nRgiHaNJe6Jqtzewy8+yuSGmsyMMGAxuU9s4H2/CncuicM8Vs95akC5U4kltn2SI396+vvFA7LVQCL2A59DVTvOFbqbWZtR0/VY7WRmyssUpRx7Mg9q1PD9a6Dy7/tHErKKW6kbBno0PDEMdtyJZ+euMBpIxux7SK6veE9Lu4QDFyJtoBmgwhJ9SOxqoWtxxdZLg8T2Nwo7C5twxH2rg06i4zGnaWv7Xlhu9SZ2CR2cbKrjyJznHfv8q2k1Wnt2BidiWA5JkUnD11o9vcyB4bqLYcu3gZR7c9PnVNs9WSxRWMmN2cAdc05u3uOIWMuuXciw947OBSEX0z6n2n5UHJw/ZyQLAMyRjtvGCPdij0aV/UM2DgxfUs/lYU7yr3V8PrsV2kUdyEYOYpBhX8WSp99LxdiSWRnEULu7PyUbpGCchR7BU+pwJZX8tpHnbEdoyaWT2MU8xlfuRg104BrQPWM7cTAHTY5S04yeYwa4WJeY4LIvVgPTzoyO8trnUTLpqTJaGQ8kTEb9oHnjp3zSK1sFguOaHOAMBc9Kc6ZY3uoTtHpygyxpv6vtwMgd/tosu6l29OxGIJWut+hfVvnP8S5QXnL0udGbxLE7Y+w1Q4tReKOa1a0jdZihW5Y9YsZyB78j/wC7WzUdOudL0UXWo9biX9zhD4PFkDr7B1qiXEIubZomOM4wfSuauyjYadr4anmVFlz2Px2jBXBqVtTZbb9nJYRs8kyyC7OdyoO6+mO/x92EA019uFxn/V0pnYQ/VLZYt24jJJ9tL4VN+Zq5t1GFYFcb5jvT54khZGciRjkD5U5fTrazjWfVG3OOqQg/jQGi6aZPq93G8Ql7KJUYqDuPXtim1zw5f3UjSS39m7HvmQ/pTWi0ulqIv1DAE8DM5rxvVW23NVplO3J/qJ7nWbhpTym5aDoFXyrKYHg6+JyLmz/6v/qsrc/yWk7WCcv9Bqf9DIFiizjmZPoJP0FbFuFPikY+zdj8qiAkJG4x7fRutTJjPdcegFYf0tA/AfE6HzXP5TsRR46Nj/cP6VJEFVsB8/7proSZx5++o3uYkYBpcN5KDk/CpXT0k7IPieZ3A5hgAVclEHtbLH8K6DIXGAGI7ELj8qEWbf8A1Ig9XcE/AfrU8EqIMfWQfbuA/CmlRV4EoLE8mGb2QFXTDAdtrAn4ipYH65xj4n8qCNzbxRl5LhFUf31pBcXy5PMt7U+WcSSe/wDyj5+6jGYBlf46hW/vbc2c9tzVBSYlwpU9MFie/u7+yj14e4alRQuozxuAASkwIJ9eqmpLqwh3AJDGqr2AUYFaSIKOiL92rQ7gYBlfQh3Ilf4s0y20W3gmsL+S65jlWVkHhGM56fpV14M02Cx0sTrMr3N3GjSElSE89o6+3vVe1eFXtJGZBlFJU47HFPOHbmOK2RRNGqFQR46lrLCvSTBFVYOQI/nEF5by2tzGGjOC8UoDKfQjyPXzrznjPSorLUrZ7KLlQTsBJyznDZ9M4GR28ulegyXcIjyLqLPoZBVT150uG8XLnRTnYJOo/wBPX5UtZX1jGI7pb2pfqBixdA0/cR+15VHkeUrf+QpNJbNFxCNNacywlwFmRcFlI/iAP2/A07i0+3u4edbeNB0JBOVPoR5VCdHCSiQSOHHY7u1UNQvtNGrxG8cvmW+wuY7GC3t4lVoIlIwRhjn1JyO/WiP2hFv6xgIR1zhjn7CtVmOJSuHLFvNg5U/EVJh0OUmyP8snX5j881eACMETMbqzkneWNmil8aSIinyMTfqfxrKrpuCOhRyf7WUj5kfhWVHlV+0jqb3gcSgkEjJ9tGRAZrVZQjeGdpxIeZLy36p3xUn1eA4zDGfeorVZRn7oH4yaO3gHaGP7ooqG3hJ6wx/dFarKsEAwJYo59YZZY1IhGY8Ljb8KblfD/E/3zWVlT3g9oDcIM92++aFaMerfeNZWUcAxTrWVMIVnAOcgMetXDSUURphQOg8qysqPeePaNJQNnYUjvANx6DvWVlRCEWrBENVs5OWpYzJnIyG6+Y7H7adcb6bZWuqMtvbRxqUDEKOmTWVlVNzGElZjgi3fy0+FF/V4cfyY/uitVleWFZODbw5/kx/dFZWVlFKp/9k=" + } + ], + "inline_videos_more_link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&tbm=vid&q=chatgpt&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ8ccDegQIIhAH", + "related_searches": [ + { + "query": "ChatGPT login", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+login&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhUEAE" + }, + { + "query": "ChatGPT free", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+free&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhXEAE" + }, + { + "query": "ChatGPT 4", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+4&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhREAE" + }, + { + "query": "ChatGPT app", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+app&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhQEAE" + }, + { + "query": "ChatGPT download", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+download&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhPEAE" + }, + { + "query": "ChatGPT OpenAI", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+OpenAI&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhOEAE" + }, + { + "query": "ChatGPT website", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+website&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhVEAE" + }, + { + "query": "ChatGPT free online", + "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+free+online&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhWEAE" + } + ], + "pagination": { + "current": 1, + "next": "https://www.google.com/search?q=chatgpt&oq=chatgpt&gl=us&hl=en&start=10&ie=UTF-8" + } +} diff --git a/backend/open_webui/retrieval/web/testdata/searxng.json b/backend/open_webui/retrieval/web/testdata/searxng.json new file mode 100644 index 0000000000000000000000000000000000000000..0e6952baa807842cf130bd0232eab6fe55f1ffba --- /dev/null +++ b/backend/open_webui/retrieval/web/testdata/searxng.json @@ -0,0 +1,476 @@ +{ + "query": "python", + "number_of_results": 116000000, + "results": [ + { + "url": "https://www.python.org/", + "title": "Welcome to Python.org", + "content": "Python is a versatile and powerful language that lets you work quickly and integrate systems more effectively. Learn how to get started, download the latest version, access documentation, find jobs, and join the Python community.", + "engine": "bing", + "parsed_url": ["https", "www.python.org", "/", "", "", ""], + "template": "default.html", + "engines": ["bing", "qwant", "duckduckgo"], + "positions": [1, 1, 1], + "score": 9.0, + "category": "general" + }, + { + "url": "https://wiki.nerdvpn.de/wiki/Python_(programming_language)", + "title": "Python (programming language) - Wikipedia", + "content": "Python is a high-level, general-purpose programming language. Its design philosophy emphasizes code readability with the use of significant indentation. Python is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), object-oriented and functional programming.", + "engine": "bing", + "parsed_url": ["https", "wiki.nerdvpn.de", "/wiki/Python_(programming_language)", "", "", ""], + "template": "default.html", + "engines": ["bing", "qwant", "duckduckgo"], + "positions": [4, 3, 2], + "score": 3.25, + "category": "general" + }, + { + "url": "https://docs.python.org/3/tutorial/index.html", + "title": "The Python Tutorial \u2014 Python 3.12.3 documentation", + "content": "3 days ago \u00b7 Python is an easy to learn, powerful programming language. It has efficient high-level data structures and a simple but effective approach to object-oriented programming. Python\u2019s elegant syntax and dynamic typing, together with its interpreted nature, make it an ideal language for scripting and rapid application development in many \u2026", + "engine": "bing", + "parsed_url": ["https", "docs.python.org", "/3/tutorial/index.html", "", "", ""], + "template": "default.html", + "engines": ["bing", "qwant", "duckduckgo"], + "positions": [5, 5, 3], + "score": 2.2, + "category": "general" + }, + { + "url": "https://www.python.org/downloads/", + "title": "Download Python | Python.org", + "content": "Python is a popular programming language for various purposes. Find the latest version of Python for different operating systems, download release notes, and learn about the development process.", + "engine": "bing", + "parsed_url": ["https", "www.python.org", "/downloads/", "", "", ""], + "template": "default.html", + "engines": ["bing", "duckduckgo"], + "positions": [2, 2], + "score": 2.0, + "category": "general" + }, + { + "url": "https://www.python.org/about/gettingstarted/", + "title": "Python For Beginners | Python.org", + "content": "Learn the basics of Python, a popular and easy-to-use programming language, from installing it to using it for various purposes. Find out how to access online documentation, tutorials, books, code samples, and more resources to help you get started with Python.", + "engine": "bing", + "parsed_url": ["https", "www.python.org", "/about/gettingstarted/", "", "", ""], + "template": "default.html", + "engines": ["bing", "qwant", "duckduckgo"], + "positions": [9, 4, 4], + "score": 1.8333333333333333, + "category": "general" + }, + { + "url": "https://www.python.org/shell/", + "title": "Welcome to Python.org", + "content": "Python is a versatile and easy-to-use programming language that lets you work quickly. Learn more about Python, download the latest version, access documentation, find jobs, and join the community.", + "engine": "bing", + "parsed_url": ["https", "www.python.org", "/shell/", "", "", ""], + "template": "default.html", + "engines": ["bing", "qwant", "duckduckgo"], + "positions": [3, 10, 8], + "score": 1.675, + "category": "general" + }, + { + "url": "https://realpython.com/", + "title": "Python Tutorials \u2013 Real Python", + "content": "Real Python offers comprehensive and up-to-date tutorials, books, and courses for Python developers of all skill levels. Whether you want to learn Python basics, web development, data science, machine learning, or more, you can find clear and practical guides and code examples here.", + "engine": "bing", + "parsed_url": ["https", "realpython.com", "/", "", "", ""], + "template": "default.html", + "engines": ["bing", "qwant", "duckduckgo"], + "positions": [6, 6, 5], + "score": 1.6, + "category": "general" + }, + { + "url": "https://wiki.nerdvpn.de/wiki/Python", + "title": "Python", + "content": "Topics referred to by the same term", + "engine": "wikipedia", + "parsed_url": ["https", "wiki.nerdvpn.de", "/wiki/Python", "", "", ""], + "template": "default.html", + "engines": ["wikipedia"], + "positions": [1], + "score": 1.0, + "category": "general" + }, + { + "title": "Online Python - IDE, Editor, Compiler, Interpreter", + "content": "Online Python IDE is a free online tool that lets you write, execute, and share Python code in the web browser. Learn about Python, its features, and its popularity as a general-purpose programming language for web development, data science, and more.", + "url": "https://www.online-python.com/", + "engine": "duckduckgo", + "parsed_url": ["https", "www.online-python.com", "/", "", "", ""], + "template": "default.html", + "engines": ["qwant", "duckduckgo"], + "positions": [8, 6], + "score": 0.5833333333333333, + "category": "general" + }, + { + "url": "https://micropython.org/", + "title": "MicroPython - Python for microcontrollers", + "content": "MicroPython is a full Python compiler and runtime that runs on the bare-metal. You get an interactive prompt (the REPL) to execute commands immediately, along ...", + "img_src": null, + "engine": "google", + "parsed_url": ["https", "micropython.org", "/", "", "", ""], + "template": "default.html", + "engines": ["google"], + "positions": [1], + "score": 1.0, + "category": "general" + }, + { + "url": "https://dictionary.cambridge.org/uk/dictionary/english/python", + "title": "PYTHON | \u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0432 \u0430\u043d\u0433\u043b\u0456\u0439\u0441\u044c\u043a\u0456\u0439 \u043c\u043e\u0432\u0456 - Cambridge Dictionary", + "content": "Apr 17, 2024 \u2014 \u0412\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f PYTHON: 1. a very large snake that kills animals for food by wrapping itself around them and crushing them\u2026. \u0414\u0456\u0437\u043d\u0430\u0439\u0442\u0435\u0441\u044f \u0431\u0456\u043b\u044c\u0448\u0435.", + "img_src": null, + "engine": "google", + "parsed_url": [ + "https", + "dictionary.cambridge.org", + "/uk/dictionary/english/python", + "", + "", + "" + ], + "template": "default.html", + "engines": ["google"], + "positions": [2], + "score": 0.5, + "category": "general" + }, + { + "url": "https://www.codetoday.co.uk/code", + "title": "Web-based Python Editor (with Turtle graphics)", + "content": "Quick way of starting to write Python code, including drawing with Turtle, provided by CodeToday using Trinket.io Ideal for young children to start ...", + "img_src": null, + "engine": "google", + "parsed_url": ["https", "www.codetoday.co.uk", "/code", "", "", ""], + "template": "default.html", + "engines": ["google"], + "positions": [3], + "score": 0.3333333333333333, + "category": "general" + }, + { + "url": "https://snapcraft.io/docs/python-plugin", + "title": "The python plugin | Snapcraft documentation", + "content": "The python plugin can be used by either Python 2 or Python 3 based parts using a setup.py script for building the project, or using a package published to ...", + "img_src": null, + "engine": "google", + "parsed_url": ["https", "snapcraft.io", "/docs/python-plugin", "", "", ""], + "template": "default.html", + "engines": ["google"], + "positions": [4], + "score": 0.25, + "category": "general" + }, + { + "url": "https://www.developer-tech.com/categories/developer-languages/developer-languages-python/", + "title": "Latest Python Developer News", + "content": "Python's status as the primary language for AI and machine learning projects, from its extensive data-handling capabilities to its flexibility and ...", + "img_src": null, + "engine": "google", + "parsed_url": [ + "https", + "www.developer-tech.com", + "/categories/developer-languages/developer-languages-python/", + "", + "", + "" + ], + "template": "default.html", + "engines": ["google"], + "positions": [5], + "score": 0.2, + "category": "general" + }, + { + "url": "https://subjectguides.york.ac.uk/coding/python", + "title": "Coding: a Practical Guide - Python - Subject Guides", + "content": "Python is a coding language used for a wide range of things, including working with data, building systems and software, and even creating games.", + "img_src": null, + "engine": "google", + "parsed_url": ["https", "subjectguides.york.ac.uk", "/coding/python", "", "", ""], + "template": "default.html", + "engines": ["google"], + "positions": [6], + "score": 0.16666666666666666, + "category": "general" + }, + { + "url": "https://hub.salford.ac.uk/psytech/python/getting-started-python/", + "title": "Getting Started - Python - Salford PsyTech Home - The Hub", + "content": "Python in itself is a very friendly programming language, when we get to grips with writing code, once you grasp the logic, it will become very intuitive.", + "img_src": null, + "engine": "google", + "parsed_url": [ + "https", + "hub.salford.ac.uk", + "/psytech/python/getting-started-python/", + "", + "", + "" + ], + "template": "default.html", + "engines": ["google"], + "positions": [7], + "score": 0.14285714285714285, + "category": "general" + }, + { + "url": "https://snapcraft.io/docs/python-apps", + "title": "Python apps | Snapcraft documentation", + "content": "Snapcraft can be used to package and distribute Python applications in a way that enables convenient installation by users. The process of creating a snap ...", + "img_src": null, + "engine": "google", + "parsed_url": ["https", "snapcraft.io", "/docs/python-apps", "", "", ""], + "template": "default.html", + "engines": ["google"], + "positions": [8], + "score": 0.125, + "category": "general" + }, + { + "url": "https://anvil.works/", + "title": "Anvil | Build Web Apps with Nothing but Python", + "content": "Anvil is a free Python-based drag-and-drop web app builder.\u200eSign Up \u00b7 \u200eSign in \u00b7 \u200ePricing \u00b7 \u200eForum", + "img_src": null, + "engine": "google", + "parsed_url": ["https", "anvil.works", "/", "", "", ""], + "template": "default.html", + "engines": ["google"], + "positions": [9], + "score": 0.1111111111111111, + "category": "general" + }, + { + "url": "https://docs.python.org/", + "title": "Python 3.12.3 documentation", + "content": "3 days ago \u00b7 This is the official documentation for Python 3.12.3. Documentation sections: What's new in Python 3.12? Or all \"What's new\" documents since Python 2.0. Tutorial. Start here: a tour of Python's syntax and features. Library reference. Standard library and builtins. Language reference.", + "engine": "bing", + "parsed_url": ["https", "docs.python.org", "/", "", "", ""], + "template": "default.html", + "engines": ["bing", "duckduckgo"], + "positions": [7, 13], + "score": 0.43956043956043955, + "category": "general" + }, + { + "title": "How to Use Python: Your First Steps - Real Python", + "content": "Learn the basics of Python syntax, installation, error handling, and more in this tutorial. You'll also code your first Python program and test your knowledge with a quiz.", + "url": "https://realpython.com/python-first-steps/", + "engine": "duckduckgo", + "parsed_url": ["https", "realpython.com", "/python-first-steps/", "", "", ""], + "template": "default.html", + "engines": ["qwant", "duckduckgo"], + "positions": [14, 7], + "score": 0.42857142857142855, + "category": "general" + }, + { + "title": "The Python Tutorial \u2014 Python 3.11.8 documentation", + "content": "This tutorial introduces the reader informally to the basic concepts and features of the Python language and system. It helps to have a Python interpreter handy for hands-on experience, but all examples are self-contained, so the tutorial can be read off-line as well. For a description of standard objects and modules, see The Python Standard ...", + "url": "https://docs.python.org/3.11/tutorial/", + "engine": "duckduckgo", + "parsed_url": ["https", "docs.python.org", "/3.11/tutorial/", "", "", ""], + "template": "default.html", + "engines": ["duckduckgo"], + "positions": [7], + "score": 0.14285714285714285, + "category": "general" + }, + { + "url": "https://realpython.com/python-introduction/", + "title": "Introduction to Python 3 \u2013 Real Python", + "content": "Python programming language, including a brief history of the development of Python and reasons why you might select Python as your language of choice.", + "engine": "bing", + "parsed_url": ["https", "realpython.com", "/python-introduction/", "", "", ""], + "template": "default.html", + "engines": ["bing"], + "positions": [8], + "score": 0.125, + "category": "general" + }, + { + "title": "Our Documentation | Python.org", + "content": "Find online or download Python's documentation, tutorials, and guides for beginners and advanced users. Learn how to port from Python 2 to Python 3, contribute to Python, and access Python videos and books.", + "url": "https://www.python.org/doc/", + "engine": "duckduckgo", + "parsed_url": ["https", "www.python.org", "/doc/", "", "", ""], + "template": "default.html", + "engines": ["duckduckgo"], + "positions": [9], + "score": 0.1111111111111111, + "category": "general" + }, + { + "title": "Welcome to Python.org", + "url": "http://www.get-python.org/shell/", + "content": "The mission of the Python Software Foundation is to promote, protect, and advance the Python programming language, and to support and facilitate the growth of a diverse and international community of Python programmers. Learn more. Become a Member Donate to the PSF.", + "engine": "qwant", + "parsed_url": ["http", "www.get-python.org", "/shell/", "", "", ""], + "template": "default.html", + "engines": ["qwant"], + "positions": [9], + "score": 0.1111111111111111, + "category": "general" + }, + { + "title": "About Python\u2122 | Python.org", + "content": "Python is a powerful, fast, and versatile programming language that runs on various platforms and is easy to learn. Learn how to get started, explore the applications, and join the community of Python programmers and users.", + "url": "https://www.python.org/about/", + "engine": "duckduckgo", + "parsed_url": ["https", "www.python.org", "/about/", "", "", ""], + "template": "default.html", + "engines": ["duckduckgo"], + "positions": [11], + "score": 0.09090909090909091, + "category": "general" + }, + { + "title": "Online Python Compiler (Interpreter) - Programiz", + "content": "Write and run Python code using this online tool. You can use Python Shell like IDLE, and take inputs from the user in our Python compiler.", + "url": "https://www.programiz.com/python-programming/online-compiler/", + "engine": "duckduckgo", + "parsed_url": [ + "https", + "www.programiz.com", + "/python-programming/online-compiler/", + "", + "", + "" + ], + "template": "default.html", + "engines": ["duckduckgo"], + "positions": [12], + "score": 0.08333333333333333, + "category": "general" + }, + { + "title": "Welcome to Python.org", + "content": "Python is a versatile and powerful language that lets you work quickly and integrate systems more effectively. Download the latest version, read the documentation, find jobs, events, success stories, and more on Python.org.", + "url": "https://www.python.org/?downloads", + "engine": "duckduckgo", + "parsed_url": ["https", "www.python.org", "/", "", "downloads", ""], + "template": "default.html", + "engines": ["duckduckgo"], + "positions": [15], + "score": 0.06666666666666667, + "category": "general" + }, + { + "url": "https://www.matillion.com/blog/the-importance-of-python-and-its-growing-influence-on-data-productivty-a-matillion-perspective", + "title": "The Importance of Python and its Growing Influence on ...", + "content": "Jan 30, 2024 \u2014 The synergy of low-code functionality with Python's versatility empowers data professionals to orchestrate complex transformations seamlessly.", + "img_src": null, + "engine": "google", + "parsed_url": [ + "https", + "www.matillion.com", + "/blog/the-importance-of-python-and-its-growing-influence-on-data-productivty-a-matillion-perspective", + "", + "", + "" + ], + "template": "default.html", + "engines": ["google"], + "positions": [10], + "score": 0.1, + "category": "general" + }, + { + "title": "BeginnersGuide - Python Wiki", + "content": "This is the program that reads Python programs and carries out their instructions; you need it before you can do any Python programming. Mac and Linux distributions may include an outdated version of Python (Python 2), but you should install an updated one (Python 3). See BeginnersGuide/Download for instructions to download the correct version ...", + "url": "https://wiki.python.org/moin/BeginnersGuide", + "engine": "duckduckgo", + "parsed_url": ["https", "wiki.python.org", "/moin/BeginnersGuide", "", "", ""], + "template": "default.html", + "engines": ["duckduckgo"], + "positions": [16], + "score": 0.0625, + "category": "general" + }, + { + "title": "Learn Python - Free Interactive Python Tutorial", + "content": "Learn Python from scratch or improve your skills with this website that offers tutorials, exercises, tests and certification. Explore topics such as basics, data science, advanced features and more with DataCamp.", + "url": "https://www.learnpython.org/", + "engine": "duckduckgo", + "parsed_url": ["https", "www.learnpython.org", "/", "", "", ""], + "template": "default.html", + "engines": ["duckduckgo"], + "positions": [17], + "score": 0.058823529411764705, + "category": "general" + } + ], + "answers": [], + "corrections": [], + "infoboxes": [ + { + "infobox": "Python", + "id": "https://en.wikipedia.org/wiki/Python_(programming_language)", + "content": "general-purpose programming language", + "img_src": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6f/.PY_file_recreation.png/500px-.PY_file_recreation.png", + "urls": [ + { + "title": "Official website", + "url": "https://www.python.org/", + "official": true + }, + { + "title": "Wikipedia (en)", + "url": "https://en.wikipedia.org/wiki/Python_(programming_language)" + }, + { + "title": "Wikidata", + "url": "http://www.wikidata.org/entity/Q28865" + } + ], + "attributes": [ + { + "label": "Inception", + "value": "Wednesday, February 20, 1991", + "entity": "P571" + }, + { + "label": "Developer", + "value": "Python Software Foundation, Guido van Rossum", + "entity": "P178" + }, + { + "label": "Copyright license", + "value": "Python Software Foundation License", + "entity": "P275" + }, + { + "label": "Programmed in", + "value": "C, Python", + "entity": "P277" + }, + { + "label": "Software version identifier", + "value": "3.12.3, 3.13.0a6", + "entity": "P348" + } + ], + "engine": "wikidata", + "engines": ["wikidata"] + } + ], + "suggestions": [ + "python turtle", + "micro python tutorial", + "python docs", + "python compiler", + "snapcraft python", + "micropython vs python", + "python online", + "python download" + ], + "unresponsive_engines": [] +} diff --git a/backend/open_webui/retrieval/web/testdata/serper.json b/backend/open_webui/retrieval/web/testdata/serper.json new file mode 100644 index 0000000000000000000000000000000000000000..b269eaf5b34fe64234ba6e7ffb27fd3fbbaa3fe0 --- /dev/null +++ b/backend/open_webui/retrieval/web/testdata/serper.json @@ -0,0 +1,190 @@ +{ + "searchParameters": { + "q": "apple inc", + "gl": "us", + "hl": "en", + "autocorrect": true, + "page": 1, + "type": "search" + }, + "knowledgeGraph": { + "title": "Apple", + "type": "Technology company", + "website": "http://www.apple.com/", + "imageUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQwGQRv5TjjkycpctY66mOg_e2-npacrmjAb6_jAWhzlzkFE3OTjxyzbA&s=0", + "description": "Apple Inc. is an American multinational technology company specializing in consumer electronics, software and online services headquartered in Cupertino, California, United States.", + "descriptionSource": "Wikipedia", + "descriptionLink": "https://en.wikipedia.org/wiki/Apple_Inc.", + "attributes": { + "Headquarters": "Cupertino, CA", + "CEO": "Tim Cook (Aug 24, 2011–)", + "Founded": "April 1, 1976, Los Altos, CA", + "Sales": "1 (800) 692-7753", + "Products": "iPhone, Apple Watch, iPad, and more", + "Founders": "Steve Jobs, Steve Wozniak, and Ronald Wayne", + "Subsidiaries": "Apple Store, Beats Electronics, Beddit, and more" + } + }, + "organic": [ + { + "title": "Apple", + "link": "https://www.apple.com/", + "snippet": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...", + "sitelinks": [ + { + "title": "Support", + "link": "https://support.apple.com/" + }, + { + "title": "iPhone", + "link": "https://www.apple.com/iphone/" + }, + { + "title": "Apple makes business better.", + "link": "https://www.apple.com/business/" + }, + { + "title": "Mac", + "link": "https://www.apple.com/mac/" + } + ], + "position": 1 + }, + { + "title": "Apple Inc. - Wikipedia", + "link": "https://en.wikipedia.org/wiki/Apple_Inc.", + "snippet": "Apple Inc. is an American multinational technology company specializing in consumer electronics, software and online services headquartered in Cupertino, ...", + "attributes": { + "Products": "AirPods; Apple Watch; iPad; iPhone; Mac", + "Founders": "Steve Jobs; Steve Wozniak; Ronald Wayne", + "Founded": "April 1, 1976; 46 years ago in Los Altos, California, U.S", + "Industry": "Consumer electronics; Software services; Online services" + }, + "sitelinks": [ + { + "title": "History", + "link": "https://en.wikipedia.org/wiki/History_of_Apple_Inc." + }, + { + "title": "Timeline of Apple Inc. products", + "link": "https://en.wikipedia.org/wiki/Timeline_of_Apple_Inc._products" + }, + { + "title": "List of software by Apple Inc.", + "link": "https://en.wikipedia.org/wiki/List_of_software_by_Apple_Inc." + }, + { + "title": "Apple Store", + "link": "https://en.wikipedia.org/wiki/Apple_Store" + } + ], + "position": 2 + }, + { + "title": "Apple Inc. | History, Products, Headquarters, & Facts | Britannica", + "link": "https://www.britannica.com/topic/Apple-Inc", + "snippet": "Apple Inc., formerly Apple Computer, Inc., American manufacturer of personal computers, smartphones, tablet computers, computer peripherals, ...", + "date": "Aug 31, 2022", + "attributes": { + "Related People": "Steve Jobs Steve Wozniak Jony Ive Tim Cook Angela Ahrendts", + "Date": "1976 - present", + "Areas Of Involvement": "peripheral device" + }, + "position": 3 + }, + { + "title": "AAPL: Apple Inc Stock Price Quote - NASDAQ GS - Bloomberg.com", + "link": "https://www.bloomberg.com/quote/AAPL:US", + "snippet": "Stock analysis for Apple Inc (AAPL:NASDAQ GS) including stock price, stock chart, company news, key statistics, fundamentals and company profile.", + "position": 4 + }, + { + "title": "Apple Inc. (AAPL) Company Profile & Facts - Yahoo Finance", + "link": "https://finance.yahoo.com/quote/AAPL/profile/", + "snippet": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. It also sells various related ...", + "position": 5 + }, + { + "title": "AAPL | Apple Inc. Stock Price & News - WSJ", + "link": "https://www.wsj.com/market-data/quotes/AAPL", + "snippet": "Apple, Inc. engages in the design, manufacture, and sale of smartphones, personal computers, tablets, wearables and accessories, and other varieties of ...", + "position": 6 + }, + { + "title": "Apple Inc Company Profile - Apple Inc Overview - GlobalData", + "link": "https://www.globaldata.com/company-profile/apple-inc/", + "snippet": "Apple Inc (Apple) designs, manufactures, and markets smartphones, tablets, personal computers (PCs), portable and wearable devices. The company also offers ...", + "position": 7 + }, + { + "title": "Apple Inc (AAPL) Stock Price & News - Google Finance", + "link": "https://www.google.com/finance/quote/AAPL:NASDAQ?hl=en", + "snippet": "Get the latest Apple Inc (AAPL) real-time quote, historical performance, charts, and other financial information to help you make more informed trading and ...", + "position": 8 + } + ], + "peopleAlsoAsk": [ + { + "question": "What does Apple Inc mean?", + "snippet": "Apple Inc., formerly Apple Computer, Inc., American manufacturer of personal\ncomputers, smartphones, tablet computers, computer peripherals, and computer\nsoftware. It was the first successful personal computer company and the\npopularizer of the graphical user interface.\nAug 31, 2022", + "title": "Apple Inc. | History, Products, Headquarters, & Facts | Britannica", + "link": "https://www.britannica.com/topic/Apple-Inc" + }, + { + "question": "Is Apple and Apple Inc same?", + "snippet": "Apple was founded as Apple Computer Company on April 1, 1976, by Steve Jobs,\nSteve Wozniak and Ronald Wayne to develop and sell Wozniak's Apple I personal\ncomputer. It was incorporated by Jobs and Wozniak as Apple Computer, Inc.", + "title": "Apple Inc. - Wikipedia", + "link": "https://en.wikipedia.org/wiki/Apple_Inc." + }, + { + "question": "Who owns Apple Inc?", + "snippet": "Apple Inc. is owned by two main institutional investors (Vanguard Group and\nBlackRock, Inc). While its major individual shareholders comprise people like\nArt Levinson, Tim Cook, Bruce Sewell, Al Gore, Johny Sroujli, and others.", + "title": "Who Owns Apple In 2022? - FourWeekMBA", + "link": "https://fourweekmba.com/who-owns-apple/" + }, + { + "question": "What products does Apple Inc offer?", + "snippet": "APPLE FOOTER\nStore.\nMac.\niPad.\niPhone.\nWatch.\nAirPods.\nTV & Home.\nAirTag.", + "title": "More items...", + "link": "https://www.apple.com/business/" + } + ], + "relatedSearches": [ + { + "query": "Who invented the iPhone" + }, + { + "query": "Apple Inc competitors" + }, + { + "query": "Apple iPad" + }, + { + "query": "iPhones" + }, + { + "query": "Apple Inc us" + }, + { + "query": "Apple company history" + }, + { + "query": "Apple Store" + }, + { + "query": "Apple customer service" + }, + { + "query": "Apple Watch" + }, + { + "query": "Apple Inc Industry" + }, + { + "query": "Apple Inc registered address" + }, + { + "query": "Apple Inc Bloomberg" + } + ] +} diff --git a/backend/open_webui/retrieval/web/testdata/serply.json b/backend/open_webui/retrieval/web/testdata/serply.json new file mode 100644 index 0000000000000000000000000000000000000000..0fc2a31e4d63cefba8aa96cad147208d596060c4 --- /dev/null +++ b/backend/open_webui/retrieval/web/testdata/serply.json @@ -0,0 +1,206 @@ +{ + "ads": [], + "ads_count": 0, + "answers": [], + "results": [ + { + "title": "Apple", + "link": "https://www.apple.com/", + "description": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...", + "additional_links": [ + { + "text": "AppleApplehttps://www.apple.com", + "href": "https://www.apple.com/" + } + ], + "cite": {}, + "subdomains": [ + { + "title": "Support", + "link": "https://support.apple.com/", + "description": "SupportContact - iPhone Support - Billing and Subscriptions - Apple Repair" + }, + { + "title": "Store", + "link": "https://www.apple.com/store", + "description": "StoreShop iPhone - Shop iPad - App Store - Shop Mac - ..." + }, + { + "title": "Mac", + "link": "https://www.apple.com/mac/", + "description": "MacMacBook Air - MacBook Pro - iMac - Compare Mac models - Mac mini" + }, + { + "title": "iPad", + "link": "https://www.apple.com/ipad/", + "description": "iPadShop iPad - iPad Pro - iPad Air - Compare iPad models - ..." + }, + { + "title": "Watch", + "link": "https://www.apple.com/watch/", + "description": "WatchShop Apple Watch - Series 9 - SE - Ultra 2 - Nike - Hermès - ..." + } + ], + "realPosition": 1 + }, + { + "title": "Apple", + "link": "https://www.apple.com/", + "description": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...", + "additional_links": [ + { + "text": "AppleApplehttps://www.apple.com", + "href": "https://www.apple.com/" + } + ], + "cite": {}, + "realPosition": 2 + }, + { + "title": "Apple Inc.", + "link": "https://en.wikipedia.org/wiki/Apple_Inc.", + "description": "Apple Inc. (formerly Apple Computer, Inc.) is an American multinational corporation and technology company headquartered in Cupertino, California, ...", + "additional_links": [ + { + "text": "Apple Inc.Wikipediahttps://en.wikipedia.org › wiki › Apple_Inc", + "href": "https://en.wikipedia.org/wiki/Apple_Inc." + }, + { + "text": "", + "href": "https://en.wikipedia.org/wiki/Apple_Inc." + }, + { + "text": "History", + "href": "https://en.wikipedia.org/wiki/History_of_Apple_Inc." + }, + { + "text": "List of Apple products", + "href": "https://en.wikipedia.org/wiki/List_of_Apple_products" + }, + { + "text": "Litigation involving Apple Inc.", + "href": "https://en.wikipedia.org/wiki/Litigation_involving_Apple_Inc." + }, + { + "text": "Apple Park", + "href": "https://en.wikipedia.org/wiki/Apple_Park" + } + ], + "cite": { + "domain": "https://en.wikipedia.org › wiki › Apple_Inc", + "span": " › wiki › Apple_Inc" + }, + "realPosition": 3 + }, + { + "title": "Apple Inc. (AAPL) Company Profile & Facts", + "link": "https://finance.yahoo.com/quote/AAPL/profile/", + "description": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. The company offers iPhone, a line ...", + "additional_links": [ + { + "text": "Apple Inc. (AAPL) Company Profile & FactsYahoo Financehttps://finance.yahoo.com › quote › AAPL › profile", + "href": "https://finance.yahoo.com/quote/AAPL/profile/" + } + ], + "cite": { + "domain": "https://finance.yahoo.com › quote › AAPL › profile", + "span": " › quote › AAPL › profile" + }, + "realPosition": 4 + }, + { + "title": "Apple Inc - Company Profile and News", + "link": "https://www.bloomberg.com/profile/company/AAPL:US", + "description": "Apple Inc. Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables and accessories, and sells a variety of related ...", + "additional_links": [ + { + "text": "Apple Inc - Company Profile and NewsBloomberghttps://www.bloomberg.com › company › AAPL:US", + "href": "https://www.bloomberg.com/profile/company/AAPL:US" + }, + { + "text": "", + "href": "https://www.bloomberg.com/profile/company/AAPL:US" + } + ], + "cite": { + "domain": "https://www.bloomberg.com › company › AAPL:US", + "span": " › company › AAPL:US" + }, + "realPosition": 5 + }, + { + "title": "Apple Inc. | History, Products, Headquarters, & Facts", + "link": "https://www.britannica.com/money/Apple-Inc", + "description": "May 22, 2024 — Apple Inc. is an American multinational technology company that revolutionized the technology sector through its innovation of computer ...", + "additional_links": [ + { + "text": "Apple Inc. | History, Products, Headquarters, & FactsBritannicahttps://www.britannica.com › money › Apple-Inc", + "href": "https://www.britannica.com/money/Apple-Inc" + }, + { + "text": "", + "href": "https://www.britannica.com/money/Apple-Inc" + } + ], + "cite": { + "domain": "https://www.britannica.com › money › Apple-Inc", + "span": " › money › Apple-Inc" + }, + "realPosition": 6 + } + ], + "shopping_ads": [], + "places": [ + { + "title": "Apple Inc." + }, + { + "title": "Apple Inc" + }, + { + "title": "Apple Inc" + } + ], + "related_searches": { + "images": [], + "text": [ + { + "title": "apple inc full form", + "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Inc+full+form&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhPEAE" + }, + { + "title": "apple company history", + "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+company+history&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhOEAE" + }, + { + "title": "apple store", + "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Store&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhQEAE" + }, + { + "title": "apple id", + "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+id&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhSEAE" + }, + { + "title": "apple inc industry", + "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Inc+industry&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhREAE" + }, + { + "title": "apple login", + "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+login&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhTEAE" + } + ] + }, + "image_results": [], + "carousel": [], + "total": 2450000000, + "knowledge_graph": "", + "related_questions": [ + "What does the Apple Inc do?", + "Why did Apple change to Apple Inc?", + "Who owns Apple Inc.?", + "What is Apple Inc best known for?" + ], + "carousel_count": 0, + "ts": 2.491065263748169, + "device_type": null +} diff --git a/backend/open_webui/retrieval/web/testdata/serpstack.json b/backend/open_webui/retrieval/web/testdata/serpstack.json new file mode 100644 index 0000000000000000000000000000000000000000..a82f689d8b2293586d6b94974e018f74e49d1013 --- /dev/null +++ b/backend/open_webui/retrieval/web/testdata/serpstack.json @@ -0,0 +1,276 @@ +{ + "request": { + "success": true, + "total_time_taken": 3.4, + "processed_timestamp": 1714968442, + "search_url": "http://www.google.com/search?q=mcdonalds\u0026gl=us\u0026hl=en\u0026safe=0\u0026num=10" + }, + "search_parameters": { + "engine": "google", + "type": "web", + "device": "desktop", + "auto_location": "1", + "google_domain": "google.com", + "gl": "us", + "hl": "en", + "safe": "0", + "news_type": "all", + "exclude_autocorrected_results": "0", + "images_color": "any", + "page": "1", + "num": "10", + "output": "json", + "csv_fields": "search_parameters.query,organic_results.position,organic_results.title,organic_results.url,organic_results.domain", + "query": "mcdonalds", + "action": "search", + "access_key": "aac48e007e15c532bb94ffb34532a4b2", + "error": {} + }, + "search_information": { + "total_results": 1170000000, + "time_taken_displayed": 0.49, + "detected_location": {}, + "did_you_mean": {}, + "no_results_for_original_query": false, + "showing_results_for": {} + }, + "organic_results": [ + { + "position": 1, + "title": "Our Full McDonald\u0027s Food Menu", + "snippet": "", + "prerender": false, + "cached_page_url": {}, + "related_pages_url": {}, + "url": "https://www.mcdonalds.com/us/en-us/full-menu.html", + "domain": "www.mcdonalds.com", + "displayed_url": "https://www.mcdonalds.com \u203a en-us \u203a full-menu" + }, + { + "position": 2, + "title": "McDonald\u0027s", + "snippet": "McDonald\u0027s is the world\u0027s largest fast food restaurant chain, serving over 69 million customers daily in over 100 countries in more than 40,000 outlets as of\u00a0...", + "prerender": false, + "cached_page_url": {}, + "related_pages_url": {}, + "url": "https://en.wikipedia.org/wiki/McDonald%27s", + "domain": "en.wikipedia.org", + "displayed_url": "https://en.wikipedia.org \u203a wiki \u203a McDonald\u0027s" + }, + { + "position": 3, + "title": "Restaurants Near Me: Nearby McDonald\u0027s Locations", + "snippet": "", + "prerender": false, + "cached_page_url": {}, + "related_pages_url": {}, + "url": "https://www.mcdonalds.com/us/en-us/restaurant-locator.html", + "domain": "www.mcdonalds.com", + "displayed_url": "https://www.mcdonalds.com \u203a en-us \u203a restaurant-locator" + }, + { + "position": 4, + "title": "Download the McDonald\u0027s App: Deals, Promotions \u0026 ...", + "snippet": "Download the McDonald\u0027s app for Mobile Order \u0026 Pay, exclusive deals and coupons, menu information and special promotions.", + "prerender": false, + "cached_page_url": {}, + "related_pages_url": {}, + "url": "https://www.mcdonalds.com/us/en-us/download-app.html", + "domain": "www.mcdonalds.com", + "displayed_url": "https://www.mcdonalds.com \u203a en-us \u203a download-app" + }, + { + "position": 5, + "title": "McDonald\u0027s Restaurant Careers in the US", + "snippet": "McDonald\u0027s restaurant jobs are one-of-a-kind \u2013 just like you. Restaurants are hiring across all levels, from Crew team to Management. Apply today!", + "prerender": false, + "cached_page_url": {}, + "related_pages_url": {}, + "url": "https://jobs.mchire.com/", + "domain": "jobs.mchire.com", + "displayed_url": "https://jobs.mchire.com" + } + ], + "inline_images": [ + { + "image_url": "https://serpstack-assets.apilayer.net/2418910010831954152.png", + "title": "" + } + ], + "local_results": [ + { + "position": 1, + "title": "McDonald\u0027s", + "coordinates": { + "latitude": 0, + "longitude": 0 + }, + "address": "", + "rating": 0, + "reviews": 0, + "type": "", + "price": {}, + "url": 0 + }, + { + "position": 2, + "title": "McDonald\u0027s", + "coordinates": { + "latitude": 0, + "longitude": 0 + }, + "address": "", + "rating": 0, + "reviews": 0, + "type": "", + "price": {}, + "url": 0 + }, + { + "position": 3, + "title": "McDonald\u0027s", + "coordinates": { + "latitude": 0, + "longitude": 0 + }, + "address": "", + "rating": 0, + "reviews": 0, + "type": "", + "price": {}, + "url": 0 + } + ], + "top_stories": [ + { + "block_position": 1, + "title": "Menu nutrition", + "url": "/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=mcdonald%27s+double+quarter+pounder+with+cheese\u0026stick=H4sIAAAAAAAAAONgFuLUz9U3ME-vLDBX4tVP1zc0TCsuNE0ytjTTUs5OttJPy89P0c9NzSuNLyjKL8tMSS2yAvNS80qKMlOLF7Hq5ian5Ocl5qSoFyuk5Jcm5aQqFJYmFpWkFikU5JfmATUolGeWZCgkZ6SmFqcCAM4ilJtxAAAA\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4Qri56BAh0EAM", + "source": "", + "uploaded": "", + "uploaded_utc": "2024-05-06T04:07:22.082Z" + }, + { + "block_position": 2, + "title": "Profiles", + "url": "https://www.instagram.com/McDonalds", + "source": "", + "uploaded": "", + "uploaded_utc": "2024-05-06T04:07:22.082Z" + }, + { + "block_position": 3, + "title": "People also search for", + "url": "/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026si=ACC90nzx_D3_zUKRnpAjmO0UBLNxnt7EyN4YYdru6U3bxLI-L5Wg8IL2sxPFxxcDEhVbocy-LJPZIvZySijw0ho2hfZ-KtV-sSEEJ9lw7JuEkXHDnRK5y4Dm8aqbiLwugbLbslwjG3hO_gpDTFZK2VoUGZPy2nrmOBCy0G3PoOfoiEtct2GSZlUz0uufG-xP8emtNzQKQpvjkAm5Zmi57iVZueiD62upz7-x2N3dAbwtm6FkInAPRw1yR91zuT7F3lEaPblTW3LaRwCDC0bvaRCh9x4N9zHgY1OOQa_rzts2jf5WpXcuw4Y%3D\u0026q=Burger+King\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4Qs9oBKAB6BAhzEAI", + "source": "", + "uploaded": "", + "uploaded_utc": "2024-05-06T04:07:22.082Z" + } + ], + "related_questions": [ + { + "question": "What\u0027s a number 7 at McDonald\u0027s?What\u0027s a number 7 at McDonald\u0027s?What\u0027s a number 7 at McDonald\u0027s?", + "answer": "", + "title": "", + "displayed_url": "" + }, + { + "question": "Why is McDonald\u0027s changing their name?Why is McDonald\u0027s changing their name?Why is McDonald\u0027s changing their name?", + "answer": "", + "title": "", + "displayed_url": "" + }, + { + "question": "What is the oldest still running Mcdonalds?What is the oldest still running Mcdonalds?What is the oldest still running Mcdonalds?", + "answer": "", + "title": "", + "displayed_url": "" + }, + { + "question": "Why is McDonald\u0027s now WcDonald\u0027s?Why is McDonald\u0027s now WcDonald\u0027s?Why is McDonald\u0027s now WcDonald\u0027s?", + "answer": "", + "title": "", + "displayed_url": "" + } + ], + "knowledge_graph": { + "title": "", + "type": "Fast-food restaurant company", + "image_urls": ["https://serpstack-assets.apilayer.net/2418910010831954152.png"], + "description": "McDonald\u0027s Corporation is an American multinational fast food chain, founded in 1940 as a restaurant operated by Richard and Maurice McDonald, in San Bernardino, California, United States.", + "source": { + "name": "Wikipedia", + "url": "https://en.wikipedia.org/wiki/McDonald\u0027s" + }, + "people_also_search_for": [], + "known_attributes": [ + { + "attribute": "kc:/business/business_operation:founder", + "link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Ray+Kroc\u0026si=ACC90nzx_D3_zUKRnpAjmO0UBLNxnt7EyN4YYdru6U3bxLI-LxARWRdbk5SkoY2sDn5Qq7yOmqYGei6qZ7sfJhsjZXBPgjMlLbS7824rpJOm69GzqVWMdoNIZiFX2T4A2td14sZOn4a1BexZLtZXHU7NZdF6VsWbGMVuiSYtXdev7uaUjEJKumiwlqTAATTebOriYTEBuSzC\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHgQAg", + "name": "Founder: ", + "value": "Ray Kroc" + }, + { + "attribute": "kc:/organization/organization:ceo", + "link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Chris+Kempczinski\u0026si=ACC90nwLLwns5sISZcdzuISy7t-NHozt8Cbt6G3WNQfC9ekAgKFbjdEFCDgxLbt57EDZGosYDGiZuq1AcBhA6IhTOSZxfVSySuGQ3VDwmmTA7Z93n3K3596jAuZH9VVv5h8PyvKJSuGuSsQWviJTl3eKj2UL1ZIWuDgkjyVMnC47rN7j0G9PlHRCCLdQF7VDQ1gubTiC4onXqLRBTbwAj6a--PD6Jv_NoA%3D%3D\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHUQAg", + "name": "CEO: ", + "value": "Chris Kempczinski (Nov 1, 2019\u2013)" + }, + { + "attribute": "kc:/business/employer:revenue", + "link": "", + "name": "Revenue: ", + "value": "25.49\u00a0billion USD (2023)" + }, + { + "attribute": "kc:/organization/organization:founded", + "link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Des+Plaines\u0026si=ACC90nyvvWro6QmnyY1IfSdgk5wwjB1r8BGd_IWRjXqmKPQqm_yqLtI_DBi5PXGOtg_Z3qrzzEP6mcih1nN7h5A7v6OefnEJiC7a8dBR-v9LxlRubfyR6vlMr3fZ3TmVKWwz9FRpvZb1eYNt-RM7KIDKQlwGEIgINvzhxjUrv6uxSmceduzxd8W7Pkz71XGwxF0F8OlSzHlx\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECG4QAg", + "name": "Founded: ", + "value": "April 15, 1955, Des Plaines, IL" + }, + { + "attribute": "kc:/organization/organization:headquarters", + "link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Chicago\u0026si=ACC90nyvvWro6QmnyY1IfSdgk5wwjB1r8BGd_IWRjXqmKPQqm-46AEJ_kJbUIEvsvEEZqteiYJvXVXs2ScRNDvFFpjfeAaW3dxtpTGCgcsf5RMdi6IdzOdtjJMN3ZaFwqZOmdi7tC6r0Mh1O9bnP3HrVDB9hH02m7aA6f70dCAfTdpOFnGxDU6wVMAI5MxWBE3wTugtUDOK-\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHYQAg", + "name": "Headquarters: ", + "value": "Chicago, IL" + }, + { + "attribute": "kc:/organization/organization:president", + "link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Chris+Kempczinski\u0026si=ACC90nwLLwns5sISZcdzuISy7t-NHozt8Cbt6G3WNQfC9ekAgKFbjdEFCDgxLbt57EDZGosYDGiZuq1AcBhA6IhTOSZxfVSySuGQ3VDwmmTA7Z93n3K3596jAuZH9VVv5h8PyvKJSuGuSsQWviJTl3eKj2UL1ZIWuDgkjyVMnC47rN7j0G9PlHRCCLdQF7VDQ1gubTiC4onXqLRBTbwAj6a--PD6Jv_NoA%3D%3D\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHEQAg", + "name": "President: ", + "value": "Chris Kempczinski" + } + ], + "website": "https://www.mcdonalds.com/us/en-us.html", + "profiles": [ + { + "name": "Instagram", + "url": "https://www.instagram.com/McDonalds" + }, + { + "name": "X (Twitter)", + "url": "https://twitter.com/McDonalds" + }, + { + "name": "Facebook", + "url": "https://www.facebook.com/McDonaldsUS" + }, + { + "name": "YouTube", + "url": "https://www.youtube.com/user/McDonaldsUS" + }, + { + "name": "Pinterest", + "url": "https://www.pinterest.com/mcdonalds" + } + ], + "founded": "April 15, 1955, Des Plaines, IL", + "headquarters": "Chicago, IL", + "founders": [ + { + "name": "Ray Kroc", + "link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Ray+Kroc\u0026si=ACC90nzx_D3_zUKRnpAjmO0UBLNxnt7EyN4YYdru6U3bxLI-LxARWRdbk5SkoY2sDn5Qq7yOmqYGei6qZ7sfJhsjZXBPgjMlLbS7824rpJOm69GzqVWMdoNIZiFX2T4A2td14sZOn4a1BexZLtZXHU7NZdF6VsWbGMVuiSYtXdev7uaUjEJKumiwlqTAATTebOriYTEBuSzC\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHgQAg" + } + ] + } +} diff --git a/backend/open_webui/retrieval/web/utils.py b/backend/open_webui/retrieval/web/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..6ee0e3781a20f3cdca790ff5eb4d307ec0105af1 --- /dev/null +++ b/backend/open_webui/retrieval/web/utils.py @@ -0,0 +1,650 @@ +import asyncio +import ipaddress +import logging +import socket +import ssl +import urllib.parse +import urllib.request + +import requests +from datetime import datetime, time, timedelta +from typing import ( + Any, + AsyncIterator, + Dict, + Iterator, + List, + Optional, + Sequence, + Union, + Literal, +) + +from fastapi.concurrency import run_in_threadpool +import aiohttp +import certifi +import validators +from langchain_community.document_loaders import PlaywrightURLLoader, WebBaseLoader +from langchain_community.document_loaders.base import BaseLoader +from langchain_core.documents import Document + +from open_webui.retrieval.loaders.tavily import TavilyLoader +from open_webui.retrieval.loaders.external_web import ExternalWebLoader +from open_webui.retrieval.web.firecrawl import scrape_firecrawl_url +from open_webui.constants import ERROR_MESSAGES +from open_webui.config import ( + ENABLE_RAG_LOCAL_WEB_FETCH, + PLAYWRIGHT_WS_URL, + PLAYWRIGHT_TIMEOUT, + WEB_LOADER_ENGINE, + WEB_LOADER_TIMEOUT, + FIRECRAWL_API_BASE_URL, + FIRECRAWL_API_KEY, + FIRECRAWL_TIMEOUT, + TAVILY_API_KEY, + TAVILY_EXTRACT_DEPTH, + EXTERNAL_WEB_LOADER_URL, + EXTERNAL_WEB_LOADER_API_KEY, + WEB_FETCH_FILTER_LIST, +) +from open_webui.utils.misc import is_string_allowed +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL + +log = logging.getLogger(__name__) + + +def resolve_hostname(hostname): + # Get address information + addr_info = socket.getaddrinfo(hostname, None) + + # Extract IP addresses from address information + ipv4_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET] + ipv6_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET6] + + return ipv4_addresses, ipv6_addresses + + +def validate_url(url: Union[str, Sequence[str]]): + if isinstance(url, str): + if isinstance(validators.url(url), validators.ValidationError): + raise ValueError(ERROR_MESSAGES.INVALID_URL) + + parsed_url = urllib.parse.urlparse(url) + + # Protocol validation - only allow http/https + if parsed_url.scheme not in ['http', 'https']: + log.warning(f'Blocked non-HTTP(S) protocol: {parsed_url.scheme} in URL: {url}') + raise ValueError(ERROR_MESSAGES.INVALID_URL) + + # Blocklist check using unified filtering logic + if WEB_FETCH_FILTER_LIST: + if not is_string_allowed(url, WEB_FETCH_FILTER_LIST): + log.warning(f'URL blocked by filter list: {url}') + raise ValueError(ERROR_MESSAGES.INVALID_URL) + + if not ENABLE_RAG_LOCAL_WEB_FETCH: + # Local web fetch is disabled, filter out any URLs that resolve to private IP addresses + parsed_url = urllib.parse.urlparse(url) + # Get IPv4 and IPv6 addresses + ipv4_addresses, ipv6_addresses = resolve_hostname(parsed_url.hostname) + # Check if any of the resolved addresses are private + # This is technically still vulnerable to DNS rebinding attacks, as we don't control WebBaseLoader + for ip in ipv4_addresses + ipv6_addresses: + addr = ipaddress.ip_address(ip) + if not addr.is_global: + raise ValueError(ERROR_MESSAGES.INVALID_URL) + return True + elif isinstance(url, Sequence): + return all(validate_url(u) for u in url) + else: + return False + + +def safe_validate_urls(url: Sequence[str]) -> Sequence[str]: + valid_urls = [] + for u in url: + try: + if validate_url(u): + valid_urls.append(u) + except Exception as e: + log.debug(f'Invalid URL {u}: {str(e)}') + continue + return valid_urls + + +def extract_metadata(soup, url): + metadata = {'source': url} + if title := soup.find('title'): + metadata['title'] = title.get_text() + if description := soup.find('meta', attrs={'name': 'description'}): + metadata['description'] = description.get('content', 'No description found.') + if html := soup.find('html'): + metadata['language'] = html.get('lang', 'No language found.') + return metadata + + +def verify_ssl_cert(url: str) -> bool: + """Verify SSL certificate for the given URL.""" + if not url.startswith('https://'): + return True + + try: + hostname = url.split('://')[-1].split('/')[0] + context = ssl.create_default_context(cafile=certifi.where()) + with context.wrap_socket(ssl.socket(), server_hostname=hostname) as s: + s.connect((hostname, 443)) + return True + except ssl.SSLError: + return False + except Exception as e: + log.warning(f'SSL verification failed for {url}: {str(e)}') + return False + + +class RateLimitMixin: + async def _wait_for_rate_limit(self): + """Wait to respect the rate limit if specified.""" + if self.requests_per_second and self.last_request_time: + min_interval = timedelta(seconds=1.0 / self.requests_per_second) + time_since_last = datetime.now() - self.last_request_time + if time_since_last < min_interval: + await asyncio.sleep((min_interval - time_since_last).total_seconds()) + self.last_request_time = datetime.now() + + def _sync_wait_for_rate_limit(self): + """Synchronous version of rate limit wait.""" + if self.requests_per_second and self.last_request_time: + min_interval = timedelta(seconds=1.0 / self.requests_per_second) + time_since_last = datetime.now() - self.last_request_time + if time_since_last < min_interval: + time.sleep((min_interval - time_since_last).total_seconds()) + self.last_request_time = datetime.now() + + +class URLProcessingMixin: + async def _verify_ssl_cert(self, url: str) -> bool: + """Verify SSL certificate for a URL.""" + return await run_in_threadpool(verify_ssl_cert, url) + + async def _safe_process_url(self, url: str) -> bool: + """Perform safety checks before processing a URL.""" + if self.verify_ssl and not await self._verify_ssl_cert(url): + raise ValueError(f'SSL certificate verification failed for {url}') + await self._wait_for_rate_limit() + return True + + def _safe_process_url_sync(self, url: str) -> bool: + """Synchronous version of safety checks.""" + if self.verify_ssl and not verify_ssl_cert(url): + raise ValueError(f'SSL certificate verification failed for {url}') + self._sync_wait_for_rate_limit() + return True + + +class SafeFireCrawlLoader(BaseLoader, RateLimitMixin, URLProcessingMixin): + def __init__( + self, + web_paths, + verify_ssl: bool = True, + trust_env: bool = False, + requests_per_second: Optional[float] = None, + continue_on_failure: bool = True, + api_key: Optional[str] = None, + api_url: Optional[str] = None, + timeout: Optional[int] = None, + mode: Literal['crawl', 'scrape', 'map'] = 'scrape', + proxy: Optional[Dict[str, str]] = None, + params: Optional[Dict] = None, + ): + proxy_server = proxy.get('server') if proxy else None + if trust_env and not proxy_server: + env_proxies = urllib.request.getproxies() + env_proxy_server = env_proxies.get('https') or env_proxies.get('http') + if env_proxy_server: + if proxy: + proxy['server'] = env_proxy_server + else: + proxy = {'server': env_proxy_server} + self.web_paths = web_paths + self.verify_ssl = verify_ssl + self.requests_per_second = requests_per_second + self.last_request_time = None + self.trust_env = trust_env + self.continue_on_failure = continue_on_failure + self.api_key = api_key + self.api_url = (api_url or 'https://api.firecrawl.dev').rstrip('/') + self.timeout = timeout + self.mode = mode + self.params = params or {} + + def lazy_load(self) -> Iterator[Document]: + try: + for url in self.web_paths: + doc = scrape_firecrawl_url( + self.api_url, + self.api_key, + url, + verify_ssl=self.verify_ssl, + timeout=self.timeout, + params=self.params, + ) + if doc is not None: + yield doc + except Exception as e: + if self.continue_on_failure: + log.warning(f'Error extracting content from URLs with Firecrawl: {e}') + else: + raise e + + async def alazy_load(self): + try: + docs = await run_in_threadpool(lambda: list(self.lazy_load())) + for doc in docs: + yield doc + except Exception as e: + if self.continue_on_failure: + log.warning(f'Error extracting content from URLs with Firecrawl: {e}') + else: + raise e + + +class SafeTavilyLoader(BaseLoader, RateLimitMixin, URLProcessingMixin): + def __init__( + self, + web_paths: Union[str, List[str]], + api_key: str, + extract_depth: Literal['basic', 'advanced'] = 'basic', + continue_on_failure: bool = True, + requests_per_second: Optional[float] = None, + verify_ssl: bool = True, + trust_env: bool = False, + proxy: Optional[Dict[str, str]] = None, + ): + """Initialize SafeTavilyLoader with rate limiting and SSL verification support. + + Args: + web_paths: List of URLs/paths to process. + api_key: The Tavily API key. + extract_depth: Depth of extraction ("basic" or "advanced"). + continue_on_failure: Whether to continue if extraction of a URL fails. + requests_per_second: Number of requests per second to limit to. + verify_ssl: If True, verify SSL certificates. + trust_env: If True, use proxy settings from environment variables. + proxy: Optional proxy configuration. + """ + # Initialize proxy configuration if using environment variables + proxy_server = proxy.get('server') if proxy else None + if trust_env and not proxy_server: + env_proxies = urllib.request.getproxies() + env_proxy_server = env_proxies.get('https') or env_proxies.get('http') + if env_proxy_server: + if proxy: + proxy['server'] = env_proxy_server + else: + proxy = {'server': env_proxy_server} + + # Store parameters for creating TavilyLoader instances + self.web_paths = web_paths if isinstance(web_paths, list) else [web_paths] + self.api_key = api_key + self.extract_depth = extract_depth + self.continue_on_failure = continue_on_failure + self.verify_ssl = verify_ssl + self.trust_env = trust_env + self.proxy = proxy + + # Add rate limiting + self.requests_per_second = requests_per_second + self.last_request_time = None + + def lazy_load(self) -> Iterator[Document]: + """Load documents with rate limiting support, delegating to TavilyLoader.""" + valid_urls = [] + for url in self.web_paths: + try: + self._safe_process_url_sync(url) + valid_urls.append(url) + except Exception as e: + log.warning(f'SSL verification failed for {url}: {str(e)}') + if not self.continue_on_failure: + raise e + if not valid_urls: + if self.continue_on_failure: + log.warning('No valid URLs to process after SSL verification') + return + raise ValueError('No valid URLs to process after SSL verification') + try: + loader = TavilyLoader( + urls=valid_urls, + api_key=self.api_key, + extract_depth=self.extract_depth, + continue_on_failure=self.continue_on_failure, + ) + yield from loader.lazy_load() + except Exception as e: + if self.continue_on_failure: + log.exception(f'Error extracting content from URLs: {e}') + else: + raise e + + async def alazy_load(self) -> AsyncIterator[Document]: + """Async version with rate limiting and SSL verification.""" + valid_urls = [] + for url in self.web_paths: + try: + await self._safe_process_url(url) + valid_urls.append(url) + except Exception as e: + log.warning(f'SSL verification failed for {url}: {str(e)}') + if not self.continue_on_failure: + raise e + + if not valid_urls: + if self.continue_on_failure: + log.warning('No valid URLs to process after SSL verification') + return + raise ValueError('No valid URLs to process after SSL verification') + + try: + loader = TavilyLoader( + urls=valid_urls, + api_key=self.api_key, + extract_depth=self.extract_depth, + continue_on_failure=self.continue_on_failure, + ) + async for document in loader.alazy_load(): + yield document + except Exception as e: + if self.continue_on_failure: + log.exception(f'Error loading URLs: {e}') + else: + raise e + + +class SafePlaywrightURLLoader(PlaywrightURLLoader, RateLimitMixin, URLProcessingMixin): + """Load HTML pages safely with Playwright, supporting SSL verification, rate limiting, and remote browser connection. + + Attributes: + web_paths (List[str]): List of URLs to load. + verify_ssl (bool): If True, verify SSL certificates. + trust_env (bool): If True, use proxy settings from environment variables. + requests_per_second (Optional[float]): Number of requests per second to limit to. + continue_on_failure (bool): If True, continue loading other URLs on failure. + headless (bool): If True, the browser will run in headless mode. + proxy (dict): Proxy override settings for the Playwright session. + playwright_ws_url (Optional[str]): WebSocket endpoint URI for remote browser connection. + playwright_timeout (Optional[int]): Maximum operation time in milliseconds. + """ + + def __init__( + self, + web_paths: List[str], + verify_ssl: bool = True, + trust_env: bool = False, + requests_per_second: Optional[float] = None, + continue_on_failure: bool = True, + headless: bool = True, + remove_selectors: Optional[List[str]] = None, + proxy: Optional[Dict[str, str]] = None, + playwright_ws_url: Optional[str] = None, + playwright_timeout: Optional[int] = 10000, + ): + """Initialize with additional safety parameters and remote browser support.""" + + proxy_server = proxy.get('server') if proxy else None + if trust_env and not proxy_server: + env_proxies = urllib.request.getproxies() + env_proxy_server = env_proxies.get('https') or env_proxies.get('http') + if env_proxy_server: + if proxy: + proxy['server'] = env_proxy_server + else: + proxy = {'server': env_proxy_server} + + # We'll set headless to False if using playwright_ws_url since it's handled by the remote browser + super().__init__( + urls=web_paths, + continue_on_failure=continue_on_failure, + headless=headless if playwright_ws_url is None else False, + remove_selectors=remove_selectors, + proxy=proxy, + ) + self.verify_ssl = verify_ssl + self.requests_per_second = requests_per_second + self.last_request_time = None + self.playwright_ws_url = playwright_ws_url + self.trust_env = trust_env + self.playwright_timeout = playwright_timeout + + def lazy_load(self) -> Iterator[Document]: + """Safely load URLs synchronously with support for remote browser.""" + from playwright.sync_api import sync_playwright + + with sync_playwright() as p: + # Use remote browser if ws_endpoint is provided, otherwise use local browser + if self.playwright_ws_url: + browser = p.chromium.connect(self.playwright_ws_url) + else: + browser = p.chromium.launch(headless=self.headless, proxy=self.proxy) + + for url in self.urls: + try: + self._safe_process_url_sync(url) + page = browser.new_page() + response = page.goto(url, timeout=self.playwright_timeout) + if response is None: + raise ValueError(f'page.goto() returned None for url {url}') + + text = self.evaluator.evaluate(page, browser, response) + metadata = {'source': url} + yield Document(page_content=text, metadata=metadata) + except Exception as e: + if self.continue_on_failure: + log.exception(f'Error loading {url}: {e}') + continue + raise e + browser.close() + + async def alazy_load(self) -> AsyncIterator[Document]: + """Safely load URLs asynchronously with support for remote browser.""" + from playwright.async_api import async_playwright + + async with async_playwright() as p: + # Use remote browser if ws_endpoint is provided, otherwise use local browser + if self.playwright_ws_url: + browser = await p.chromium.connect(self.playwright_ws_url) + else: + browser = await p.chromium.launch(headless=self.headless, proxy=self.proxy) + + for url in self.urls: + try: + await self._safe_process_url(url) + page = await browser.new_page() + response = await page.goto(url, timeout=self.playwright_timeout) + if response is None: + raise ValueError(f'page.goto() returned None for url {url}') + + text = await self.evaluator.evaluate_async(page, browser, response) + metadata = {'source': url} + yield Document(page_content=text, metadata=metadata) + except Exception as e: + if self.continue_on_failure: + log.exception(f'Error loading {url}: {e}') + continue + raise e + await browser.close() + + +class SafeWebBaseLoader(WebBaseLoader): + """WebBaseLoader with enhanced error handling for URLs.""" + + def __init__(self, trust_env: bool = False, *args, **kwargs): + """Initialize SafeWebBaseLoader + Args: + trust_env (bool, optional): set to True if using proxy to make web requests, for example + using http(s)_proxy environment variables. Defaults to False. + """ + super().__init__(*args, **kwargs) + self.trust_env = trust_env + + async def _fetch(self, url: str, retries: int = 3, cooldown: int = 2, backoff: float = 1.5) -> str: + async with aiohttp.ClientSession(trust_env=self.trust_env) as session: + for i in range(retries): + try: + kwargs: Dict = dict( + headers=self.session.headers, + cookies=self.session.cookies.get_dict(), + ) + if not self.session.verify: + kwargs['ssl'] = False + else: + kwargs['ssl'] = AIOHTTP_CLIENT_SESSION_SSL + + async with session.get( + url, + **(self.requests_kwargs | kwargs), + allow_redirects=False, + ) as response: + if self.raise_for_status: + response.raise_for_status() + return await response.text() + except aiohttp.ClientConnectionError as e: + if i == retries - 1: + raise + else: + log.warning(f'Error fetching {url} with attempt {i + 1}/{retries}: {e}. Retrying...') + await asyncio.sleep(cooldown * backoff**i) + raise ValueError('retry count exceeded') + + def _unpack_fetch_results(self, results: Any, urls: List[str], parser: Union[str, None] = None) -> List[Any]: + """Unpack fetch results into BeautifulSoup objects.""" + from bs4 import BeautifulSoup + + final_results = [] + for i, result in enumerate(results): + url = urls[i] + if parser is None: + if url.endswith('.xml'): + parser = 'xml' + else: + parser = self.default_parser + self._check_parser(parser) + final_results.append(BeautifulSoup(result, parser, **self.bs_kwargs)) + return final_results + + async def ascrape_all(self, urls: List[str], parser: Union[str, None] = None) -> List[Any]: + """Async fetch all urls, then return soups for all results.""" + results = await self.fetch_all(urls) + return self._unpack_fetch_results(results, urls, parser=parser) + + def lazy_load(self) -> Iterator[Document]: + """Lazy load text from the url(s) in web_path with error handling.""" + for path in self.web_paths: + try: + soup = self._scrape(path, bs_kwargs=self.bs_kwargs) + text = soup.get_text(**self.bs_get_text_kwargs) + + # Build metadata + metadata = extract_metadata(soup, path) + + yield Document(page_content=text, metadata=metadata) + except Exception as e: + # Log the error and continue with the next URL + log.exception(f'Error loading {path}: {e}') + + async def alazy_load(self) -> AsyncIterator[Document]: + """Async lazy load text from the url(s) in web_path.""" + results = await self.ascrape_all(self.web_paths) + for path, soup in zip(self.web_paths, results): + text = soup.get_text(**self.bs_get_text_kwargs) + metadata = {'source': path} + if title := soup.find('title'): + metadata['title'] = title.get_text() + if description := soup.find('meta', attrs={'name': 'description'}): + metadata['description'] = description.get('content', 'No description found.') + if html := soup.find('html'): + metadata['language'] = html.get('lang', 'No language found.') + yield Document(page_content=text, metadata=metadata) + + async def aload(self) -> list[Document]: + """Load data into Document objects.""" + return [document async for document in self.alazy_load()] + + +def get_web_loader( + urls: Union[str, Sequence[str]], + verify_ssl: bool = True, + requests_per_second: int = 2, + trust_env: bool = False, +): + # Check if the URLs are valid + safe_urls = safe_validate_urls([urls] if isinstance(urls, str) else urls) + + if not safe_urls: + log.warning(f'All provided URLs were blocked or invalid: {urls}') + raise ValueError(ERROR_MESSAGES.INVALID_URL) + + web_loader_args = { + 'web_paths': safe_urls, + 'verify_ssl': verify_ssl, + 'requests_per_second': requests_per_second, + 'continue_on_failure': True, + 'trust_env': trust_env, + } + + if WEB_LOADER_ENGINE.value == '' or WEB_LOADER_ENGINE.value == 'safe_web': + WebLoaderClass = SafeWebBaseLoader + + request_kwargs = {} + if WEB_LOADER_TIMEOUT.value: + try: + timeout_value = float(WEB_LOADER_TIMEOUT.value) + except ValueError: + timeout_value = None + + if timeout_value: + request_kwargs['timeout'] = timeout_value + + if request_kwargs: + web_loader_args['requests_kwargs'] = request_kwargs + + if WEB_LOADER_ENGINE.value == 'playwright': + WebLoaderClass = SafePlaywrightURLLoader + web_loader_args['playwright_timeout'] = PLAYWRIGHT_TIMEOUT.value + if PLAYWRIGHT_WS_URL.value: + web_loader_args['playwright_ws_url'] = PLAYWRIGHT_WS_URL.value + + if WEB_LOADER_ENGINE.value == 'firecrawl': + WebLoaderClass = SafeFireCrawlLoader + web_loader_args['api_key'] = FIRECRAWL_API_KEY.value + web_loader_args['api_url'] = FIRECRAWL_API_BASE_URL.value + if FIRECRAWL_TIMEOUT.value: + try: + web_loader_args['timeout'] = int(FIRECRAWL_TIMEOUT.value) + except ValueError: + pass + + if WEB_LOADER_ENGINE.value == 'tavily': + WebLoaderClass = SafeTavilyLoader + web_loader_args['api_key'] = TAVILY_API_KEY.value + web_loader_args['extract_depth'] = TAVILY_EXTRACT_DEPTH.value + + if WEB_LOADER_ENGINE.value == 'external': + WebLoaderClass = ExternalWebLoader + web_loader_args['external_url'] = EXTERNAL_WEB_LOADER_URL.value + web_loader_args['external_api_key'] = EXTERNAL_WEB_LOADER_API_KEY.value + + if WebLoaderClass: + web_loader = WebLoaderClass(**web_loader_args) + + log.debug( + 'Using WEB_LOADER_ENGINE %s for %s URLs', + web_loader.__class__.__name__, + len(safe_urls), + ) + + return web_loader + else: + raise ValueError( + f'Invalid WEB_LOADER_ENGINE: {WEB_LOADER_ENGINE.value}. ' + "Please set it to 'safe_web', 'playwright', 'firecrawl', or 'tavily'." + ) diff --git a/backend/open_webui/retrieval/web/yacy.py b/backend/open_webui/retrieval/web/yacy.py new file mode 100644 index 0000000000000000000000000000000000000000..32ca04f5313fab62b45c2e0fe53877f550712d3e --- /dev/null +++ b/backend/open_webui/retrieval/web/yacy.py @@ -0,0 +1,85 @@ +import logging +from typing import Optional + +import requests +from requests.auth import HTTPDigestAuth +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def search_yacy( + query_url: str, + username: Optional[str], + password: Optional[str], + query: str, + count: int, + filter_list: Optional[list[str]] = None, +) -> list[SearchResult]: + """ + Search a Yacy instance for a given query and return the results as a list of SearchResult objects. + + The function accepts username and password for authenticating to Yacy. + + Args: + query_url (str): The base URL of the Yacy server. + username (str): Optional YaCy username. + password (str): Optional YaCy password. + query (str): The search term or question to find in the Yacy database. + count (int): The maximum number of results to retrieve from the search. + + Returns: + list[SearchResult]: A list of SearchResults sorted by relevance score in descending order. + + Raise: + requests.exceptions.RequestException: If a request error occurs during the search process. + """ + + # Use authentication if either username or password is set + yacy_auth = None + if username or password: + yacy_auth = HTTPDigestAuth(username, password) + + params = { + 'query': query, + 'contentdom': 'text', + 'resource': 'global', + 'maximumRecords': count, + 'nav': 'none', + } + + # Check if provided a json API URL + if not query_url.endswith('yacysearch.json'): + # Strip all query parameters from the URL + query_url = query_url.rstrip('/') + '/yacysearch.json' + + log.debug(f'searching {query_url}') + + response = requests.get( + query_url, + auth=yacy_auth, + headers={ + 'User-Agent': 'Open WebUI (https://github.com/open-webui/open-webui) RAG Bot', + 'Accept': 'text/html', + 'Accept-Encoding': 'gzip, deflate', + 'Accept-Language': 'en-US,en;q=0.5', + 'Connection': 'keep-alive', + }, + params=params, + ) + + response.raise_for_status() # Raise an exception for HTTP errors. + + json_response = response.json() + results = json_response.get('channels', [{}])[0].get('items', []) + sorted_results = sorted(results, key=lambda x: x.get('ranking', 0), reverse=True) + if filter_list: + sorted_results = get_filtered_results(sorted_results, filter_list) + return [ + SearchResult( + link=result['link'], + title=result.get('title'), + snippet=result.get('description'), + ) + for result in sorted_results[:count] + ] diff --git a/backend/open_webui/retrieval/web/yandex.py b/backend/open_webui/retrieval/web/yandex.py new file mode 100644 index 0000000000000000000000000000000000000000..352d2a3afbc43d3239d7f94db2bdcd315856c518 --- /dev/null +++ b/backend/open_webui/retrieval/web/yandex.py @@ -0,0 +1,150 @@ +import base64 +import io +import json +import logging +import os +from typing import Optional, List + +import requests + +from fastapi import Request + +from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from open_webui.utils.headers import include_user_info_headers +from open_webui.env import FORWARD_SESSION_INFO_HEADER_CHAT_ID + +from xml.etree import ElementTree as ET +from xml.etree.ElementTree import Element + +log = logging.getLogger(__name__) + + +def xml_element_contents_to_string(element: Element) -> str: + buffer = [element.text if element.text else ''] + + for child in element: + buffer.append(xml_element_contents_to_string(child)) + + buffer.append(element.tail if element.tail else '') + + return ''.join(buffer) + + +def search_yandex( + request: Request, + yandex_search_url: str, + yandex_search_api_key: str, + yandex_search_config: str, + query: str, + count: int, + filter_list: Optional[List[str]] = None, + user=None, +) -> List[SearchResult]: + try: + headers = { + 'User-Agent': 'Open WebUI (https://github.com/open-webui/open-webui) RAG Bot', + 'Authorization': f'Api-Key {yandex_search_api_key}', + } + + if user is not None: + headers = include_user_info_headers(headers, user) + + chat_id = getattr(request.state, 'chat_id', None) + if chat_id: + headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = str(chat_id) + + payload = {} if yandex_search_config == '' else json.loads(yandex_search_config) + + if type(payload.get('query', None)) != dict: + payload['query'] = {} + + if 'searchType' not in payload['query']: + payload['query']['searchType'] = 'SEARCH_TYPE_RU' + + payload['query']['queryText'] = query + + if type(payload.get('groupSpec', None)) != dict: + payload['groupSpec'] = {} + + if 'groupMode' not in payload['groupSpec']: + payload['groupSpec']['groupMode'] = 'GROUP_MODE_DEEP' + + payload['groupSpec']['groupsOnPage'] = count + payload['groupSpec']['docsInGroup'] = 1 + + response = requests.post( + ('https://searchapi.api.cloud.yandex.net/v2/web/search' if yandex_search_url == '' else yandex_search_url), + headers=headers, + json=payload, + ) + + response.raise_for_status() + + response_body = response.json() + if 'rawData' not in response_body: + raise Exception(f'No `rawData` in response body: {response_body}') + + search_result_body_bytes = base64.decodebytes(bytes(response_body['rawData'], 'utf-8')) + + doc_root = ET.parse(io.BytesIO(search_result_body_bytes)) + + results = [] + + for group in doc_root.findall('response/results/grouping/group'): + results.append( + { + 'url': xml_element_contents_to_string(group.find('doc/url')).strip('\n'), + 'title': xml_element_contents_to_string(group.find('doc/title')).strip('\n'), + 'snippet': xml_element_contents_to_string(group.find('doc/passages/passage')), + } + ) + + results = get_filtered_results(results, filter_list) + + results = [ + SearchResult( + link=result.get('url'), + title=result.get('title'), + snippet=result.get('snippet'), + ) + for result in results[:count] + ] + + log.info(f'Yandex search results: {results}') + + return results + except Exception as e: + log.error(f'Error in search: {e}') + + return [] + + +if __name__ == '__main__': + from starlette.datastructures import Headers + from fastapi import FastAPI + + result = search_yandex( + Request( + { + 'type': 'http', + 'asgi.version': '3.0', + 'asgi.spec_version': '2.0', + 'method': 'GET', + 'path': '/internal', + 'query_string': b'', + 'headers': Headers({}).raw, + 'client': ('127.0.0.1', 12345), + 'server': ('127.0.0.1', 80), + 'scheme': 'http', + 'app': FastAPI(), + }, + None, + ), + os.environ.get('YANDEX_WEB_SEARCH_URL', ''), + os.environ.get('YANDEX_WEB_SEARCH_API_KEY', ''), + os.environ.get('YANDEX_WEB_SEARCH_CONFIG', '{"query": {"searchType": "SEARCH_TYPE_COM"}}'), + 'TOP movies of the past year', + 3, + ) + + print(result) diff --git a/backend/open_webui/retrieval/web/ydc.py b/backend/open_webui/retrieval/web/ydc.py new file mode 100644 index 0000000000000000000000000000000000000000..21059d8b039d40f1ec14994f2ca7d8a1fcf7bbb5 --- /dev/null +++ b/backend/open_webui/retrieval/web/ydc.py @@ -0,0 +1,73 @@ +import logging +from typing import Optional, List + +import requests +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def search_youcom( + api_key: str, + query: str, + count: int, + filter_list: Optional[List[str]] = None, + language: str = 'EN', +) -> List[SearchResult]: + """Search using You.com's YDC Index API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A You.com API key + query (str): The query to search for + count (int): Maximum number of results to return + filter_list (list[str], optional): Domain filter list + language (str): Language code for search results (default: "EN") + """ + url = 'https://ydc-index.io/v1/search' + headers = { + 'Accept': 'application/json', + 'X-API-KEY': api_key, + } + params = { + 'query': query, + 'count': count, + 'language': language, + } + + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + + json_response = response.json() + results = json_response.get('results', {}).get('web', []) + + if filter_list: + results = get_filtered_results(results, filter_list) + + return [ + SearchResult( + link=result['url'], + title=result.get('title'), + snippet=_build_snippet(result), + ) + for result in results[:count] + ] + + +def _build_snippet(result: dict) -> str: + """Combine the description and snippets list into a single string. + + The You.com API returns a short ``description`` plus a ``snippets`` + list with richer passages. Merging them gives downstream retrieval + (embedding, BM25, bypass-loader context) the most content to work with. + """ + parts: list[str] = [] + + description = result.get('description') + if description: + parts.append(description) + + snippets = result.get('snippets') + if snippets and isinstance(snippets, list): + parts.extend(snippets) + + return '\n\n'.join(parts) diff --git a/backend/open_webui/routers/analytics.py b/backend/open_webui/routers/analytics.py new file mode 100644 index 0000000000000000000000000000000000000000..fd045f79e72361a0990f26057977655b03c3b8a9 --- /dev/null +++ b/backend/open_webui/routers/analytics.py @@ -0,0 +1,442 @@ +from typing import Optional +from datetime import datetime, timedelta +from collections import defaultdict +import logging +from fastapi import APIRouter, Depends, Query +from pydantic import BaseModel + +from open_webui.models.chat_messages import ChatMessages, ChatMessageModel +from open_webui.models.chats import Chats +from open_webui.models.groups import Groups +from open_webui.models.users import Users +from open_webui.models.feedbacks import Feedbacks +from open_webui.utils.auth import get_admin_user +from open_webui.internal.db import get_async_session +from sqlalchemy.ext.asyncio import AsyncSession + +log = logging.getLogger(__name__) + + +router = APIRouter() + + +#################### +# Response Models +#################### + + +class ModelAnalyticsEntry(BaseModel): + model_id: str + count: int + + +class ModelAnalyticsResponse(BaseModel): + models: list[ModelAnalyticsEntry] + + +class UserAnalyticsEntry(BaseModel): + user_id: str + name: Optional[str] = None + email: Optional[str] = None + count: int + input_tokens: int = 0 + output_tokens: int = 0 + total_tokens: int = 0 + + +class UserAnalyticsResponse(BaseModel): + users: list[UserAnalyticsEntry] + + +#################### +# Endpoints +#################### + + +@router.get('/models', response_model=ModelAnalyticsResponse) +async def get_model_analytics( + start_date: Optional[int] = Query(None, description='Start timestamp (epoch)'), + end_date: Optional[int] = Query(None, description='End timestamp (epoch)'), + group_id: Optional[str] = Query(None, description='Filter by user group ID'), + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Get message counts per model.""" + counts = await ChatMessages.get_message_count_by_model( + start_date=start_date, end_date=end_date, group_id=group_id, db=db + ) + models = [ + ModelAnalyticsEntry(model_id=model_id, count=count) + for model_id, count in sorted(counts.items(), key=lambda x: -x[1]) + ] + return ModelAnalyticsResponse(models=models) + + +@router.get('/users', response_model=UserAnalyticsResponse) +async def get_user_analytics( + start_date: Optional[int] = Query(None, description='Start timestamp (epoch)'), + end_date: Optional[int] = Query(None, description='End timestamp (epoch)'), + group_id: Optional[str] = Query(None, description='Filter by user group ID'), + limit: int = Query(50, description='Max users to return'), + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Get message counts and token usage per user with user info.""" + counts = await ChatMessages.get_message_count_by_user( + start_date=start_date, end_date=end_date, group_id=group_id, db=db + ) + token_usage = await ChatMessages.get_token_usage_by_user( + start_date=start_date, end_date=end_date, group_id=group_id, db=db + ) + + # Get user info for top users + top_user_ids = [uid for uid, _ in sorted(counts.items(), key=lambda x: -x[1])[:limit]] + user_info = {u.id: u for u in await Users.get_users_by_user_ids(top_user_ids, db=db)} + + users = [] + for user_id in top_user_ids: + u = user_info.get(user_id) + tokens = token_usage.get(user_id, {}) + users.append( + UserAnalyticsEntry( + user_id=user_id, + name=u.name if u else None, + email=u.email if u else None, + count=counts[user_id], + input_tokens=tokens.get('input_tokens', 0), + output_tokens=tokens.get('output_tokens', 0), + total_tokens=tokens.get('total_tokens', 0), + ) + ) + + return UserAnalyticsResponse(users=users) + + +@router.get('/messages', response_model=list[ChatMessageModel]) +async def get_messages( + model_id: Optional[str] = Query(None, description='Filter by model ID'), + user_id: Optional[str] = Query(None, description='Filter by user ID'), + chat_id: Optional[str] = Query(None, description='Filter by chat ID'), + start_date: Optional[int] = Query(None, description='Start timestamp (epoch)'), + end_date: Optional[int] = Query(None, description='End timestamp (epoch)'), + skip: int = Query(0), + limit: int = Query(50, le=100), + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Query messages with filters.""" + if chat_id: + return await ChatMessages.get_messages_by_chat_id(chat_id=chat_id, db=db) + elif model_id: + return await ChatMessages.get_messages_by_model_id( + model_id=model_id, + start_date=start_date, + end_date=end_date, + skip=skip, + limit=limit, + db=db, + ) + elif user_id: + return await ChatMessages.get_messages_by_user_id(user_id=user_id, skip=skip, limit=limit, db=db) + else: + # Return empty if no filter specified + return [] + + +class SummaryResponse(BaseModel): + total_messages: int + total_chats: int + total_models: int + total_users: int + + +@router.get('/summary', response_model=SummaryResponse) +async def get_summary( + start_date: Optional[int] = Query(None, description='Start timestamp (epoch)'), + end_date: Optional[int] = Query(None, description='End timestamp (epoch)'), + group_id: Optional[str] = Query(None, description='Filter by user group ID'), + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Get summary statistics for the dashboard.""" + model_counts = await ChatMessages.get_message_count_by_model( + start_date=start_date, end_date=end_date, group_id=group_id, db=db + ) + user_counts = await ChatMessages.get_message_count_by_user( + start_date=start_date, end_date=end_date, group_id=group_id, db=db + ) + chat_counts = await ChatMessages.get_message_count_by_chat( + start_date=start_date, end_date=end_date, group_id=group_id, db=db + ) + + return SummaryResponse( + total_messages=sum(model_counts.values()), + total_chats=len(chat_counts), + total_models=len(model_counts), + total_users=len(user_counts), + ) + + +class DailyStatsEntry(BaseModel): + date: str + models: dict[str, int] + + +class DailyStatsResponse(BaseModel): + data: list[DailyStatsEntry] + + +@router.get('/daily', response_model=DailyStatsResponse) +async def get_daily_stats( + start_date: Optional[int] = Query(None, description='Start timestamp (epoch)'), + end_date: Optional[int] = Query(None, description='End timestamp (epoch)'), + group_id: Optional[str] = Query(None, description='Filter by user group ID'), + granularity: str = Query('daily', description="Granularity: 'hourly' or 'daily'"), + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Get message counts grouped by model for time-series chart.""" + if granularity == 'hourly': + counts = await ChatMessages.get_hourly_message_counts_by_model(start_date=start_date, end_date=end_date, db=db) + else: + counts = await ChatMessages.get_daily_message_counts_by_model( + start_date=start_date, end_date=end_date, group_id=group_id, db=db + ) + return DailyStatsResponse( + data=[DailyStatsEntry(date=date, models=models) for date, models in sorted(counts.items())] + ) + + +class TokenUsageEntry(BaseModel): + model_id: str + input_tokens: int + output_tokens: int + total_tokens: int + message_count: int + + +class TokenUsageResponse(BaseModel): + models: list[TokenUsageEntry] + total_input_tokens: int + total_output_tokens: int + total_tokens: int + + +@router.get('/tokens', response_model=TokenUsageResponse) +async def get_token_usage( + start_date: Optional[int] = Query(None), + end_date: Optional[int] = Query(None), + group_id: Optional[str] = Query(None, description='Filter by user group ID'), + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Get token usage aggregated by model.""" + usage = await ChatMessages.get_token_usage_by_model( + start_date=start_date, end_date=end_date, group_id=group_id, db=db + ) + + models = [ + TokenUsageEntry(model_id=model_id, **data) + for model_id, data in sorted(usage.items(), key=lambda x: -x[1]['total_tokens']) + ] + + total_input = sum(m.input_tokens for m in models) + total_output = sum(m.output_tokens for m in models) + + return TokenUsageResponse( + models=models, + total_input_tokens=total_input, + total_output_tokens=total_output, + total_tokens=total_input + total_output, + ) + + +#################### +# Model Chats Browser +#################### + + +class ModelChatEntry(BaseModel): + chat_id: str + user_id: Optional[str] = None + user_name: Optional[str] = None + first_message: Optional[str] = None + updated_at: int + + +class ModelChatsResponse(BaseModel): + chats: list[ModelChatEntry] + total: int + + +@router.get('/models/{model_id:path}/chats', response_model=ModelChatsResponse) +async def get_model_chats( + model_id: str, + start_date: Optional[int] = Query(None), + end_date: Optional[int] = Query(None), + skip: int = Query(0), + limit: int = Query(50, le=100), + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Get chats that used a specific model, with preview and feedback info.""" + + # Get chat IDs that used this model + chat_ids = await ChatMessages.get_chat_ids_by_model_id( + model_id=model_id, + start_date=start_date, + end_date=end_date, + skip=skip, + limit=limit, + db=db, + ) + + if not chat_ids: + return ModelChatsResponse(chats=[], total=0) + + # Get chat details from messages only + chats_data = [] + for chat_id in chat_ids: + messages = await ChatMessages.get_messages_by_chat_id(chat_id, db=db) + if not messages: + continue + + # Get user_id from first user message + first_user_msg = next((m for m in messages if m.role == 'user'), None) + user_id = first_user_msg.user_id if first_user_msg else None + + # Extract first message content as preview + first_message = None + if first_user_msg and first_user_msg.content: + content = first_user_msg.content + if isinstance(content, str): + first_message = content[:200] + elif isinstance(content, list): + text_parts = [b.get('text', '') for b in content if isinstance(b, dict)] + first_message = ' '.join(text_parts)[:200] + + # Get user info + user_name = None + if user_id: + user_info = await Users.get_user_by_id(user_id, db=db) + user_name = user_info.name if user_info else None + + # Timestamps from messages + updated_at = max(m.created_at for m in messages) if messages else 0 + + chats_data.append( + ModelChatEntry( + chat_id=chat_id, + user_id=user_id, + user_name=user_name, + first_message=first_message, + updated_at=updated_at, + ) + ) + + return ModelChatsResponse(chats=chats_data, total=len(chats_data)) + + +#################### +# Model Overview +#################### + + +class HistoryEntry(BaseModel): + date: str + won: int = 0 + lost: int = 0 + + +class TagEntry(BaseModel): + tag: str + count: int + + +class ModelOverviewResponse(BaseModel): + history: list[HistoryEntry] + tags: list[TagEntry] + + +@router.get('/models/{model_id:path}/overview', response_model=ModelOverviewResponse) +async def get_model_overview( + model_id: str, + days: int = Query(30, description='Number of days of history (0 for all)'), + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Get model overview with feedback history and chat tags.""" + + # Get chat IDs that used this model + chat_ids = await ChatMessages.get_chat_ids_by_model_id( + model_id=model_id, + start_date=None, + end_date=None, + skip=0, + limit=10000, # Get all chats + db=db, + ) + + # Get feedback history per day + history_counts: dict[str, dict] = defaultdict(lambda: {'won': 0, 'lost': 0}) + + # Calculate start date for history + now = datetime.now() + start_dt = None + if days > 0: + start_dt = now - timedelta(days=days) + + for chat_id in chat_ids: + feedbacks = await Feedbacks.get_feedbacks_by_chat_id(chat_id, db=db) + for fb in feedbacks: + if fb.data and 'rating' in fb.data: + rating = fb.data['rating'] + fb_date = datetime.fromtimestamp(fb.created_at) + + # Filter by date range + if start_dt and fb_date < start_dt: + continue + + date_str = fb_date.strftime('%Y-%m-%d') + if rating == 1: + history_counts[date_str]['won'] += 1 + elif rating == -1: + history_counts[date_str]['lost'] += 1 + + # Fill in missing days + history = [] + if history_counts or days > 0: + end_dt = now + if days > 0: + current = start_dt + elif history_counts: + # Find earliest date + min_date = min(history_counts.keys()) + current = datetime.strptime(min_date, '%Y-%m-%d') + else: + current = now + + while current <= end_dt: + date_str = current.strftime('%Y-%m-%d') + counts = history_counts.get(date_str, {'won': 0, 'lost': 0}) + history.append( + HistoryEntry( + date=date_str, + won=counts['won'], + lost=counts['lost'], + ) + ) + current += timedelta(days=1) + + # Get chat tags + tag_counts: dict[str, int] = defaultdict(int) + for chat_id in chat_ids: + chat = await Chats.get_chat_by_id(chat_id, db=db) + if chat and chat.meta: + for tag in chat.meta.get('tags', []): + tag_counts[tag] += 1 + + # Sort by count and take top 10 + tags = [TagEntry(tag=tag, count=count) for tag, count in sorted(tag_counts.items(), key=lambda x: -x[1])[:10]] + + return ModelOverviewResponse(history=history, tags=tags) diff --git a/backend/open_webui/routers/audio.py b/backend/open_webui/routers/audio.py new file mode 100644 index 0000000000000000000000000000000000000000..c69be124e5ee18765bde5245a28a57d8104534d9 --- /dev/null +++ b/backend/open_webui/routers/audio.py @@ -0,0 +1,1484 @@ +import hashlib +import json +import logging +import os +import uuid +import html +import base64 +from pydub import AudioSegment +from pydub.silence import split_on_silence +from concurrent.futures import ThreadPoolExecutor +from typing import Optional + +from fnmatch import fnmatch +import aiohttp +import aiofiles +import requests +import mimetypes + +from fastapi import ( + Depends, + FastAPI, + File, + Form, + HTTPException, + Request, + UploadFile, + status, + APIRouter, +) +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse +from pydantic import BaseModel + + +from open_webui.utils.misc import strict_match_mime_type +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.access_control import has_permission +from open_webui.utils.headers import include_user_info_headers +from open_webui.config import ( + WHISPER_MODEL_AUTO_UPDATE, + WHISPER_COMPUTE_TYPE, + WHISPER_MODEL_DIR, + WHISPER_VAD_FILTER, + CACHE_DIR, + WHISPER_LANGUAGE, + WHISPER_MULTILINGUAL, + ELEVENLABS_API_BASE_URL, +) + +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import ( + ENV, + AIOHTTP_CLIENT_SESSION_SSL, + AIOHTTP_CLIENT_TIMEOUT, + AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST, + BYPASS_PYDUB_PREPROCESSING, + DEVICE_TYPE, + ENABLE_FORWARD_USER_INFO_HEADERS, +) + +router = APIRouter() + +# Constants +MAX_FILE_SIZE_MB = 20 +MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes +AZURE_MAX_FILE_SIZE_MB = 200 +AZURE_MAX_FILE_SIZE = AZURE_MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes + +log = logging.getLogger(__name__) + +SPEECH_CACHE_DIR = CACHE_DIR / 'audio' / 'speech' +SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True) + + +########################################## +# +# Utility functions +# Let what is spoken here be heard clearly, and let +# no voice be reduced to noise along the way. +# +########################################## + +from pydub import AudioSegment +from pydub.utils import mediainfo + + +def is_audio_conversion_required(file_path): + """ + Check if the given audio file needs conversion to mp3. + """ + SUPPORTED_FORMATS = {'flac', 'm4a', 'mp3', 'mp4', 'mpeg', 'wav', 'webm'} + + if not os.path.isfile(file_path): + log.error(f'File not found: {file_path}') + return False + + try: + info = mediainfo(file_path) + codec_name = info.get('codec_name', '').lower() + codec_type = info.get('codec_type', '').lower() + codec_tag_string = info.get('codec_tag_string', '').lower() + + if codec_name == 'aac' and codec_type == 'audio' and codec_tag_string == 'mp4a': + # File is AAC/mp4a audio, recommend mp3 conversion + return True + + # If the codec name is in the supported formats + if codec_name in SUPPORTED_FORMATS: + return False + + return True + except Exception as e: + log.error(f'Error getting audio format: {e}') + return False + + +def convert_audio_to_mp3(file_path): + """Convert audio file to mp3 format.""" + try: + output_path = os.path.splitext(file_path)[0] + '.mp3' + audio = AudioSegment.from_file(file_path) + audio.export(output_path, format='mp3') + log.info(f'Converted {file_path} to {output_path}') + return output_path + except Exception as e: + log.error(f'Error converting audio file: {e}') + return None + + +def set_faster_whisper_model(model: str, auto_update: bool = False): + whisper_model = None + if model: + from faster_whisper import WhisperModel + + faster_whisper_kwargs = { + 'model_size_or_path': model, + 'device': DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == 'cuda' else 'cpu', + 'compute_type': WHISPER_COMPUTE_TYPE, + 'download_root': WHISPER_MODEL_DIR, + 'local_files_only': not auto_update, + } + + try: + whisper_model = WhisperModel(**faster_whisper_kwargs) + except Exception: + log.warning('WhisperModel initialization failed, attempting download with local_files_only=False') + faster_whisper_kwargs['local_files_only'] = False + whisper_model = WhisperModel(**faster_whisper_kwargs) + return whisper_model + + +########################################## +# +# Audio API +# +########################################## + + +class TTSConfigForm(BaseModel): + OPENAI_API_BASE_URL: str + OPENAI_API_KEY: str + OPENAI_PARAMS: Optional[dict] = None + API_KEY: str + ENGINE: str + MODEL: str + VOICE: str + SPLIT_ON: str + AZURE_SPEECH_REGION: str + AZURE_SPEECH_BASE_URL: str + AZURE_SPEECH_OUTPUT_FORMAT: str + MISTRAL_API_KEY: str + MISTRAL_API_BASE_URL: str + + +class STTConfigForm(BaseModel): + OPENAI_API_BASE_URL: str + OPENAI_API_KEY: str + ENGINE: str + MODEL: str + SUPPORTED_CONTENT_TYPES: list[str] = [] + WHISPER_MODEL: str + DEEPGRAM_API_KEY: str + AZURE_API_KEY: str + AZURE_REGION: str + AZURE_LOCALES: str + AZURE_BASE_URL: str + AZURE_MAX_SPEAKERS: str + MISTRAL_API_KEY: str + MISTRAL_API_BASE_URL: str + MISTRAL_USE_CHAT_COMPLETIONS: bool + + +class AudioConfigUpdateForm(BaseModel): + tts: TTSConfigForm + stt: STTConfigForm + + +@router.get('/config') +async def get_audio_config(request: Request, user=Depends(get_admin_user)): + return { + 'tts': { + 'OPENAI_API_BASE_URL': request.app.state.config.TTS_OPENAI_API_BASE_URL, + 'OPENAI_API_KEY': request.app.state.config.TTS_OPENAI_API_KEY, + 'OPENAI_PARAMS': request.app.state.config.TTS_OPENAI_PARAMS, + 'API_KEY': request.app.state.config.TTS_API_KEY, + 'ENGINE': request.app.state.config.TTS_ENGINE, + 'MODEL': request.app.state.config.TTS_MODEL, + 'VOICE': request.app.state.config.TTS_VOICE, + 'SPLIT_ON': request.app.state.config.TTS_SPLIT_ON, + 'AZURE_SPEECH_REGION': request.app.state.config.TTS_AZURE_SPEECH_REGION, + 'AZURE_SPEECH_BASE_URL': request.app.state.config.TTS_AZURE_SPEECH_BASE_URL, + 'AZURE_SPEECH_OUTPUT_FORMAT': request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT, + 'MISTRAL_API_KEY': request.app.state.config.TTS_MISTRAL_API_KEY, + 'MISTRAL_API_BASE_URL': request.app.state.config.TTS_MISTRAL_API_BASE_URL, + }, + 'stt': { + 'OPENAI_API_BASE_URL': request.app.state.config.STT_OPENAI_API_BASE_URL, + 'OPENAI_API_KEY': request.app.state.config.STT_OPENAI_API_KEY, + 'ENGINE': request.app.state.config.STT_ENGINE, + 'MODEL': request.app.state.config.STT_MODEL, + 'SUPPORTED_CONTENT_TYPES': request.app.state.config.STT_SUPPORTED_CONTENT_TYPES, + 'WHISPER_MODEL': request.app.state.config.WHISPER_MODEL, + 'DEEPGRAM_API_KEY': request.app.state.config.DEEPGRAM_API_KEY, + 'AZURE_API_KEY': request.app.state.config.AUDIO_STT_AZURE_API_KEY, + 'AZURE_REGION': request.app.state.config.AUDIO_STT_AZURE_REGION, + 'AZURE_LOCALES': request.app.state.config.AUDIO_STT_AZURE_LOCALES, + 'AZURE_BASE_URL': request.app.state.config.AUDIO_STT_AZURE_BASE_URL, + 'AZURE_MAX_SPEAKERS': request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS, + 'MISTRAL_API_KEY': request.app.state.config.AUDIO_STT_MISTRAL_API_KEY, + 'MISTRAL_API_BASE_URL': request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL, + 'MISTRAL_USE_CHAT_COMPLETIONS': request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS, + }, + } + + +@router.post('/config/update') +async def update_audio_config(request: Request, form_data: AudioConfigUpdateForm, user=Depends(get_admin_user)): + request.app.state.config.TTS_OPENAI_API_BASE_URL = form_data.tts.OPENAI_API_BASE_URL + request.app.state.config.TTS_OPENAI_API_KEY = form_data.tts.OPENAI_API_KEY + request.app.state.config.TTS_OPENAI_PARAMS = form_data.tts.OPENAI_PARAMS + request.app.state.config.TTS_API_KEY = form_data.tts.API_KEY + request.app.state.config.TTS_ENGINE = form_data.tts.ENGINE + request.app.state.config.TTS_MODEL = form_data.tts.MODEL + request.app.state.config.TTS_VOICE = form_data.tts.VOICE + request.app.state.config.TTS_SPLIT_ON = form_data.tts.SPLIT_ON + request.app.state.config.TTS_AZURE_SPEECH_REGION = form_data.tts.AZURE_SPEECH_REGION + request.app.state.config.TTS_AZURE_SPEECH_BASE_URL = form_data.tts.AZURE_SPEECH_BASE_URL + request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = form_data.tts.AZURE_SPEECH_OUTPUT_FORMAT + request.app.state.config.TTS_MISTRAL_API_KEY = form_data.tts.MISTRAL_API_KEY + request.app.state.config.TTS_MISTRAL_API_BASE_URL = form_data.tts.MISTRAL_API_BASE_URL + + request.app.state.config.STT_OPENAI_API_BASE_URL = form_data.stt.OPENAI_API_BASE_URL + request.app.state.config.STT_OPENAI_API_KEY = form_data.stt.OPENAI_API_KEY + request.app.state.config.STT_ENGINE = form_data.stt.ENGINE + request.app.state.config.STT_MODEL = form_data.stt.MODEL + request.app.state.config.STT_SUPPORTED_CONTENT_TYPES = form_data.stt.SUPPORTED_CONTENT_TYPES + + request.app.state.config.WHISPER_MODEL = form_data.stt.WHISPER_MODEL + request.app.state.config.DEEPGRAM_API_KEY = form_data.stt.DEEPGRAM_API_KEY + request.app.state.config.AUDIO_STT_AZURE_API_KEY = form_data.stt.AZURE_API_KEY + request.app.state.config.AUDIO_STT_AZURE_REGION = form_data.stt.AZURE_REGION + request.app.state.config.AUDIO_STT_AZURE_LOCALES = form_data.stt.AZURE_LOCALES + request.app.state.config.AUDIO_STT_AZURE_BASE_URL = form_data.stt.AZURE_BASE_URL + request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = form_data.stt.AZURE_MAX_SPEAKERS + request.app.state.config.AUDIO_STT_MISTRAL_API_KEY = form_data.stt.MISTRAL_API_KEY + request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL = form_data.stt.MISTRAL_API_BASE_URL + request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = form_data.stt.MISTRAL_USE_CHAT_COMPLETIONS + + if request.app.state.config.STT_ENGINE == '': + request.app.state.faster_whisper_model = set_faster_whisper_model( + form_data.stt.WHISPER_MODEL, WHISPER_MODEL_AUTO_UPDATE + ) + else: + request.app.state.faster_whisper_model = None + + return { + 'tts': { + 'ENGINE': request.app.state.config.TTS_ENGINE, + 'MODEL': request.app.state.config.TTS_MODEL, + 'VOICE': request.app.state.config.TTS_VOICE, + 'OPENAI_API_BASE_URL': request.app.state.config.TTS_OPENAI_API_BASE_URL, + 'OPENAI_API_KEY': request.app.state.config.TTS_OPENAI_API_KEY, + 'OPENAI_PARAMS': request.app.state.config.TTS_OPENAI_PARAMS, + 'API_KEY': request.app.state.config.TTS_API_KEY, + 'SPLIT_ON': request.app.state.config.TTS_SPLIT_ON, + 'AZURE_SPEECH_REGION': request.app.state.config.TTS_AZURE_SPEECH_REGION, + 'AZURE_SPEECH_BASE_URL': request.app.state.config.TTS_AZURE_SPEECH_BASE_URL, + 'AZURE_SPEECH_OUTPUT_FORMAT': request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT, + 'MISTRAL_API_KEY': request.app.state.config.TTS_MISTRAL_API_KEY, + 'MISTRAL_API_BASE_URL': request.app.state.config.TTS_MISTRAL_API_BASE_URL, + }, + 'stt': { + 'OPENAI_API_BASE_URL': request.app.state.config.STT_OPENAI_API_BASE_URL, + 'OPENAI_API_KEY': request.app.state.config.STT_OPENAI_API_KEY, + 'ENGINE': request.app.state.config.STT_ENGINE, + 'MODEL': request.app.state.config.STT_MODEL, + 'SUPPORTED_CONTENT_TYPES': request.app.state.config.STT_SUPPORTED_CONTENT_TYPES, + 'WHISPER_MODEL': request.app.state.config.WHISPER_MODEL, + 'DEEPGRAM_API_KEY': request.app.state.config.DEEPGRAM_API_KEY, + 'AZURE_API_KEY': request.app.state.config.AUDIO_STT_AZURE_API_KEY, + 'AZURE_REGION': request.app.state.config.AUDIO_STT_AZURE_REGION, + 'AZURE_LOCALES': request.app.state.config.AUDIO_STT_AZURE_LOCALES, + 'AZURE_BASE_URL': request.app.state.config.AUDIO_STT_AZURE_BASE_URL, + 'AZURE_MAX_SPEAKERS': request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS, + 'MISTRAL_API_KEY': request.app.state.config.AUDIO_STT_MISTRAL_API_KEY, + 'MISTRAL_API_BASE_URL': request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL, + 'MISTRAL_USE_CHAT_COMPLETIONS': request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS, + }, + } + + +def load_speech_pipeline(request): + from transformers import pipeline + from datasets import load_dataset + + if request.app.state.speech_synthesiser is None: + request.app.state.speech_synthesiser = pipeline('text-to-speech', 'microsoft/speecht5_tts') + + if request.app.state.speech_speaker_embeddings_dataset is None: + request.app.state.speech_speaker_embeddings_dataset = load_dataset( + 'Matthijs/cmu-arctic-xvectors', split='validation' + ) + + +@router.post('/speech') +async def speech(request: Request, user=Depends(get_verified_user)): + if request.app.state.config.TTS_ENGINE == '': + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if user.role != 'admin' and not await has_permission( + user.id, 'chat.tts', request.app.state.config.USER_PERMISSIONS + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + body = await request.body() + name = hashlib.sha256( + body + + str(request.app.state.config.TTS_ENGINE).encode('utf-8') + + str(request.app.state.config.TTS_MODEL).encode('utf-8') + ).hexdigest() + + file_path = SPEECH_CACHE_DIR.joinpath(f'{name}.mp3') + file_body_path = SPEECH_CACHE_DIR.joinpath(f'{name}.json') + + # Check if the file already exists in the cache + if file_path.is_file(): + return FileResponse(file_path) + + payload = None + try: + payload = json.loads(body.decode('utf-8')) + except Exception as e: + log.exception(e) + raise HTTPException(status_code=400, detail='Invalid JSON payload') + + r = None + if request.app.state.config.TTS_ENGINE == 'openai': + payload['model'] = request.app.state.config.TTS_MODEL + + try: + timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + payload = { + **payload, + **(request.app.state.config.TTS_OPENAI_PARAMS or {}), + } + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {request.app.state.config.TTS_OPENAI_API_KEY}', + } + if ENABLE_FORWARD_USER_INFO_HEADERS: + headers = include_user_info_headers(headers, user) + + r = await session.post( + url=f'{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech', + json=payload, + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) + + r.raise_for_status() + + async with aiofiles.open(file_path, 'wb') as f: + await f.write(await r.read()) + + async with aiofiles.open(file_body_path, 'w') as f: + await f.write(json.dumps(payload)) + + return FileResponse(file_path) + + except Exception as e: + log.exception(e) + detail = None + + status_code = 500 + detail = f'Open WebUI: Server Connection Error' + + if r is not None: + status_code = r.status + + try: + res = await r.json() + if 'error' in res: + detail = f'External: {res["error"]}' + except Exception: + detail = f'External: {e}' + + raise HTTPException( + status_code=status_code, + detail=detail, + ) + + elif request.app.state.config.TTS_ENGINE == 'elevenlabs': + voice_id = payload.get('voice', '') + + if voice_id not in await get_available_voices(request): + raise HTTPException( + status_code=400, + detail='Invalid voice id', + ) + + try: + timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.post( + f'{ELEVENLABS_API_BASE_URL}/v1/text-to-speech/{voice_id}', + json={ + 'text': payload['input'], + 'model_id': request.app.state.config.TTS_MODEL, + 'voice_settings': {'stability': 0.5, 'similarity_boost': 0.5}, + }, + headers={ + 'Accept': 'audio/mpeg', + 'Content-Type': 'application/json', + 'xi-api-key': request.app.state.config.TTS_API_KEY, + }, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + + async with aiofiles.open(file_path, 'wb') as f: + await f.write(await r.read()) + + async with aiofiles.open(file_body_path, 'w') as f: + await f.write(json.dumps(payload)) + + return FileResponse(file_path) + + except Exception as e: + log.exception(e) + detail = None + + try: + if r.status != 200: + res = await r.json() + if 'error' in res: + detail = f'External: {res["error"].get("message", "")}' + except Exception: + detail = f'External: {e}' + + raise HTTPException( + status_code=getattr(r, 'status', 500) if r else 500, + detail=detail if detail else 'Open WebUI: Server Connection Error', + ) + + elif request.app.state.config.TTS_ENGINE == 'azure': + try: + payload = json.loads(body.decode('utf-8')) + except Exception as e: + log.exception(e) + raise HTTPException(status_code=400, detail='Invalid JSON payload') + + region = request.app.state.config.TTS_AZURE_SPEECH_REGION or 'eastus' + base_url = request.app.state.config.TTS_AZURE_SPEECH_BASE_URL + language = request.app.state.config.TTS_VOICE + locale = '-'.join(request.app.state.config.TTS_VOICE.split('-')[:2]) + output_format = request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT + + try: + data = f""" + {html.escape(payload['input'])} + """ + timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.post( + (base_url or f'https://{region}.tts.speech.microsoft.com') + '/cognitiveservices/v1', + headers={ + 'Ocp-Apim-Subscription-Key': request.app.state.config.TTS_API_KEY, + 'Content-Type': 'application/ssml+xml', + 'X-Microsoft-OutputFormat': output_format, + }, + data=data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + + async with aiofiles.open(file_path, 'wb') as f: + await f.write(await r.read()) + + async with aiofiles.open(file_body_path, 'w') as f: + await f.write(json.dumps(payload)) + + return FileResponse(file_path) + + except Exception as e: + log.exception(e) + detail = None + + try: + if r.status != 200: + res = await r.json() + if 'error' in res: + detail = f'External: {res["error"].get("message", "")}' + except Exception: + detail = f'External: {e}' + + raise HTTPException( + status_code=getattr(r, 'status', 500) if r else 500, + detail=detail if detail else 'Open WebUI: Server Connection Error', + ) + + elif request.app.state.config.TTS_ENGINE == 'transformers': + payload = None + try: + payload = json.loads(body.decode('utf-8')) + except Exception as e: + log.exception(e) + raise HTTPException(status_code=400, detail='Invalid JSON payload') + + import torch + import soundfile as sf + + load_speech_pipeline(request) + + embeddings_dataset = request.app.state.speech_speaker_embeddings_dataset + + speaker_index = 6799 + try: + speaker_index = embeddings_dataset['filename'].index(request.app.state.config.TTS_MODEL) + except Exception: + pass + + speaker_embedding = torch.tensor(embeddings_dataset[speaker_index]['xvector']).unsqueeze(0) + + speech = request.app.state.speech_synthesiser( + payload['input'], + forward_params={'speaker_embeddings': speaker_embedding}, + ) + + sf.write(file_path, speech['audio'], samplerate=speech['sampling_rate']) + + async with aiofiles.open(file_body_path, 'w') as f: + await f.write(json.dumps(payload)) + + return FileResponse(file_path) + + elif request.app.state.config.TTS_ENGINE == 'mistral': + api_key = request.app.state.config.TTS_MISTRAL_API_KEY + api_base_url = request.app.state.config.TTS_MISTRAL_API_BASE_URL or 'https://api.mistral.ai/v1' + + if not api_key: + raise HTTPException( + status_code=400, + detail='Mistral API key is required for Mistral TTS', + ) + + try: + timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + mistral_payload = { + 'input': payload.get('input', ''), + 'model': request.app.state.config.TTS_MODEL or 'voxtral-mini-tts-2603', + 'voice_id': payload.get('voice', ''), + 'response_format': 'mp3', + } + + r = await session.post( + url=f'{api_base_url}/audio/speech', + json=mistral_payload, + headers={ + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {api_key}', + }, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) + + r.raise_for_status() + + res = await r.json() + audio_data = res.get('audio_data', '') + if not audio_data: + raise ValueError('No audio_data in Mistral TTS response') + + audio_bytes = base64.b64decode(audio_data) + + async with aiofiles.open(file_path, 'wb') as f: + await f.write(audio_bytes) + + async with aiofiles.open(file_body_path, 'w') as f: + await f.write(json.dumps(payload)) + + return FileResponse(file_path) + + except Exception as e: + log.exception(e) + detail = None + + status_code = 500 + detail = 'Open WebUI: Server Connection Error' + + if r is not None: + status_code = r.status + + try: + res = await r.json() + if 'error' in res: + detail = f'External: {res["error"]}' + elif 'message' in res: + detail = f'External: {res["message"]}' + except Exception: + detail = f'External: {e}' + + raise HTTPException( + status_code=status_code, + detail=detail, + ) + + +def transcription_handler(request, file_path, metadata, user=None): + filename = os.path.basename(file_path) + file_dir = os.path.dirname(file_path) + id = filename.split('.')[0] + + metadata = metadata or {} + + languages = [ + metadata.get('language', None) if not WHISPER_LANGUAGE else WHISPER_LANGUAGE, + None, # Always fallback to None in case transcription fails + ] + + if request.app.state.config.STT_ENGINE == '': + if request.app.state.faster_whisper_model is None: + request.app.state.faster_whisper_model = set_faster_whisper_model(request.app.state.config.WHISPER_MODEL) + + model = request.app.state.faster_whisper_model + segments, info = model.transcribe( + file_path, + beam_size=5, + vad_filter=WHISPER_VAD_FILTER, + language=languages[0], + multilingual=WHISPER_MULTILINGUAL, + ) + log.info("Detected language '%s' with probability %f" % (info.language, info.language_probability)) + + transcript = ''.join([segment.text for segment in list(segments)]) + data = {'text': transcript.strip()} + + # save the transcript to a json file + transcript_file = os.path.join(file_dir, f'{id}.json') + with open(transcript_file, 'w') as f: + json.dump(data, f) + + log.debug(data) + return data + elif request.app.state.config.STT_ENGINE == 'openai': + r = None + try: + for language in languages: + payload = { + 'model': request.app.state.config.STT_MODEL, + } + + if language: + payload['language'] = language + + headers = {'Authorization': f'Bearer {request.app.state.config.STT_OPENAI_API_KEY}'} + if user and ENABLE_FORWARD_USER_INFO_HEADERS: + headers = include_user_info_headers(headers, user) + + with open(file_path, 'rb') as audio_file: + r = requests.post( + url=f'{request.app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions', + headers=headers, + files={'file': (filename, audio_file)}, + data=payload, + timeout=AIOHTTP_CLIENT_TIMEOUT, + ) + + if r.status_code == 200: + # Successful transcription + break + + r.raise_for_status() + data = r.json() + + # save the transcript to a json file + transcript_file = os.path.join(file_dir, f'{id}.json') + with open(transcript_file, 'w') as f: + json.dump(data, f) + + return data + except Exception as e: + log.exception(e) + + detail = None + if r is not None: + try: + res = r.json() + if 'error' in res: + detail = f'External: {res["error"].get("message", "")}' + except Exception: + detail = f'External: {e}' + + raise Exception(detail if detail else 'Open WebUI: Server Connection Error') + + elif request.app.state.config.STT_ENGINE == 'deepgram': + try: + # Determine the MIME type of the file + mime, _ = mimetypes.guess_type(file_path) + if not mime: + mime = 'audio/wav' # fallback to wav if undetectable + + # Read the audio file + with open(file_path, 'rb') as f: + file_data = f.read() + + # Build headers and parameters + headers = { + 'Authorization': f'Token {request.app.state.config.DEEPGRAM_API_KEY}', + 'Content-Type': mime, + } + + for language in languages: + params = {} + if request.app.state.config.STT_MODEL: + params['model'] = request.app.state.config.STT_MODEL + + if language: + params['language'] = language + + # Make request to Deepgram API + r = requests.post( + 'https://api.deepgram.com/v1/listen?smart_format=true', + headers=headers, + params=params, + data=file_data, + timeout=AIOHTTP_CLIENT_TIMEOUT, + ) + + if r.status_code == 200: + # Successful transcription + break + + r.raise_for_status() + response_data = r.json() + + # Extract transcript from Deepgram response + try: + transcript = response_data['results']['channels'][0]['alternatives'][0].get('transcript', '') + except (KeyError, IndexError) as e: + log.error(f'Malformed response from Deepgram: {str(e)}') + raise Exception('Failed to parse Deepgram response - unexpected response format') + data = {'text': transcript.strip()} + + # Save transcript + transcript_file = os.path.join(file_dir, f'{id}.json') + with open(transcript_file, 'w') as f: + json.dump(data, f) + + return data + + except Exception as e: + log.exception(e) + detail = None + if r is not None: + try: + res = r.json() + if 'error' in res: + detail = f'External: {res["error"].get("message", "")}' + except Exception: + detail = f'External: {e}' + raise Exception(detail if detail else 'Open WebUI: Server Connection Error') + + elif request.app.state.config.STT_ENGINE == 'azure': + # Check file exists and size + if not os.path.exists(file_path): + raise HTTPException(status_code=400, detail='Audio file not found') + + # Check file size (Azure has a larger limit of 200MB) + file_size = os.path.getsize(file_path) + if file_size > AZURE_MAX_FILE_SIZE: + raise HTTPException( + status_code=400, + detail=f"File size exceeds Azure's limit of {AZURE_MAX_FILE_SIZE_MB}MB", + ) + + api_key = request.app.state.config.AUDIO_STT_AZURE_API_KEY + region = request.app.state.config.AUDIO_STT_AZURE_REGION or 'eastus' + locales = request.app.state.config.AUDIO_STT_AZURE_LOCALES + base_url = request.app.state.config.AUDIO_STT_AZURE_BASE_URL + max_speakers = request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS or 3 + + # IF NO LOCALES, USE DEFAULTS + if len(locales) < 2: + locales = [ + 'en-US', + 'es-ES', + 'es-MX', + 'fr-FR', + 'hi-IN', + 'it-IT', + 'de-DE', + 'en-GB', + 'en-IN', + 'ja-JP', + 'ko-KR', + 'pt-BR', + 'zh-CN', + ] + locales = ','.join(locales) + + if not api_key or not region: + raise HTTPException( + status_code=400, + detail='Azure API key is required for Azure STT', + ) + + r = None + try: + # Prepare the request + data = { + 'definition': json.dumps( + { + 'locales': locales.split(','), + 'diarization': {'maxSpeakers': max_speakers, 'enabled': True}, + } + if locales + else {} + ) + } + + url = ( + base_url or f'https://{region}.api.cognitive.microsoft.com' + ) + '/speechtotext/transcriptions:transcribe?api-version=2024-11-15' + + # Use context manager to ensure file is properly closed + with open(file_path, 'rb') as audio_file: + r = requests.post( + url=url, + files={'audio': audio_file}, + data=data, + headers={ + 'Ocp-Apim-Subscription-Key': api_key, + }, + timeout=AIOHTTP_CLIENT_TIMEOUT, + ) + + r.raise_for_status() + response = r.json() + + # Extract transcript from response + if not response.get('combinedPhrases'): + raise ValueError('No transcription found in response') + + # Get the full transcript from combinedPhrases + transcript = response['combinedPhrases'][0].get('text', '').strip() + if not transcript: + raise ValueError('Empty transcript in response') + + data = {'text': transcript} + + # Save transcript to json file (consistent with other providers) + transcript_file = os.path.join(file_dir, f'{id}.json') + with open(transcript_file, 'w') as f: + json.dump(data, f) + + log.debug(data) + return data + + except (KeyError, IndexError, ValueError) as e: + log.exception('Error parsing Azure response') + raise HTTPException( + status_code=500, + detail=f'Failed to parse Azure response: {str(e)}', + ) + except requests.exceptions.RequestException as e: + log.exception(e) + detail = None + status_code = getattr(r, 'status_code', 500) if r else 500 + + try: + if r is not None and r.status_code != 200: + res = r.json() + # Azure returns {"code": "...", "message": "...", "innerError": {...}} + if 'code' in res and 'message' in res: + azure_code = res.get('innerError', {}).get('code', res['code']) + user_facing_codes = { + 'EmptyAudioFile', + 'AudioLengthLimitExceeded', + 'NoLanguageIdentified', + 'MultipleLanguagesIdentified', + } + if azure_code in user_facing_codes: + detail = res['message'] + else: + log.error(f'Azure STT error [{azure_code}]: {res["message"]}') + detail = 'An error occurred during transcription.' + elif 'error' in res: + detail = f'External: {res["error"].get("message", "")}' + except Exception: + detail = f'External: {e}' + + raise HTTPException( + status_code=status_code, + detail=detail if detail else 'Open WebUI: Server Connection Error', + ) + + elif request.app.state.config.STT_ENGINE == 'mistral': + # Check file exists + if not os.path.exists(file_path): + raise HTTPException(status_code=400, detail='Audio file not found') + + # Check file size + file_size = os.path.getsize(file_path) + if file_size > MAX_FILE_SIZE: + raise HTTPException( + status_code=400, + detail=f'File size exceeds limit of {MAX_FILE_SIZE_MB}MB', + ) + + api_key = request.app.state.config.AUDIO_STT_MISTRAL_API_KEY + api_base_url = request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL or 'https://api.mistral.ai/v1' + use_chat_completions = request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS + + if not api_key: + raise HTTPException( + status_code=400, + detail='Mistral API key is required for Mistral STT', + ) + + r = None + try: + # Use voxtral-mini-latest as the default model for transcription + model = request.app.state.config.STT_MODEL or 'voxtral-mini-latest' + + log.info( + f'Mistral STT - model: {model}, ' + f'method: {"chat_completions" if use_chat_completions else "transcriptions"}' + ) + + if use_chat_completions: + # Use chat completions API with audio input + # This method requires mp3 or wav format + audio_file_to_use = file_path + + if is_audio_conversion_required(file_path): + log.debug('Converting audio to mp3 for chat completions API') + converted_path = convert_audio_to_mp3(file_path) + if converted_path: + audio_file_to_use = converted_path + else: + log.error('Audio conversion failed') + raise HTTPException( + status_code=500, + detail='Audio conversion failed. Chat completions API requires mp3 or wav format.', + ) + + # Read and encode audio file as base64 + with open(audio_file_to_use, 'rb') as audio_file: + audio_base64 = { + 'data': base64.b64encode(audio_file.read()).decode('utf-8'), + 'format': mimetypes.guess_extension(mimetypes.guess_type(audio_file_to_use)[0]).lstrip('.'), + } + + # Prepare chat completions request + url = f'{api_base_url}/chat/completions' + + # Add language instruction if specified + language = metadata.get('language', None) if metadata else None + if language: + text_instruction = f'Transcribe this audio exactly as spoken in {language}. Do not translate it.' + else: + text_instruction = 'Transcribe this audio exactly as spoken in its original language. Do not translate it to another language.' + + payload = { + 'model': model, + 'messages': [ + { + 'role': 'user', + 'content': [ + { + 'type': 'input_audio', + 'input_audio': audio_base64, + }, + {'type': 'text', 'text': text_instruction}, + ], + } + ], + } + + r = requests.post( + url=url, + json=payload, + headers={ + 'Authorization': f'Bearer {api_key}', + 'Content-Type': 'application/json', + }, + timeout=AIOHTTP_CLIENT_TIMEOUT, + ) + + r.raise_for_status() + response = r.json() + + # Extract transcript from chat completion response + transcript = response.get('choices', [{}])[0].get('message', {}).get('content', '').strip() + if not transcript: + raise ValueError('Empty transcript in response') + + data = {'text': transcript} + + else: + # Use dedicated transcriptions API + url = f'{api_base_url}/audio/transcriptions' + + # Determine the MIME type + mime_type, _ = mimetypes.guess_type(file_path) + if not mime_type: + mime_type = 'audio/webm' + + # Use context manager to ensure file is properly closed + with open(file_path, 'rb') as audio_file: + files = {'file': (filename, audio_file, mime_type)} + data_form = {'model': model} + + # Add language if specified in metadata + language = metadata.get('language', None) if metadata else None + if language: + data_form['language'] = language + + r = requests.post( + url=url, + files=files, + data=data_form, + headers={ + 'Authorization': f'Bearer {api_key}', + }, + timeout=AIOHTTP_CLIENT_TIMEOUT, + ) + + r.raise_for_status() + response = r.json() + + # Extract transcript from response + transcript = response.get('text', '').strip() + if not transcript: + raise ValueError('Empty transcript in response') + + data = {'text': transcript} + + # Save transcript to json file (consistent with other providers) + transcript_file = os.path.join(file_dir, f'{id}.json') + with open(transcript_file, 'w') as f: + json.dump(data, f) + + log.debug(data) + return data + + except ValueError as e: + log.exception('Error parsing Mistral response') + raise HTTPException( + status_code=500, + detail=f'Failed to parse Mistral response: {str(e)}', + ) + except requests.exceptions.RequestException as e: + log.exception(e) + detail = None + + try: + if r is not None and r.status_code != 200: + res = r.json() + if 'error' in res: + detail = f'External: {res["error"].get("message", "")}' + else: + detail = f'External: {r.text}' + except Exception: + detail = f'External: {e}' + + raise HTTPException( + status_code=getattr(r, 'status_code', 500) if r else 500, + detail=detail if detail else 'Open WebUI: Server Connection Error', + ) + + +def transcribe(request: Request, file_path: str, metadata: Optional[dict] = None, user=None): + log.info(f'transcribe: {file_path} {metadata}') + + if BYPASS_PYDUB_PREPROCESSING: + log.info('Bypassing pydub preprocessing (BYPASS_PYDUB_PREPROCESSING=true)') + chunk_paths = [file_path] + else: + if is_audio_conversion_required(file_path): + file_path = convert_audio_to_mp3(file_path) + + try: + file_path = compress_audio(file_path) + except Exception as e: + log.exception(e) + + # Always produce a list of chunk paths (could be one entry if small) + try: + chunk_paths = split_audio(file_path, MAX_FILE_SIZE) + print(f'Chunk paths: {chunk_paths}') + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + results = [] + try: + with ThreadPoolExecutor() as executor: + # Submit tasks for each chunk_path + futures = [ + executor.submit(transcription_handler, request, chunk_path, metadata, user) + for chunk_path in chunk_paths + ] + # Gather results as they complete + for future in futures: + try: + results.append(future.result()) + except HTTPException: + raise + except Exception as transcribe_exc: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f'Error transcribing chunk: {transcribe_exc}', + ) + finally: + # Clean up only the temporary chunks, never the original file + for chunk_path in chunk_paths: + if chunk_path != file_path and os.path.isfile(chunk_path): + try: + os.remove(chunk_path) + except Exception: + pass + + return { + 'text': ' '.join([result['text'] for result in results]), + } + + +def compress_audio(file_path): + if os.path.getsize(file_path) > MAX_FILE_SIZE: + id = os.path.splitext(os.path.basename(file_path))[0] # Handles names with multiple dots + file_dir = os.path.dirname(file_path) + + audio = AudioSegment.from_file(file_path) + audio = audio.set_frame_rate(16000).set_channels(1) # Compress audio + + compressed_path = os.path.join(file_dir, f'{id}_compressed.mp3') + audio.export(compressed_path, format='mp3', bitrate='32k') + # log.debug(f"Compressed audio to {compressed_path}") # Uncomment if log is defined + + return compressed_path + else: + return file_path + + +def split_audio(file_path, max_bytes, format='mp3', bitrate='32k'): + """ + Splits audio into chunks not exceeding max_bytes. + Returns a list of chunk file paths. If audio fits, returns list with original path. + """ + file_size = os.path.getsize(file_path) + if file_size <= max_bytes: + return [file_path] # Nothing to split + + audio = AudioSegment.from_file(file_path) + duration_ms = len(audio) + orig_size = file_size + + approx_chunk_ms = max(int(duration_ms * (max_bytes / orig_size)) - 1000, 1000) + chunks = [] + start = 0 + i = 0 + + base, _ = os.path.splitext(file_path) + + while start < duration_ms: + end = min(start + approx_chunk_ms, duration_ms) + chunk = audio[start:end] + chunk_path = f'{base}_chunk_{i}.{format}' + chunk.export(chunk_path, format=format, bitrate=bitrate) + + # Reduce chunk duration if still too large + while os.path.getsize(chunk_path) > max_bytes and (end - start) > 5000: + end = start + ((end - start) // 2) + chunk = audio[start:end] + chunk.export(chunk_path, format=format, bitrate=bitrate) + + if os.path.getsize(chunk_path) > max_bytes: + os.remove(chunk_path) + raise Exception('Audio chunk cannot be reduced below max file size.') + + chunks.append(chunk_path) + start = end + i += 1 + + return chunks + + +@router.post('/transcriptions') +async def transcription( + request: Request, + file: UploadFile = File(...), + language: Optional[str] = Form(None), + user=Depends(get_verified_user), +): + if user.role != 'admin' and not await has_permission( + user.id, 'chat.stt', request.app.state.config.USER_PERMISSIONS + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + log.info(f'file.content_type: {file.content_type}') + stt_supported_content_types = getattr(request.app.state.config, 'STT_SUPPORTED_CONTENT_TYPES', []) + + if not strict_match_mime_type(stt_supported_content_types, file.content_type): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED, + ) + + try: + safe_name = os.path.basename(file.filename) if file.filename else '' + ext = safe_name.rsplit('.', 1)[-1] if '.' in safe_name else '' + + id = uuid.uuid4() + + filename = f'{id}.{ext}' + contents = file.file.read() + + file_dir = os.path.join(CACHE_DIR, 'audio', 'transcriptions') + os.makedirs(file_dir, exist_ok=True) + file_path = os.path.join(file_dir, filename) + + # Defense-in-depth: ensure resolved path stays within intended directory + if not os.path.realpath(file_path).startswith(os.path.realpath(file_dir)): + raise ValueError('Invalid file path detected') + + with open(file_path, 'wb') as f: + f.write(contents) + + try: + metadata = None + + if language: + metadata = {'language': language} + + result = transcribe(request, file_path, metadata, user) + + return { + **result, + 'filename': os.path.basename(file_path), + } + + except HTTPException: + raise + except Exception as e: + log.exception(e) + + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Transcription failed.', + ) + + except HTTPException: + raise + except Exception as e: + log.exception(e) + + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Transcription failed.', + ) + + +async def get_available_models(request: Request) -> list[dict]: + available_models = [] + if request.app.state.config.TTS_ENGINE == 'openai': + # Use custom endpoint if not using the official OpenAI API URL + if not request.app.state.config.TTS_OPENAI_API_BASE_URL.startswith('https://api.openai.com'): + timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + try: + async with session.get( + f'{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/models', + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() + available_models = data.get('models', []) + except Exception as e: + log.debug(f'/audio/models not available, trying /models fallback: {str(e)}') + # Fallback to standard OpenAI-compatible /models endpoint + # (used by KokoroTTS and similar custom TTS servers) + try: + async with session.get( + f'{request.app.state.config.TTS_OPENAI_API_BASE_URL}/models', + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() + # OpenAI /models returns {"data": [...]}, /audio/models returns {"models": [...]} + available_models = data.get('data', data.get('models', [])) + except Exception as e2: + log.error(f'Error fetching models from custom endpoint: {str(e2)}') + available_models = [{'id': 'tts-1'}, {'id': 'tts-1-hd'}] + else: + available_models = [{'id': 'tts-1'}, {'id': 'tts-1-hd'}] + elif request.app.state.config.TTS_ENGINE == 'elevenlabs': + try: + timeout = aiohttp.ClientTimeout(total=5) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.get( + f'{ELEVENLABS_API_BASE_URL}/v1/models', + headers={ + 'xi-api-key': request.app.state.config.TTS_API_KEY, + 'Content-Type': 'application/json', + }, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + models = await response.json() + available_models = [{'name': model['name'], 'id': model['model_id']} for model in models] + except Exception as e: + log.error(f'Error fetching models: {str(e)}') + elif request.app.state.config.TTS_ENGINE == 'mistral': + available_models = [{'id': 'voxtral-mini-tts-2603'}] + return available_models + + +@router.get('/models') +async def get_models(request: Request, user=Depends(get_verified_user)): + return {'models': await get_available_models(request)} + + +async def get_available_voices(request) -> dict: + """Returns {voice_id: voice_name} dict""" + available_voices = {} + if request.app.state.config.TTS_ENGINE == 'openai': + # Use custom endpoint if not using the official OpenAI API URL + if not request.app.state.config.TTS_OPENAI_API_BASE_URL.startswith('https://api.openai.com'): + try: + timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.get( + f'{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/voices', + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() + voices_list = data.get('voices', []) + available_voices = {voice['id']: voice['name'] for voice in voices_list} + except Exception as e: + log.error(f'Error fetching voices from custom endpoint: {str(e)}') + available_voices = { + 'alloy': 'alloy', + 'echo': 'echo', + 'fable': 'fable', + 'onyx': 'onyx', + 'nova': 'nova', + 'shimmer': 'shimmer', + } + else: + available_voices = { + 'alloy': 'alloy', + 'echo': 'echo', + 'fable': 'fable', + 'onyx': 'onyx', + 'nova': 'nova', + 'shimmer': 'shimmer', + } + elif request.app.state.config.TTS_ENGINE == 'elevenlabs': + try: + available_voices = await get_elevenlabs_voices(api_key=request.app.state.config.TTS_API_KEY) + except Exception: + # Avoided @lru_cache with exception + pass + elif request.app.state.config.TTS_ENGINE == 'azure': + try: + region = request.app.state.config.TTS_AZURE_SPEECH_REGION + base_url = request.app.state.config.TTS_AZURE_SPEECH_BASE_URL + url = (base_url or f'https://{region}.tts.speech.microsoft.com') + '/cognitiveservices/voices/list' + headers = {'Ocp-Apim-Subscription-Key': request.app.state.config.TTS_API_KEY} + + timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.get(url, headers=headers, ssl=AIOHTTP_CLIENT_SESSION_SSL) as response: + response.raise_for_status() + voices = await response.json() + + for voice in voices: + available_voices[voice['ShortName']] = f'{voice["DisplayName"]} ({voice["ShortName"]})' + except Exception as e: + log.error(f'Error fetching voices: {str(e)}') + elif request.app.state.config.TTS_ENGINE == 'mistral': + api_key = request.app.state.config.TTS_MISTRAL_API_KEY + api_base_url = request.app.state.config.TTS_MISTRAL_API_BASE_URL or 'https://api.mistral.ai/v1' + + if api_key: + try: + timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.get( + f'{api_base_url}/audio/voices', + headers={ + 'Authorization': f'Bearer {api_key}', + }, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + voices_data = await response.json() + + # Mistral returns a paginated response: {"items": [...], "page": ..., "total": ...} + voices_list = voices_data.get('items', []) if isinstance(voices_data, dict) else voices_data + for voice in voices_list: + if isinstance(voice, dict): + voice_id = voice.get('voice_id', voice.get('id', '')) + voice_name = voice.get('name', voice_id) + if voice_id: + available_voices[voice_id] = voice_name + except Exception as e: + log.error(f'Error fetching Mistral voices: {str(e)}') + + return available_voices + + +async def get_elevenlabs_voices(api_key: str) -> dict: + """ + Note, set the following in your .env file to use Elevenlabs: + AUDIO_TTS_ENGINE=elevenlabs + AUDIO_TTS_API_KEY=sk_... # Your Elevenlabs API key + AUDIO_TTS_VOICE=EXAVITQu4vr4xnSDxMaL # From https://api.elevenlabs.io/v1/voices + AUDIO_TTS_MODEL=eleven_multilingual_v2 + """ + + try: + # TODO: Add retries + timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.get( + f'{ELEVENLABS_API_BASE_URL}/v1/voices', + headers={ + 'xi-api-key': api_key, + 'Content-Type': 'application/json', + }, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + voices_data = await response.json() + + voices = {} + for voice in voices_data.get('voices', []): + voices[voice['voice_id']] = voice['name'] + except Exception as e: + log.error(f'Error fetching voices: {str(e)}') + raise RuntimeError(f'Error fetching voices: {str(e)}') + + return voices + + +@router.get('/voices') +async def get_voices(request: Request, user=Depends(get_verified_user)): + return {'voices': [{'id': k, 'name': v} for k, v in (await get_available_voices(request)).items()]} diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py new file mode 100644 index 0000000000000000000000000000000000000000..6d2349f89f468940f80dd613179941e9b2cd3739 --- /dev/null +++ b/backend/open_webui/routers/auths.py @@ -0,0 +1,1381 @@ +import asyncio +import re +import uuid +import time +import datetime +import logging +from aiohttp import ClientSession +import urllib + + +from open_webui.models.auths import ( + AddUserForm, + ApiKey, + Auths, + Token, + LdapForm, + SigninForm, + SigninResponse, + SignupForm, + UpdatePasswordForm, +) +from open_webui.models.users import ( + UserModel, + UserProfileImageResponse, + Users, + UpdateProfileForm, + UserStatus, +) +from open_webui.models.groups import Groups +from open_webui.models.oauth_sessions import OAuthSessions + +from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES +from open_webui.env import ( + WEBUI_AUTH, + WEBUI_AUTH_TRUSTED_EMAIL_HEADER, + WEBUI_AUTH_TRUSTED_NAME_HEADER, + WEBUI_AUTH_TRUSTED_GROUPS_HEADER, + WEBUI_AUTH_TRUSTED_ROLE_HEADER, + WEBUI_AUTH_COOKIE_SAME_SITE, + WEBUI_AUTH_COOKIE_SECURE, + WEBUI_AUTH_SIGNOUT_REDIRECT_URL, + ENABLE_INITIAL_ADMIN_SIGNUP, + ENABLE_OAUTH_TOKEN_EXCHANGE, + AIOHTTP_CLIENT_SESSION_SSL, +) +from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi.responses import RedirectResponse, Response, JSONResponse +from open_webui.config import ( + OPENID_PROVIDER_URL, + OPENID_END_SESSION_ENDPOINT, + ENABLE_OAUTH_SIGNUP, + ENABLE_LDAP, + ENABLE_PASSWORD_AUTH, + OAUTH_PROVIDERS, + OAUTH_MERGE_ACCOUNTS_BY_EMAIL, +) +from open_webui.utils.oauth import auth_manager_config +from pydantic import BaseModel + +from open_webui.utils.misc import parse_duration, validate_email_format +from open_webui.utils.auth import ( + validate_password, + verify_password, + decode_token, + invalidate_token, + create_api_key, + create_token, + get_admin_user, + get_verified_user, + get_current_user, + get_password_hash, + get_http_authorization_cred, +) +from open_webui.internal.db import get_async_session +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.utils.webhook import post_webhook +from open_webui.utils.access_control import get_permissions, has_permission +from open_webui.utils.groups import apply_default_group_assignment + +from open_webui.utils.redis import get_redis_client +from open_webui.utils.rate_limit import RateLimiter + + +from typing import Optional, List + +from ssl import CERT_NONE, CERT_REQUIRED, PROTOCOL_TLS + +from ldap3 import Server, Connection, NONE, Tls +from ldap3.utils.conv import escape_filter_chars + +router = APIRouter() + +log = logging.getLogger(__name__) + +# Forgive us our failed attempts, as we forgive those +# who exceed their allotted rate against this gate. +signin_rate_limiter = RateLimiter(redis_client=get_redis_client(), limit=5 * 3, window=60 * 3) + + +async def create_session_response( + request: Request, user, db, response: Response = None, set_cookie: bool = False +) -> dict: + """ + Create JWT token and build session response for a user. + Shared helper for signin, signup, ldap_auth, add_user, and token_exchange endpoints. + + Args: + request: FastAPI request object + user: User object + db: Database session + response: FastAPI response object (required if set_cookie is True) + set_cookie: Whether to set the auth cookie on the response + """ + expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN) + expires_at = None + if expires_delta: + expires_at = int(time.time()) + int(expires_delta.total_seconds()) + + token = create_token( + data={'id': user.id}, + expires_delta=expires_delta, + ) + + if set_cookie and response: + datetime_expires_at = datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc) if expires_at else None + max_age = int(expires_delta.total_seconds()) if expires_delta else None + response.set_cookie( + key='token', + value=token, + expires=datetime_expires_at, + httponly=True, + samesite=WEBUI_AUTH_COOKIE_SAME_SITE, + secure=WEBUI_AUTH_COOKIE_SECURE, + **({'max_age': max_age} if max_age is not None else {}), + ) + + user_permissions = await get_permissions(user.id, request.app.state.config.USER_PERMISSIONS, db=db) + + return { + 'token': token, + 'token_type': 'Bearer', + 'expires_at': expires_at, + 'id': user.id, + 'email': user.email, + 'name': user.name, + 'role': user.role, + 'profile_image_url': f'/api/v1/users/{user.id}/profile/image', + 'permissions': user_permissions, + } + + +############################ +# GetSessionUser +############################ + + +class SessionUserResponse(Token, UserProfileImageResponse): + expires_at: Optional[int] = None + permissions: Optional[dict] = None + + +class SessionUserInfoResponse(SessionUserResponse, UserStatus): + bio: Optional[str] = None + gender: Optional[str] = None + date_of_birth: Optional[datetime.date] = None + + +@router.get('/', response_model=SessionUserInfoResponse) +async def get_session_user( + request: Request, + response: Response, + user=Depends(get_current_user), + db: AsyncSession = Depends(get_async_session), +): + token = None + auth_header = request.headers.get('Authorization') + if auth_header: + auth_token = get_http_authorization_cred(auth_header) + if auth_token is not None: + token = auth_token.credentials + if token is None: + token = request.cookies.get('token') + if token is None and getattr(request.state, 'token', None): + token = request.state.token.credentials + data = decode_token(token) if token else None + + expires_at = None + + if data: + expires_at = data.get('exp') + + if (expires_at is not None) and int(time.time()) > expires_at: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) + + # Set the cookie token + max_age = int(expires_at - time.time()) if expires_at else None + response.set_cookie( + key='token', + value=token, + expires=(datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc) if expires_at else None), + httponly=True, # Ensures the cookie is not accessible via JavaScript + samesite=WEBUI_AUTH_COOKIE_SAME_SITE, + secure=WEBUI_AUTH_COOKIE_SECURE, + **({'max_age': max_age} if max_age is not None else {}), + ) + + user_permissions = await get_permissions(user.id, request.app.state.config.USER_PERMISSIONS, db=db) + + return { + 'token': token, + 'token_type': 'Bearer', + 'expires_at': expires_at, + 'id': user.id, + 'email': user.email, + 'name': user.name, + 'role': user.role, + 'profile_image_url': user.profile_image_url, + 'bio': user.bio, + 'gender': user.gender, + 'date_of_birth': user.date_of_birth, + 'status_emoji': user.status_emoji, + 'status_message': user.status_message, + 'status_expires_at': user.status_expires_at, + 'permissions': user_permissions, + } + + +############################ +# Update Profile +############################ + + +@router.post('/update/profile', response_model=UserProfileImageResponse) +async def update_profile( + form_data: UpdateProfileForm, + session_user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if session_user: + user = await Users.update_user_by_id( + session_user.id, + form_data.model_dump(), + db=db, + ) + if user: + return user + else: + raise HTTPException(400, detail=ERROR_MESSAGES.DEFAULT()) + else: + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + + +############################ +# Update Timezone +############################ + + +class UpdateTimezoneForm(BaseModel): + timezone: str + + +@router.post('/update/timezone') +async def update_timezone( + form_data: UpdateTimezoneForm, + session_user=Depends(get_current_user), + db: AsyncSession = Depends(get_async_session), +): + if session_user: + await Users.update_user_by_id( + session_user.id, + {'timezone': form_data.timezone}, + db=db, + ) + return {'status': True} + else: + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + + +############################ +# Update Password +############################ + + +@router.post('/update/password', response_model=bool) +async def update_password( + form_data: UpdatePasswordForm, + session_user=Depends(get_current_user), + db: AsyncSession = Depends(get_async_session), +): + if WEBUI_AUTH_TRUSTED_EMAIL_HEADER: + raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED) + if session_user: + user = await Auths.authenticate_user( + session_user.email, + lambda pw: verify_password(form_data.password, pw), + db=db, + ) + + if user: + try: + validate_password(form_data.new_password) + except Exception as e: + raise HTTPException(400, detail=str(e)) + hashed = get_password_hash(form_data.new_password) + return await Auths.update_user_password_by_id(user.id, hashed, db=db) + else: + raise HTTPException(400, detail=ERROR_MESSAGES.INCORRECT_PASSWORD) + else: + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + + +############################ +# LDAP Authentication +############################ +@router.post('/ldap', response_model=SessionUserResponse) +async def ldap_auth( + request: Request, + response: Response, + form_data: LdapForm, + db: AsyncSession = Depends(get_async_session), +): + # Security checks FIRST - before loading any config + if not request.app.state.config.ENABLE_LDAP: + raise HTTPException(400, detail='LDAP authentication is not enabled') + + if not ENABLE_PASSWORD_AUTH: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACTION_PROHIBITED, + ) + + # Reject empty passwords before attempting the LDAP bind. + # Per RFC 4513 §5.1.2, a Simple Bind with a non-empty DN but empty + # password is "unauthenticated simple authentication" — many LDAP + # servers (OpenLDAP default, some AD configs) return success for these, + # which would grant access without valid credentials. + if not form_data.password or not form_data.password.strip(): + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + + # NOW load LDAP config variables + LDAP_SERVER_LABEL = request.app.state.config.LDAP_SERVER_LABEL + LDAP_SERVER_HOST = request.app.state.config.LDAP_SERVER_HOST + LDAP_SERVER_PORT = request.app.state.config.LDAP_SERVER_PORT + LDAP_ATTRIBUTE_FOR_MAIL = request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL + LDAP_ATTRIBUTE_FOR_USERNAME = request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME + LDAP_SEARCH_BASE = request.app.state.config.LDAP_SEARCH_BASE + LDAP_SEARCH_FILTERS = request.app.state.config.LDAP_SEARCH_FILTERS + LDAP_APP_DN = request.app.state.config.LDAP_APP_DN + LDAP_APP_PASSWORD = request.app.state.config.LDAP_APP_PASSWORD + LDAP_USE_TLS = request.app.state.config.LDAP_USE_TLS + LDAP_CA_CERT_FILE = request.app.state.config.LDAP_CA_CERT_FILE + LDAP_VALIDATE_CERT = CERT_REQUIRED if request.app.state.config.LDAP_VALIDATE_CERT else CERT_NONE + LDAP_CIPHERS = request.app.state.config.LDAP_CIPHERS if request.app.state.config.LDAP_CIPHERS else 'ALL' + + try: + tls = Tls( + validate=LDAP_VALIDATE_CERT, + version=PROTOCOL_TLS, + ca_certs_file=LDAP_CA_CERT_FILE, + ciphers=LDAP_CIPHERS, + ) + except Exception as e: + log.error(f'TLS configuration error: {str(e)}') + raise HTTPException(400, detail='Failed to configure TLS for LDAP connection.') + + try: + server = Server( + host=LDAP_SERVER_HOST, + port=LDAP_SERVER_PORT, + get_info=NONE, + use_ssl=LDAP_USE_TLS, + tls=tls, + ) + connection_app = Connection( + server, + LDAP_APP_DN, + LDAP_APP_PASSWORD, + auto_bind='NONE', + authentication='SIMPLE' if LDAP_APP_DN else 'ANONYMOUS', + ) + if not await asyncio.to_thread(connection_app.bind): + raise HTTPException(400, detail='Application account bind failed') + + ENABLE_LDAP_GROUP_MANAGEMENT = request.app.state.config.ENABLE_LDAP_GROUP_MANAGEMENT + ENABLE_LDAP_GROUP_CREATION = request.app.state.config.ENABLE_LDAP_GROUP_CREATION + LDAP_ATTRIBUTE_FOR_GROUPS = request.app.state.config.LDAP_ATTRIBUTE_FOR_GROUPS + + search_attributes = [ + f'{LDAP_ATTRIBUTE_FOR_USERNAME}', + f'{LDAP_ATTRIBUTE_FOR_MAIL}', + 'cn', + ] + if ENABLE_LDAP_GROUP_MANAGEMENT: + search_attributes.append(f'{LDAP_ATTRIBUTE_FOR_GROUPS}') + log.info(f'LDAP Group Management enabled. Adding {LDAP_ATTRIBUTE_FOR_GROUPS} to search attributes') + log.info(f'LDAP search attributes: {search_attributes}') + + search_success = await asyncio.to_thread( + connection_app.search, + search_base=LDAP_SEARCH_BASE, + search_filter=f'(&({LDAP_ATTRIBUTE_FOR_USERNAME}={escape_filter_chars(form_data.user.lower())}){LDAP_SEARCH_FILTERS})', + attributes=search_attributes, + ) + if not search_success or not connection_app.entries: + raise HTTPException(400, detail='User not found in the LDAP server') + + entry = connection_app.entries[0] + entry_username = entry[f'{LDAP_ATTRIBUTE_FOR_USERNAME}'].value + email = entry[f'{LDAP_ATTRIBUTE_FOR_MAIL}'].value # retrieve the Attribute value + + username_list = [] # list of usernames from LDAP attribute + if isinstance(entry_username, list): + username_list = [str(name).lower() for name in entry_username] + else: + username_list = [str(entry_username).lower()] + + # TODO: support multiple emails if LDAP returns a list + if not email: + raise HTTPException(400, 'User does not have a valid email address.') + elif isinstance(email, str): + email = email.lower() + elif isinstance(email, list): + email = email[0].lower() + else: + email = str(email).lower() + + cn = str(entry['cn']) # common name + user_dn = entry.entry_dn # user distinguished name + + user_groups = [] + if ENABLE_LDAP_GROUP_MANAGEMENT and LDAP_ATTRIBUTE_FOR_GROUPS in entry: + group_dns = entry[LDAP_ATTRIBUTE_FOR_GROUPS] + log.info(f'LDAP raw group DNs for user {username_list}: {group_dns}') + + if group_dns: + log.info(f'LDAP group_dns original: {group_dns}') + log.info(f'LDAP group_dns type: {type(group_dns)}') + log.info(f'LDAP group_dns length: {len(group_dns)}') + + if hasattr(group_dns, 'value'): + group_dns = group_dns.value + log.info(f'Extracted .value property: {group_dns}') + elif hasattr(group_dns, '__iter__') and not isinstance(group_dns, (str, bytes)): + group_dns = list(group_dns) + log.info(f'Converted to list: {group_dns}') + + if isinstance(group_dns, list): + group_dns = [str(item) for item in group_dns] + else: + group_dns = [str(group_dns)] + + log.info(f'LDAP group_dns after processing - type: {type(group_dns)}, length: {len(group_dns)}') + + for group_idx, group_dn in enumerate(group_dns): + group_dn = str(group_dn) + log.info(f'Processing group DN #{group_idx + 1}: {group_dn}') + + try: + group_cn = None + + for item in group_dn.split(','): + item = item.strip() + if item.upper().startswith('CN='): + group_cn = item[3:] + break + + if group_cn: + user_groups.append(group_cn) + + else: + log.warning(f'Could not extract CN from group DN: {group_dn}') + except Exception as e: + log.warning(f'Failed to extract group name from DN {group_dn}: {e}') + + log.info(f'LDAP groups for user {username_list}: {user_groups} (total: {len(user_groups)})') + else: + log.info(f'No groups found for user {username_list}') + elif ENABLE_LDAP_GROUP_MANAGEMENT: + log.warning( + f'LDAP Group Management enabled but {LDAP_ATTRIBUTE_FOR_GROUPS} attribute not found in user entry' + ) + + if username_list and form_data.user.lower() in username_list: + connection_user = Connection( + server, + user_dn, + form_data.password, + auto_bind='NONE', + authentication='SIMPLE', + ) + if not await asyncio.to_thread(connection_user.bind): + raise HTTPException(400, 'Authentication failed.') + + user = await Users.get_user_by_email(email, db=db) + if not user: + try: + # Insert with default role first to avoid TOCTOU race on + # first-user registration. Matches signup_handler pattern. + user = await Auths.insert_new_auth( + email=email, + password=str(uuid.uuid4()), + name=cn, + role=request.app.state.config.DEFAULT_USER_ROLE, + db=db, + ) + + if not user: + raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) + + # Atomically check if this is the only user *after* the + # insert. Only the single user present should become admin. + if await Users.get_num_users(db=db) == 1: + await Users.update_user_role_by_id(user.id, 'admin', db=db) + user = await Users.get_user_by_id(user.id, db=db) + + await apply_default_group_assignment( + request.app.state.config.DEFAULT_GROUP_ID, + user.id, + db=db, + ) + + except HTTPException: + raise + except Exception as err: + log.error(f'LDAP user creation error: {str(err)}') + raise HTTPException(500, detail='Internal error occurred during LDAP user creation.') + + user = await Auths.authenticate_user_by_email(email, db=db) + + if user: + if ENABLE_LDAP_GROUP_MANAGEMENT and user_groups: + if ENABLE_LDAP_GROUP_CREATION: + await Groups.create_groups_by_group_names(user.id, user_groups, db=db) + try: + await Groups.sync_groups_by_group_names(user.id, user_groups, db=db) + log.info(f'Successfully synced groups for user {user.id}: {user_groups}') + except Exception as e: + log.error(f'Failed to sync groups for user {user.id}: {e}') + + return await create_session_response(request, user, db, response, set_cookie=True) + else: + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + else: + raise HTTPException(400, 'User record mismatch.') + except Exception as e: + log.error(f'LDAP authentication error: {str(e)}') + raise HTTPException(400, detail='LDAP authentication failed.') + + +############################ +# SignIn +############################ + + +@router.post('/signin', response_model=SessionUserResponse) +async def signin( + request: Request, + response: Response, + form_data: SigninForm, + db: AsyncSession = Depends(get_async_session), +): + if not ENABLE_PASSWORD_AUTH: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACTION_PROHIBITED, + ) + + if WEBUI_AUTH_TRUSTED_EMAIL_HEADER: + if WEBUI_AUTH_TRUSTED_EMAIL_HEADER not in request.headers: + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER) + + email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower() + name = email + + if WEBUI_AUTH_TRUSTED_NAME_HEADER: + name = request.headers.get(WEBUI_AUTH_TRUSTED_NAME_HEADER, email) + try: + name = urllib.parse.unquote(name, encoding='utf-8') + except Exception as e: + pass + + if not await Users.get_user_by_email(email.lower(), db=db): + await signup_handler( + request, + email, + str(uuid.uuid4()), + name, + db=db, + ) + + user = await Auths.authenticate_user_by_email(email, db=db) + if user: + if WEBUI_AUTH_TRUSTED_GROUPS_HEADER: + group_names = request.headers.get(WEBUI_AUTH_TRUSTED_GROUPS_HEADER, '').split(',') + group_names = [name.strip() for name in group_names if name.strip()] + + if group_names: + await Groups.sync_groups_by_group_names(user.id, group_names, db=db) + + if WEBUI_AUTH_TRUSTED_ROLE_HEADER: + trusted_role = request.headers.get(WEBUI_AUTH_TRUSTED_ROLE_HEADER, '').lower().strip() + if trusted_role in {'admin', 'user', 'pending'}: + if user.role != trusted_role: + await Users.update_user_role_by_id(user.id, trusted_role, db=db) + elif trusted_role: + log.warning(f'Ignoring invalid trusted role header value: {trusted_role}') + + elif WEBUI_AUTH == False: + admin_email = 'admin@localhost' + admin_password = 'admin' + + if await Users.get_user_by_email(admin_email.lower(), db=db): + user = await Auths.authenticate_user( + admin_email.lower(), + lambda pw: verify_password(admin_password, pw), + db=db, + ) + else: + if await Users.has_users(db=db): + raise HTTPException(400, detail=ERROR_MESSAGES.EXISTING_USERS) + + await signup_handler( + request, + admin_email, + admin_password, + 'User', + db=db, + ) + + user = await Auths.authenticate_user( + admin_email.lower(), + lambda pw: verify_password(admin_password, pw), + db=db, + ) + else: + if signin_rate_limiter.is_limited(form_data.email.lower()): + raise HTTPException( + status_code=status.HTTP_429_TOO_MANY_REQUESTS, + detail=ERROR_MESSAGES.RATE_LIMIT_EXCEEDED, + ) + + password_bytes = form_data.password.encode('utf-8') + if len(password_bytes) > 72: + # TODO: Implement other hashing algorithms that support longer passwords + log.info('Password too long, truncating to 72 bytes for bcrypt') + password_bytes = password_bytes[:72] + + # decode safely — ignore incomplete UTF-8 sequences + form_data.password = password_bytes.decode('utf-8', errors='ignore') + + user = await Auths.authenticate_user( + form_data.email.lower(), + lambda pw: verify_password(form_data.password, pw), + db=db, + ) + + if user: + return await create_session_response(request, user, db, response, set_cookie=True) + else: + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + + +############################ +# SignUp +############################ + + +async def signup_handler( + request: Request, + email: str, + password: str, + name: str, + profile_image_url: str = '/user.png', + *, + db: AsyncSession, +) -> UserModel: + """ + Core user-creation logic shared by the signup endpoint and + trusted-header / no-auth auto-registration flows. + + Returns the newly created UserModel. + Raises HTTPException on failure. + """ + # Insert with default role first to avoid TOCTOU race on first signup. + # If has_users() is checked before insert, concurrent requests during + # first-user registration can all see an empty table and each get admin. + hashed = get_password_hash(password) + + user = await Auths.insert_new_auth( + email=email.lower(), + password=hashed, + name=name, + profile_image_url=profile_image_url, + role=request.app.state.config.DEFAULT_USER_ROLE, + db=db, + ) + if not user: + raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) + + # Atomically check if this is the only user *after* the insert. + # Only the single user present at this point should become admin. + if await Users.get_num_users(db=db) == 1: + await Users.update_user_role_by_id(user.id, 'admin', db=db) + user = await Users.get_user_by_id(user.id, db=db) + request.app.state.config.ENABLE_SIGNUP = False + + if request.app.state.config.WEBHOOK_URL: + await post_webhook( + request.app.state.WEBUI_NAME, + request.app.state.config.WEBHOOK_URL, + WEBHOOK_MESSAGES.USER_SIGNUP(user.name), + { + 'action': 'signup', + 'message': WEBHOOK_MESSAGES.USER_SIGNUP(user.name), + 'user': user.model_dump_json(exclude_none=True), + }, + ) + + await apply_default_group_assignment( + request.app.state.config.DEFAULT_GROUP_ID, + user.id, + db=db, + ) + + return user + + +@router.post('/signup', response_model=SessionUserResponse) +async def signup( + request: Request, + response: Response, + form_data: SignupForm, + db: AsyncSession = Depends(get_async_session), +): + has_users = await Users.has_users(db=db) + + if WEBUI_AUTH: + if not request.app.state.config.ENABLE_SIGNUP or not request.app.state.config.ENABLE_LOGIN_FORM: + if has_users or not ENABLE_INITIAL_ADMIN_SIGNUP: + raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + else: + if has_users: + raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + + if not validate_email_format(form_data.email.lower()): + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT) + + if await Users.get_user_by_email(form_data.email.lower(), db=db): + raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) + + try: + try: + validate_password(form_data.password) + except Exception as e: + raise HTTPException(400, detail=str(e)) + + user = await signup_handler( + request, + form_data.email, + form_data.password, + form_data.name, + form_data.profile_image_url, + db=db, + ) + return await create_session_response(request, user, db, response, set_cookie=True) + except HTTPException: + raise + except Exception as err: + log.error(f'Signup error: {str(err)}') + raise HTTPException(500, detail='An internal error occurred during signup.') + + +@router.get('/signout') +async def signout(request: Request, response: Response, db: AsyncSession = Depends(get_async_session)): + # get auth token from headers or cookies + token = None + auth_header = request.headers.get('Authorization') + if auth_header: + auth_cred = get_http_authorization_cred(auth_header) + if auth_cred is not None: + token = auth_cred.credentials + if token is None: + token = request.cookies.get('token') + + if token: + await invalidate_token(request, token) + + response.delete_cookie('token') + response.delete_cookie('oui-session') + response.delete_cookie('oauth_id_token') + + oauth_session_id = request.cookies.get('oauth_session_id') + if oauth_session_id: + response.delete_cookie('oauth_session_id') + + session = await OAuthSessions.get_session_by_id(oauth_session_id, db=db) + + # If a custom end_session_endpoint is configured (e.g. AWS Cognito), redirect + # there directly instead of attempting OIDC discovery. + if OPENID_END_SESSION_ENDPOINT.value: + return JSONResponse( + status_code=200, + content={ + 'status': True, + 'redirect_url': OPENID_END_SESSION_ENDPOINT.value, + }, + headers=response.headers, + ) + + oauth_server_metadata_url = ( + request.app.state.oauth_manager.get_server_metadata_url(session.provider) if session else None + ) or OPENID_PROVIDER_URL.value + + if session and oauth_server_metadata_url: + oauth_id_token = session.token.get('id_token') + try: + async with ClientSession(trust_env=True) as session: + async with session.get(oauth_server_metadata_url, ssl=AIOHTTP_CLIENT_SESSION_SSL) as r: + if r.status == 200: + openid_data = await r.json() + logout_url = openid_data.get('end_session_endpoint') + + if logout_url: + return JSONResponse( + status_code=200, + content={ + 'status': True, + 'redirect_url': f'{logout_url}?id_token_hint={oauth_id_token}' + + ( + f'&post_logout_redirect_uri={WEBUI_AUTH_SIGNOUT_REDIRECT_URL}' + if WEBUI_AUTH_SIGNOUT_REDIRECT_URL + else '' + ), + }, + headers=response.headers, + ) + else: + raise Exception('Failed to fetch OpenID configuration') + + except Exception as e: + log.error(f'OpenID signout error: {str(e)}') + raise HTTPException( + status_code=500, + detail='Failed to sign out from the OpenID provider.', + headers=response.headers, + ) + + if WEBUI_AUTH_SIGNOUT_REDIRECT_URL: + return JSONResponse( + status_code=200, + content={ + 'status': True, + 'redirect_url': WEBUI_AUTH_SIGNOUT_REDIRECT_URL, + }, + headers=response.headers, + ) + + return JSONResponse(status_code=200, content={'status': True}, headers=response.headers) + + +############################ +# OAuth Session Management +############################ + + +@router.delete('/oauth/sessions/{provider:path}', response_model=bool) +async def delete_oauth_session_by_provider( + provider: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """ + Disconnect the current user's OAuth session for a specific provider. + The provider string matches the 'provider' field in the oauth_session table + (e.g. 'mcp:server-id' for MCP connections). + """ + result = await OAuthSessions.delete_sessions_by_user_id_and_provider(user.id, provider, db=db) + if not result: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='No OAuth session found for this provider', + ) + return True + + +############################ +# AddUser +############################ + + +@router.post('/add', response_model=SigninResponse) +async def add_user( + request: Request, + form_data: AddUserForm, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + if not validate_email_format(form_data.email.lower()): + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT) + + if await Users.get_user_by_email(form_data.email.lower(), db=db): + raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) + + try: + try: + validate_password(form_data.password) + except Exception as e: + raise HTTPException(400, detail=str(e)) + + hashed = get_password_hash(form_data.password) + user = await Auths.insert_new_auth( + form_data.email.lower(), + hashed, + form_data.name, + form_data.profile_image_url, + form_data.role, + db=db, + ) + + if user: + await apply_default_group_assignment( + request.app.state.config.DEFAULT_GROUP_ID, + user.id, + db=db, + ) + + expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN) + token = create_token(data={'id': user.id}, expires_delta=expires_delta) + return { + 'token': token, + 'token_type': 'Bearer', + 'id': user.id, + 'email': user.email, + 'name': user.name, + 'role': user.role, + 'profile_image_url': f'/api/v1/users/{user.id}/profile/image', + } + else: + raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) + except HTTPException: + raise + except Exception as err: + log.error(f'Add user error: {str(err)}') + raise HTTPException(500, detail='An internal error occurred while adding the user.') + + +############################ +# GetAdminDetails +############################ + + +@router.get('/admin/details') +async def get_admin_details( + request: Request, user=Depends(get_current_user), db: AsyncSession = Depends(get_async_session) +): + if request.app.state.config.SHOW_ADMIN_DETAILS: + admin_email = request.app.state.config.ADMIN_EMAIL + admin_name = None + + log.info(f'Admin details - Email: {admin_email}, Name: {admin_name}') + + if admin_email: + admin = await Users.get_user_by_email(admin_email, db=db) + if admin: + admin_name = admin.name + else: + admin = await Users.get_first_user(db=db) + if admin: + admin_email = admin.email + admin_name = admin.name + + return { + 'name': admin_name, + 'email': admin_email, + } + else: + raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED) + + +############################ +# ToggleSignUp +############################ + + +@router.get('/admin/config') +async def get_admin_config(request: Request, user=Depends(get_admin_user)): + return { + 'SHOW_ADMIN_DETAILS': request.app.state.config.SHOW_ADMIN_DETAILS, + 'ADMIN_EMAIL': request.app.state.config.ADMIN_EMAIL, + 'WEBUI_URL': request.app.state.config.WEBUI_URL, + 'ENABLE_SIGNUP': request.app.state.config.ENABLE_SIGNUP, + 'ENABLE_API_KEYS': request.app.state.config.ENABLE_API_KEYS, + 'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS': request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS, + 'API_KEYS_ALLOWED_ENDPOINTS': request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS, + 'DEFAULT_USER_ROLE': request.app.state.config.DEFAULT_USER_ROLE, + 'DEFAULT_GROUP_ID': request.app.state.config.DEFAULT_GROUP_ID, + 'JWT_EXPIRES_IN': request.app.state.config.JWT_EXPIRES_IN, + 'ENABLE_COMMUNITY_SHARING': request.app.state.config.ENABLE_COMMUNITY_SHARING, + 'ENABLE_MESSAGE_RATING': request.app.state.config.ENABLE_MESSAGE_RATING, + 'ENABLE_FOLDERS': request.app.state.config.ENABLE_FOLDERS, + 'FOLDER_MAX_FILE_COUNT': request.app.state.config.FOLDER_MAX_FILE_COUNT, + 'AUTOMATION_MAX_COUNT': request.app.state.config.AUTOMATION_MAX_COUNT, + 'AUTOMATION_MIN_INTERVAL': request.app.state.config.AUTOMATION_MIN_INTERVAL, + 'ENABLE_AUTOMATIONS': request.app.state.config.ENABLE_AUTOMATIONS, + 'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS, + 'ENABLE_CALENDAR': request.app.state.config.ENABLE_CALENDAR, + 'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES, + 'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES, + 'ENABLE_USER_WEBHOOKS': request.app.state.config.ENABLE_USER_WEBHOOKS, + 'ENABLE_USER_STATUS': request.app.state.config.ENABLE_USER_STATUS, + 'PENDING_USER_OVERLAY_TITLE': request.app.state.config.PENDING_USER_OVERLAY_TITLE, + 'PENDING_USER_OVERLAY_CONTENT': request.app.state.config.PENDING_USER_OVERLAY_CONTENT, + 'RESPONSE_WATERMARK': request.app.state.config.RESPONSE_WATERMARK, + } + + +class AdminConfig(BaseModel): + SHOW_ADMIN_DETAILS: bool + ADMIN_EMAIL: Optional[str] = None + WEBUI_URL: str + ENABLE_SIGNUP: bool + ENABLE_API_KEYS: bool + ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS: bool + API_KEYS_ALLOWED_ENDPOINTS: str + DEFAULT_USER_ROLE: str + DEFAULT_GROUP_ID: str + JWT_EXPIRES_IN: str + ENABLE_COMMUNITY_SHARING: bool + ENABLE_MESSAGE_RATING: bool + ENABLE_FOLDERS: bool + FOLDER_MAX_FILE_COUNT: Optional[int | str] = None + AUTOMATION_MAX_COUNT: Optional[int | str] = None + AUTOMATION_MIN_INTERVAL: Optional[int | str] = None + ENABLE_AUTOMATIONS: bool + ENABLE_CHANNELS: bool + ENABLE_CALENDAR: bool + ENABLE_MEMORIES: bool + ENABLE_NOTES: bool + ENABLE_USER_WEBHOOKS: bool + ENABLE_USER_STATUS: bool + PENDING_USER_OVERLAY_TITLE: Optional[str] = None + PENDING_USER_OVERLAY_CONTENT: Optional[str] = None + RESPONSE_WATERMARK: Optional[str] = None + + +@router.post('/admin/config') +async def update_admin_config(request: Request, form_data: AdminConfig, user=Depends(get_admin_user)): + request.app.state.config.SHOW_ADMIN_DETAILS = form_data.SHOW_ADMIN_DETAILS + request.app.state.config.ADMIN_EMAIL = form_data.ADMIN_EMAIL + request.app.state.config.WEBUI_URL = form_data.WEBUI_URL + request.app.state.config.ENABLE_SIGNUP = form_data.ENABLE_SIGNUP + + request.app.state.config.ENABLE_API_KEYS = form_data.ENABLE_API_KEYS + request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS = form_data.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS + request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS = form_data.API_KEYS_ALLOWED_ENDPOINTS + + request.app.state.config.ENABLE_FOLDERS = form_data.ENABLE_FOLDERS + request.app.state.config.FOLDER_MAX_FILE_COUNT = ( + int(form_data.FOLDER_MAX_FILE_COUNT) if form_data.FOLDER_MAX_FILE_COUNT else '' + ) + request.app.state.config.AUTOMATION_MAX_COUNT = ( + int(form_data.AUTOMATION_MAX_COUNT) if form_data.AUTOMATION_MAX_COUNT else '' + ) + request.app.state.config.AUTOMATION_MIN_INTERVAL = ( + int(form_data.AUTOMATION_MIN_INTERVAL) if form_data.AUTOMATION_MIN_INTERVAL else '' + ) + request.app.state.config.ENABLE_AUTOMATIONS = form_data.ENABLE_AUTOMATIONS + request.app.state.config.ENABLE_CHANNELS = form_data.ENABLE_CHANNELS + request.app.state.config.ENABLE_CALENDAR = form_data.ENABLE_CALENDAR + request.app.state.config.ENABLE_MEMORIES = form_data.ENABLE_MEMORIES + request.app.state.config.ENABLE_NOTES = form_data.ENABLE_NOTES + + if form_data.DEFAULT_USER_ROLE in ['pending', 'user', 'admin']: + request.app.state.config.DEFAULT_USER_ROLE = form_data.DEFAULT_USER_ROLE + + request.app.state.config.DEFAULT_GROUP_ID = form_data.DEFAULT_GROUP_ID + + pattern = r'^(-1|0|(-?\d+(\.\d+)?)(ms|s|m|h|d|w))$' + + # Check if the input string matches the pattern + if re.match(pattern, form_data.JWT_EXPIRES_IN): + request.app.state.config.JWT_EXPIRES_IN = form_data.JWT_EXPIRES_IN + + request.app.state.config.ENABLE_COMMUNITY_SHARING = form_data.ENABLE_COMMUNITY_SHARING + request.app.state.config.ENABLE_MESSAGE_RATING = form_data.ENABLE_MESSAGE_RATING + + request.app.state.config.ENABLE_USER_WEBHOOKS = form_data.ENABLE_USER_WEBHOOKS + request.app.state.config.ENABLE_USER_STATUS = form_data.ENABLE_USER_STATUS + + request.app.state.config.PENDING_USER_OVERLAY_TITLE = form_data.PENDING_USER_OVERLAY_TITLE + request.app.state.config.PENDING_USER_OVERLAY_CONTENT = form_data.PENDING_USER_OVERLAY_CONTENT + + request.app.state.config.RESPONSE_WATERMARK = form_data.RESPONSE_WATERMARK + + return { + 'SHOW_ADMIN_DETAILS': request.app.state.config.SHOW_ADMIN_DETAILS, + 'ADMIN_EMAIL': request.app.state.config.ADMIN_EMAIL, + 'WEBUI_URL': request.app.state.config.WEBUI_URL, + 'ENABLE_SIGNUP': request.app.state.config.ENABLE_SIGNUP, + 'ENABLE_API_KEYS': request.app.state.config.ENABLE_API_KEYS, + 'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS': request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS, + 'API_KEYS_ALLOWED_ENDPOINTS': request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS, + 'DEFAULT_USER_ROLE': request.app.state.config.DEFAULT_USER_ROLE, + 'DEFAULT_GROUP_ID': request.app.state.config.DEFAULT_GROUP_ID, + 'JWT_EXPIRES_IN': request.app.state.config.JWT_EXPIRES_IN, + 'ENABLE_COMMUNITY_SHARING': request.app.state.config.ENABLE_COMMUNITY_SHARING, + 'ENABLE_MESSAGE_RATING': request.app.state.config.ENABLE_MESSAGE_RATING, + 'ENABLE_FOLDERS': request.app.state.config.ENABLE_FOLDERS, + 'FOLDER_MAX_FILE_COUNT': request.app.state.config.FOLDER_MAX_FILE_COUNT, + 'AUTOMATION_MAX_COUNT': request.app.state.config.AUTOMATION_MAX_COUNT, + 'AUTOMATION_MIN_INTERVAL': request.app.state.config.AUTOMATION_MIN_INTERVAL, + 'ENABLE_AUTOMATIONS': request.app.state.config.ENABLE_AUTOMATIONS, + 'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS, + 'ENABLE_CALENDAR': request.app.state.config.ENABLE_CALENDAR, + 'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES, + 'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES, + 'ENABLE_USER_WEBHOOKS': request.app.state.config.ENABLE_USER_WEBHOOKS, + 'ENABLE_USER_STATUS': request.app.state.config.ENABLE_USER_STATUS, + 'PENDING_USER_OVERLAY_TITLE': request.app.state.config.PENDING_USER_OVERLAY_TITLE, + 'PENDING_USER_OVERLAY_CONTENT': request.app.state.config.PENDING_USER_OVERLAY_CONTENT, + 'RESPONSE_WATERMARK': request.app.state.config.RESPONSE_WATERMARK, + } + + +class LdapServerConfig(BaseModel): + label: str + host: str + port: Optional[int] = None + attribute_for_mail: str = 'mail' + attribute_for_username: str = 'uid' + app_dn: str + app_dn_password: str + search_base: str + search_filters: str = '' + use_tls: bool = True + certificate_path: Optional[str] = None + validate_cert: bool = True + ciphers: Optional[str] = 'ALL' + + +@router.get('/admin/config/ldap/server', response_model=LdapServerConfig) +async def get_ldap_server(request: Request, user=Depends(get_admin_user)): + return { + 'label': request.app.state.config.LDAP_SERVER_LABEL, + 'host': request.app.state.config.LDAP_SERVER_HOST, + 'port': request.app.state.config.LDAP_SERVER_PORT, + 'attribute_for_mail': request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL, + 'attribute_for_username': request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME, + 'app_dn': request.app.state.config.LDAP_APP_DN, + 'app_dn_password': request.app.state.config.LDAP_APP_PASSWORD, + 'search_base': request.app.state.config.LDAP_SEARCH_BASE, + 'search_filters': request.app.state.config.LDAP_SEARCH_FILTERS, + 'use_tls': request.app.state.config.LDAP_USE_TLS, + 'certificate_path': request.app.state.config.LDAP_CA_CERT_FILE, + 'validate_cert': request.app.state.config.LDAP_VALIDATE_CERT, + 'ciphers': request.app.state.config.LDAP_CIPHERS, + } + + +@router.post('/admin/config/ldap/server') +async def update_ldap_server(request: Request, form_data: LdapServerConfig, user=Depends(get_admin_user)): + required_fields = [ + 'label', + 'host', + 'attribute_for_mail', + 'attribute_for_username', + 'search_base', + ] + for key in required_fields: + value = getattr(form_data, key) + if not value: + raise HTTPException(400, detail=ERROR_MESSAGES.REQUIRED_FIELD_EMPTY(key)) + + request.app.state.config.LDAP_SERVER_LABEL = form_data.label + request.app.state.config.LDAP_SERVER_HOST = form_data.host + request.app.state.config.LDAP_SERVER_PORT = form_data.port + request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL = form_data.attribute_for_mail + request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = form_data.attribute_for_username + request.app.state.config.LDAP_APP_DN = form_data.app_dn or '' + request.app.state.config.LDAP_APP_PASSWORD = form_data.app_dn_password or '' + request.app.state.config.LDAP_SEARCH_BASE = form_data.search_base + request.app.state.config.LDAP_SEARCH_FILTERS = form_data.search_filters + request.app.state.config.LDAP_USE_TLS = form_data.use_tls + request.app.state.config.LDAP_CA_CERT_FILE = form_data.certificate_path + request.app.state.config.LDAP_VALIDATE_CERT = form_data.validate_cert + request.app.state.config.LDAP_CIPHERS = form_data.ciphers + + return { + 'label': request.app.state.config.LDAP_SERVER_LABEL, + 'host': request.app.state.config.LDAP_SERVER_HOST, + 'port': request.app.state.config.LDAP_SERVER_PORT, + 'attribute_for_mail': request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL, + 'attribute_for_username': request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME, + 'app_dn': request.app.state.config.LDAP_APP_DN, + 'app_dn_password': request.app.state.config.LDAP_APP_PASSWORD, + 'search_base': request.app.state.config.LDAP_SEARCH_BASE, + 'search_filters': request.app.state.config.LDAP_SEARCH_FILTERS, + 'use_tls': request.app.state.config.LDAP_USE_TLS, + 'certificate_path': request.app.state.config.LDAP_CA_CERT_FILE, + 'validate_cert': request.app.state.config.LDAP_VALIDATE_CERT, + 'ciphers': request.app.state.config.LDAP_CIPHERS, + } + + +@router.get('/admin/config/ldap') +async def get_ldap_config(request: Request, user=Depends(get_admin_user)): + return {'ENABLE_LDAP': request.app.state.config.ENABLE_LDAP} + + +class LdapConfigForm(BaseModel): + enable_ldap: Optional[bool] = None + + +@router.post('/admin/config/ldap') +async def update_ldap_config(request: Request, form_data: LdapConfigForm, user=Depends(get_admin_user)): + request.app.state.config.ENABLE_LDAP = form_data.enable_ldap + return {'ENABLE_LDAP': request.app.state.config.ENABLE_LDAP} + + +############################ +# API Key +############################ + + +# create api key +@router.post('/api_key', response_model=ApiKey) +async def generate_api_key( + request: Request, user=Depends(get_current_user), db: AsyncSession = Depends(get_async_session) +): + if not request.app.state.config.ENABLE_API_KEYS or ( + user.role != 'admin' + and not await has_permission(user.id, 'features.api_keys', request.app.state.config.USER_PERMISSIONS) + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.API_KEY_CREATION_NOT_ALLOWED, + ) + + api_key = create_api_key() + success = await Users.update_user_api_key_by_id(user.id, api_key, db=db) + + if success: + return { + 'api_key': api_key, + } + else: + raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_API_KEY_ERROR) + + +# delete api key +@router.delete('/api_key', response_model=bool) +async def delete_api_key(user=Depends(get_current_user), db: AsyncSession = Depends(get_async_session)): + return await Users.delete_user_api_key_by_id(user.id, db=db) + + +# get api key +@router.get('/api_key', response_model=ApiKey) +async def get_api_key(user=Depends(get_current_user), db: AsyncSession = Depends(get_async_session)): + api_key = await Users.get_user_api_key_by_id(user.id, db=db) + if api_key: + return { + 'api_key': api_key, + } + else: + raise HTTPException(404, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) + + +############################ +# Token Exchange +############################ + + +class TokenExchangeForm(BaseModel): + token: str # OAuth access token from external provider + + +@router.post('/oauth/{provider}/token/exchange', response_model=SessionUserResponse) +async def token_exchange( + request: Request, + response: Response, + provider: str, + form_data: TokenExchangeForm, + db: AsyncSession = Depends(get_async_session), +): + """ + Exchange an external OAuth provider token for an OpenWebUI JWT. + This endpoint is disabled by default. Set ENABLE_OAUTH_TOKEN_EXCHANGE=True to enable. + """ + if not ENABLE_OAUTH_TOKEN_EXCHANGE: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail='Token exchange is disabled', + ) + + provider = provider.lower() + + # Check if provider is configured + if provider not in OAUTH_PROVIDERS: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.OAUTH_NOT_CONFIGURED(provider), + ) + # Get the OAuth client for this provider + oauth_manager = request.app.state.oauth_manager + client = oauth_manager.get_client(provider) + if not client: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.OAUTH_NOT_CONFIGURED(provider), + ) + + # Validate the token by calling the userinfo endpoint + try: + token_data = {'access_token': form_data.token, 'token_type': 'Bearer'} + user_data = await client.userinfo(token=token_data) + + if not user_data: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Invalid token or unable to fetch user info', + ) + except Exception as e: + log.warning(f'Token exchange failed for provider {provider}: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Invalid token or unable to validate with provider', + ) + + # Extract user information from the token claims + email_claim = request.app.state.config.OAUTH_EMAIL_CLAIM + username_claim = request.app.state.config.OAUTH_USERNAME_CLAIM + + # Get sub claim + sub = user_data.get(request.app.state.config.OAUTH_SUB_CLAIM or OAUTH_PROVIDERS[provider].get('sub_claim', 'sub')) + if not sub: + log.warning(f'Token exchange failed: sub claim missing from user data') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Token missing required 'sub' claim", + ) + + email = user_data.get(email_claim, '') + if not email: + log.warning(f'Token exchange failed: email claim missing from user data') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Token missing required email claim', + ) + email = email.lower() + + # Enforce domain allowlist — same check as the normal OAuth callback + if ( + '*' not in auth_manager_config.OAUTH_ALLOWED_DOMAINS + and email.split('@')[-1] not in auth_manager_config.OAUTH_ALLOWED_DOMAINS + ): + log.warning(f'Token exchange denied: email domain not in allowed domains list') + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + # Try to find the user by OAuth sub + user = await Users.get_user_by_oauth_sub(provider, sub, db=db) + + if not user and OAUTH_MERGE_ACCOUNTS_BY_EMAIL.value: + # Try to find by email if merge is enabled + user = await Users.get_user_by_email(email, db=db) + if user: + # Link the OAuth sub to this user + await Users.update_user_oauth_by_id(user.id, provider, sub, db=db) + + if not user: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail='User not found. Please sign in via the web interface first.', + ) + + return await create_session_response(request, user, db) diff --git a/backend/open_webui/routers/automations.py b/backend/open_webui/routers/automations.py new file mode 100644 index 0000000000000000000000000000000000000000..4ff66feb9744bdcffc729b7087b640712329a522 --- /dev/null +++ b/backend/open_webui/routers/automations.py @@ -0,0 +1,304 @@ +import asyncio +import logging + +from typing import Optional +from fastapi import APIRouter, Depends, HTTPException, Request, status +from sqlalchemy.ext.asyncio import AsyncSession + +from open_webui.models.automations import ( + Automations, + AutomationRuns, + AutomationForm, + AutomationModel, + AutomationResponse, + AutomationRunModel, + AutomationListResponse, +) +from open_webui.utils.automations import ( + validate_rrule, + next_run_ns, + next_n_runs_ns, + execute_automation, + rrule_interval_seconds, +) +from open_webui.utils.auth import get_verified_user, get_admin_user +from open_webui.utils.access_control import has_permission +from open_webui.internal.db import get_async_session +from open_webui.constants import ERROR_MESSAGES + +log = logging.getLogger(__name__) + +router = APIRouter() + +PAGE_ITEM_COUNT = 30 + + +############################ +# Helpers +############################ + + +async def check_automations_permission(request, user): + if not request.app.state.config.ENABLE_AUTOMATIONS: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + if user.role != 'admin' and not await has_permission( + user.id, 'features.automations', request.app.state.config.USER_PERMISSIONS + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + +def check_automation_access(automation, user): + if not automation: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + if user.role != 'admin' and user.id != automation.user_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + +async def check_automation_limits(request, user, rrule_str: str, db, is_create: bool = False): + """Enforce global automation limits. Admins bypass all checks.""" + if user.role == 'admin': + return + + # Max count (create only) + if is_create: + max_count = request.app.state.config.AUTOMATION_MAX_COUNT + if max_count: + max_count = int(max_count) + if max_count > 0 and await Automations.count_by_user(user.id, db=db) >= max_count: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.AUTOMATION_LIMIT_EXCEEDED(max_count), + ) + + # Min interval (create + update) + min_interval = request.app.state.config.AUTOMATION_MIN_INTERVAL + if min_interval: + min_interval = int(min_interval) + if min_interval > 0: + interval = rrule_interval_seconds(rrule_str) + if interval is not None and interval < min_interval: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.AUTOMATION_TOO_FREQUENT(min_interval), + ) + + +async def enrich_automation(automation: AutomationModel, db: AsyncSession, tz: str = None) -> AutomationResponse: + """Full enrichment for single-item views (includes next_runs computation).""" + last_run = await AutomationRuns.get_latest(automation.id, db=db) + return AutomationResponse( + **automation.model_dump(), + last_run=last_run, + next_runs=next_n_runs_ns(automation.data['rrule'], tz=tz), + ) + + +############################ +# GetAutomationItems (paginated) +############################ + + +@router.get('/list') +async def get_automation_items( + request: Request, + query: Optional[str] = None, + status: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_automations_permission(request, user) + limit = PAGE_ITEM_COUNT + page = max(1, page) + skip = (page - 1) * limit + + result = await Automations.search_automations( + user_id=user.id, + query=query, + status=status, + skip=skip, + limit=limit, + db=db, + ) + + # Batch-fetch latest runs in a single query instead of N+1 + ids = [item.id for item in result.items] + latest_runs = await AutomationRuns.get_latest_batch(ids, db=db) if ids else {} + + return { + 'items': [ + AutomationResponse( + **item.model_dump(), + last_run=latest_runs.get(item.id), + ) + for item in result.items + ], + 'total': result.total, + } + + +############################ +# CreateNewAutomation +############################ + + +@router.post('/create', response_model=AutomationResponse) +async def create_new_automation( + request: Request, + form_data: AutomationForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_automations_permission(request, user) + try: + validate_rrule(form_data.data.rrule, tz=user.timezone) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + + await check_automation_limits(request, user, form_data.data.rrule, db, is_create=True) + + tz = user.timezone + automation = await Automations.insert(user.id, form_data, next_run_ns(form_data.data.rrule, tz=tz), db=db) + return await enrich_automation(automation, db, tz=tz) + + +############################ +# GetAutomationById +############################ + + +@router.get('/{id}', response_model=AutomationResponse) +async def get_automation_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_automations_permission(request, user) + automation = await Automations.get_by_id(id, db=db) + check_automation_access(automation, user) + return await enrich_automation(automation, db, tz=user.timezone) + + +############################ +# UpdateAutomationById +############################ + + +@router.post('/{id}/update', response_model=AutomationResponse) +async def update_automation_by_id( + request: Request, + id: str, + form_data: AutomationForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_automations_permission(request, user) + automation = await Automations.get_by_id(id, db=db) + check_automation_access(automation, user) + + try: + validate_rrule(form_data.data.rrule, tz=user.timezone) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + + await check_automation_limits(request, user, form_data.data.rrule, db, is_create=False) + + tz = user.timezone + updated = await Automations.update_by_id(id, form_data, next_run_ns(form_data.data.rrule, tz=tz), db=db) + return await enrich_automation(updated, db, tz=tz) + + +############################ +# ToggleAutomationById +############################ + + +@router.post('/{id}/toggle', response_model=AutomationResponse) +async def toggle_automation_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_automations_permission(request, user) + automation = await Automations.get_by_id(id, db=db) + check_automation_access(automation, user) + toggled = await Automations.toggle(id, next_run_ns(automation.data['rrule'], tz=user.timezone), db=db) + return await enrich_automation(toggled, db, tz=user.timezone) + + +############################ +# RunAutomationById +############################ + + +@router.post('/{id}/run') +async def run_automation_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_automations_permission(request, user) + automation = await Automations.get_by_id(id, db=db) + check_automation_access(automation, user) + asyncio.create_task(execute_automation(request.app, automation)) + return await enrich_automation(automation, db, tz=user.timezone) + + +############################ +# DeleteAutomationById +############################ + + +@router.delete('/{id}/delete') +async def delete_automation_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_automations_permission(request, user) + automation = await Automations.get_by_id(id, db=db) + check_automation_access(automation, user) + await AutomationRuns.delete_by_automation(id, db=db) + return await Automations.delete(id, db=db) + + +############################ +# GetAutomationRuns +############################ + + +@router.get('/{id}/runs', response_model=list[AutomationRunModel]) +async def get_automation_runs( + request: Request, + id: str, + skip: int = 0, + limit: int = 50, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_automations_permission(request, user) + automation = await Automations.get_by_id(id, db=db) + check_automation_access(automation, user) + return await AutomationRuns.get_by_automation(id, skip=skip, limit=limit, db=db) diff --git a/backend/open_webui/routers/calendar.py b/backend/open_webui/routers/calendar.py new file mode 100644 index 0000000000000000000000000000000000000000..c95888ebfac642cbf4653d9291968c1161ed469b --- /dev/null +++ b/backend/open_webui/routers/calendar.py @@ -0,0 +1,389 @@ +import logging +import time +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Request, status + +from open_webui.models.calendar import ( + Calendars, + CalendarEvents, + CalendarEventAttendees, + CalendarForm, + CalendarUpdateForm, + CalendarEventForm, + CalendarEventUpdateForm, + CalendarModel, + CalendarEventModel, + CalendarEventUserResponse, + CalendarEventListResponse, + RSVPForm, +) +from open_webui.models.access_grants import AccessGrants +from open_webui.models.groups import Groups +from open_webui.models.users import UserModel +from open_webui.utils.auth import get_verified_user +from open_webui.utils.access_control import has_permission +from open_webui.utils.calendar import expand_recurring_event +from open_webui.constants import ERROR_MESSAGES + +log = logging.getLogger(__name__) + +router = APIRouter() + +SCHEDULED_TASKS_CALENDAR_ID = '__scheduled_tasks__' + + +async def check_calendar_permission(request: Request, user): + """Check global feature flag AND per-user permission for calendar access.""" + if not request.app.state.config.ENABLE_CALENDAR: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + if user.role != 'admin' and not await has_permission( + user.id, 'features.calendar', request.app.state.config.USER_PERMISSIONS + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + +async def _user_has_automations(request: Request, user) -> bool: + """Check if automations feature is available to this user.""" + if not getattr(request.app.state.config, 'ENABLE_AUTOMATIONS', False): + return False + if user.role == 'admin': + return True + return await has_permission(user.id, 'features.automations', request.app.state.config.USER_PERMISSIONS) + + +async def _check_calendar_access(calendar_id: str, user: UserModel, permission: str = 'write') -> CalendarModel: + """Verify user has access to a calendar. Returns the calendar or raises 403/404.""" + cal = await Calendars.get_calendar_by_id(calendar_id) + if not cal: + raise HTTPException(status_code=404, detail='Calendar not found') + if cal.user_id == user.id or user.role == 'admin': + return cal + user_groups = await Groups.get_groups_by_member_id(user.id) + user_group_ids = [g.id for g in user_groups] + if await AccessGrants.has_access( + user_id=user.id, + resource_type='calendar', + resource_id=cal.id, + permission=permission, + user_group_ids=user_group_ids, + ): + return cal + raise HTTPException(status_code=403, detail='Access denied') + + +#################### +# Calendar CRUD (static paths first) +#################### + + +@router.get('/', response_model=list[CalendarModel]) +async def get_calendars(request: Request, user: UserModel = Depends(get_verified_user)): + """List user's calendars (owned + shared), plus a virtual Scheduled Tasks calendar + when automations are available.""" + await check_calendar_permission(request, user) + calendars = await Calendars.get_calendars_by_user(user.id) + + if await _user_has_automations(request, user): + now = int(time.time_ns()) + calendars.append( + CalendarModel( + id=SCHEDULED_TASKS_CALENDAR_ID, + user_id=user.id, + name='Scheduled Tasks', + color='#8b5cf6', + is_default=False, + is_system=True, + created_at=now, + updated_at=now, + ) + ) + + return calendars + + +@router.post('/create', response_model=CalendarModel) +async def create_calendar(request: Request, form_data: CalendarForm, user: UserModel = Depends(get_verified_user)): + """Create a new user calendar.""" + await check_calendar_permission(request, user) + return await Calendars.insert_new_calendar(user.id, form_data) + + +#################### +# Event CRUD (before /{calendar_id} to avoid route conflicts) +#################### + + +@router.get('/events') +async def get_events( + request: Request, + start: str, + end: str, + calendar_ids: Optional[str] = None, + user: UserModel = Depends(get_verified_user), +): + """Get events in date range. + + Args: + start: ISO 8601 datetime string (e.g. 2026-04-01T00:00:00) + end: ISO 8601 datetime string (e.g. 2026-05-01T00:00:00) + calendar_ids: optional comma-separated list to filter + + Includes: + - Stored events from the database + - Virtual events computed from active automation RRULEs (Scheduled Tasks calendar) + """ + await check_calendar_permission(request, user) + from datetime import datetime + + try: + start_dt = datetime.fromisoformat(start.replace('Z', '+00:00')) + end_dt = datetime.fromisoformat(end.replace('Z', '+00:00')) + except ValueError: + raise HTTPException(status_code=400, detail='Invalid date format. Use ISO 8601 (e.g. 2026-04-01T00:00:00)') + + NS = 1_000_000 + start_ns = int(start_dt.timestamp() * 1000) * NS + end_ns = int(end_dt.timestamp() * 1000) * NS + cal_id_list = calendar_ids.split(',') if calendar_ids else None + + # 1. Stored events + events = await CalendarEvents.get_events_by_range( + user_id=user.id, + start=start_ns, + end=end_ns, + calendar_ids=cal_id_list, + ) + + # Expand recurring stored events + expanded = [] + for event in events: + event_dict = event.model_dump() + if event_dict.get('rrule'): + instances = expand_recurring_event(event_dict, start_ns, end_ns, tz=user.timezone) + for inst in instances: + expanded.append(CalendarEventUserResponse(**{**inst, 'user': event.user})) + else: + expanded.append(event) + + # 2. Virtual automation events (Scheduled Tasks calendar) + if await _user_has_automations(request, user) and ( + cal_id_list is None or SCHEDULED_TASKS_CALENDAR_ID in cal_id_list + ): + try: + from open_webui.models.automations import Automations, AutomationRuns + + # Future runs: expand RRULEs for active automations only + active_automations = await Automations.get_active_by_user(user.id) + for auto in active_automations: + rrule_str = auto.data.get('rrule', '') if auto.data else '' + if not rrule_str: + continue + + virtual = { + 'id': f'auto_{auto.id}', + 'calendar_id': SCHEDULED_TASKS_CALENDAR_ID, + 'user_id': user.id, + 'title': auto.name, + 'description': auto.data.get('prompt', '') if auto.data else '', + 'start_at': auto.next_run_at or 0, + 'end_at': None, + 'all_day': False, + 'rrule': rrule_str, + 'color': None, + 'location': None, + 'data': None, + 'meta': {'automation_id': auto.id}, + 'is_cancelled': False, + 'attendees': [], + 'created_at': auto.created_at, + 'updated_at': auto.updated_at, + 'user': None, + } + + # Only expand into the future — past runs are handled below + now_ns = int(time.time_ns()) + rrule_start = max(start_ns, now_ns) + instances = expand_recurring_event(virtual, rrule_start, end_ns, tz=user.timezone) + for inst in instances: + expanded.append(CalendarEventUserResponse(**inst)) + + # Past runs: single range query joined with automation + runs_with_auto = await AutomationRuns.get_runs_by_user_range(user.id, start_ns, end_ns) + for run, auto in runs_with_auto: + expanded.append( + CalendarEventUserResponse( + id=f'run_{run.id}', + calendar_id=SCHEDULED_TASKS_CALENDAR_ID, + user_id=user.id, + title=auto.name, + description=run.error if run.status == 'error' else '', + start_at=run.created_at, + end_at=None, + all_day=False, + color=None, + location=None, + data=None, + meta={ + 'automation_id': auto.id, + 'run_id': run.id, + 'chat_id': run.chat_id, + 'status': run.status, + }, + is_cancelled=False, + attendees=[], + created_at=run.created_at, + updated_at=run.created_at, + user=None, + ) + ) + except Exception as e: + log.warning(f'Failed to compute automation events: {e}', exc_info=True) + + return [e.model_dump() if hasattr(e, 'model_dump') else e for e in expanded] + + +@router.post('/events/create', response_model=CalendarEventModel) +async def create_event(request: Request, form_data: CalendarEventForm, user: UserModel = Depends(get_verified_user)): + await check_calendar_permission(request, user) + await _check_calendar_access(form_data.calendar_id, user, 'write') + return await CalendarEvents.insert_new_event(user.id, form_data) + + +@router.get('/events/search', response_model=CalendarEventListResponse) +async def search_events( + request: Request, + query: Optional[str] = None, + skip: int = 0, + limit: int = 30, + user: UserModel = Depends(get_verified_user), +): + await check_calendar_permission(request, user) + return await CalendarEvents.search_events(user_id=user.id, query=query, skip=skip, limit=limit) + + +@router.get('/events/{event_id}', response_model=CalendarEventModel) +async def get_event(request: Request, event_id: str, user: UserModel = Depends(get_verified_user)): + await check_calendar_permission(request, user) + event = await CalendarEvents.get_event_by_id(event_id) + if not event: + raise HTTPException(status_code=404, detail='Event not found') + + await _check_calendar_access(event.calendar_id, user, 'read') + + return event + + +@router.post('/events/{event_id}/update', response_model=CalendarEventModel) +async def update_event( + request: Request, event_id: str, form_data: CalendarEventUpdateForm, user: UserModel = Depends(get_verified_user) +): + await check_calendar_permission(request, user) + event = await CalendarEvents.get_event_by_id(event_id) + if not event: + raise HTTPException(status_code=404, detail='Event not found') + + await _check_calendar_access(event.calendar_id, user, 'write') + + updated = await CalendarEvents.update_event_by_id(event_id, form_data) + if not updated: + raise HTTPException(status_code=500, detail='Failed to update') + return updated + + +@router.delete('/events/{event_id}/delete') +async def delete_event(request: Request, event_id: str, user: UserModel = Depends(get_verified_user)): + await check_calendar_permission(request, user) + event = await CalendarEvents.get_event_by_id(event_id) + if not event: + raise HTTPException(status_code=404, detail='Event not found') + + await _check_calendar_access(event.calendar_id, user, 'write') + + result = await CalendarEvents.delete_event_by_id(event_id) + if not result: + raise HTTPException(status_code=500, detail='Failed to delete') + return {'status': True} + + +@router.post('/events/{event_id}/rsvp', response_model=dict) +async def rsvp_event( + request: Request, event_id: str, form_data: RSVPForm, user: UserModel = Depends(get_verified_user) +): + """Update own RSVP status for an event.""" + await check_calendar_permission(request, user) + if form_data.status not in ('accepted', 'declined', 'tentative', 'pending'): + raise HTTPException(status_code=400, detail='Invalid status') + + result = await CalendarEventAttendees.update_rsvp(event_id, user.id, form_data.status) + if not result: + raise HTTPException(status_code=404, detail='Not an attendee of this event') + return {'status': True, 'rsvp': result.status} + + +#################### +# Calendar by ID (dynamic path — MUST come after /events* routes) +#################### + + +@router.get('/{calendar_id}', response_model=CalendarModel) +async def get_calendar_by_id(request: Request, calendar_id: str, user: UserModel = Depends(get_verified_user)): + await check_calendar_permission(request, user) + cal = await _check_calendar_access(calendar_id, user, 'read') + return cal + + +@router.post('/{calendar_id}/update', response_model=CalendarModel) +async def update_calendar( + request: Request, calendar_id: str, form_data: CalendarUpdateForm, user: UserModel = Depends(get_verified_user) +): + await check_calendar_permission(request, user) + cal = await _check_calendar_access(calendar_id, user, 'write') + + # Only owner/admin can change access grants + if form_data.access_grants is not None and cal.user_id != user.id and user.role != 'admin': + raise HTTPException(status_code=403, detail='Only owner can manage sharing') + + updated = await Calendars.update_calendar_by_id(calendar_id, form_data) + if not updated: + raise HTTPException(status_code=500, detail='Failed to update') + return updated + + +@router.delete('/{calendar_id}/delete') +async def delete_calendar(request: Request, calendar_id: str, user: UserModel = Depends(get_verified_user)): + await check_calendar_permission(request, user) + + # Block deletion of the virtual Scheduled Tasks calendar + if calendar_id == SCHEDULED_TASKS_CALENDAR_ID: + raise HTTPException(status_code=400, detail='System calendars cannot be deleted') + + cal = await _check_calendar_access(calendar_id, user, 'write') + + # Only owner/admin can delete + if cal.user_id != user.id and user.role != 'admin': + raise HTTPException(status_code=403, detail='Only owner can delete calendar') + + # Block deletion of default calendar + if cal.is_default: + raise HTTPException(status_code=400, detail='Default calendar cannot be deleted') + + result = await Calendars.delete_calendar_by_id(calendar_id) + if not result: + raise HTTPException(status_code=500, detail='Failed to delete') + return {'status': True} + + +@router.post('/{calendar_id}/default') +async def set_default_calendar(request: Request, calendar_id: str, user: UserModel = Depends(get_verified_user)): + await check_calendar_permission(request, user) + cal = await Calendars.set_default_calendar(user.id, calendar_id) + if not cal: + raise HTTPException(status_code=404, detail='Calendar not found') + return cal diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py new file mode 100644 index 0000000000000000000000000000000000000000..487899fccf47470cf715ec1db7d1b82b00e9f280 --- /dev/null +++ b/backend/open_webui/routers/channels.py @@ -0,0 +1,1847 @@ +import json +import logging +import base64 +import io +from typing import Optional + + +from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks +from fastapi.responses import Response, StreamingResponse, FileResponse +from pydantic import BaseModel +from pydantic import field_validator + +from open_webui.socket.main import ( + emit_to_users, + enter_room_for_users, + sio, + get_user_ids_from_room, +) +from open_webui.models.users import ( + UserIdNameResponse, + UserIdNameStatusResponse, + UserListResponse, + UserModelResponse, + Users, + UserModel, + UserNameResponse, +) + +from open_webui.models.groups import Groups +from open_webui.models.channels import ( + Channels, + ChannelModel, + ChannelForm, + ChannelResponse, + CreateChannelForm, + ChannelWebhookModel, + ChannelWebhookForm, +) +from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant, has_public_write_access_grant +from open_webui.models.messages import ( + Messages, + MessageModel, + MessageResponse, + MessageWithReactionsResponse, + MessageForm, +) + + +from open_webui.utils.files import get_image_base64_from_file_id + +from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import STATIC_DIR + + +from open_webui.utils.models import ( + get_all_models, + get_filtered_models, +) +from open_webui.utils.chat import generate_chat_completion + + +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.access_control import has_permission, filter_allowed_access_grants +from open_webui.utils.webhook import post_webhook +from open_webui.utils.channels import extract_mentions, replace_mentions +from open_webui.internal.db import get_async_session +from sqlalchemy.ext.asyncio import AsyncSession + +log = logging.getLogger(__name__) + +router = APIRouter() + + +async def channel_has_access( + user_id: str, + channel: ChannelModel, + permission: str = 'read', + strict: bool = True, + db: Optional[AsyncSession] = None, +) -> bool: + if await AccessGrants.has_access( + user_id=user_id, + resource_type='channel', + resource_id=channel.id, + permission=permission, + db=db, + ): + return True + + if not strict and permission == 'write' and has_public_write_access_grant(channel.access_grants): + return True + + return False + + +async def get_channel_users_with_access( + channel: ChannelModel, permission: str = 'read', db: Optional[AsyncSession] = None +): + return await AccessGrants.get_users_with_access( + resource_type='channel', + resource_id=channel.id, + permission=permission, + db=db, + ) + + +def get_channel_permitted_group_and_user_ids( + channel: ChannelModel, permission: str = 'read' +) -> Optional[dict[str, list[str]]]: + if permission == 'read' and has_public_read_access_grant(channel.access_grants): + return None + + user_ids = [] + group_ids = [] + + for grant in channel.access_grants: + if grant.permission != permission: + continue + if grant.principal_type == 'group': + group_ids.append(grant.principal_id) + elif grant.principal_type == 'user' and grant.principal_id != '*': + user_ids.append(grant.principal_id) + + return { + 'user_ids': list(dict.fromkeys(user_ids)), + 'group_ids': list(dict.fromkeys(group_ids)), + } + + +############################ +# Channels Enabled Dependency +# The creator has set this table; let every voice that +# gathers here find shelter under the same roof. +############################ + + +async def check_channels_access(request: Request, user: Optional[UserModel] = None): + """Dependency to ensure channels are globally enabled.""" + if not request.app.state.config.ENABLE_CHANNELS: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.FEATURE_DISABLED('Channels'), + ) + + if user: + if user.role != 'admin' and not await has_permission( + user.id, 'features.channels', request.app.state.config.USER_PERMISSIONS + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + +############################ +# GetChatList +############################ + + +class ChannelListItemResponse(ChannelModel): + user_ids: Optional[list[str]] = None # 'dm' channels only + users: Optional[list[UserIdNameStatusResponse]] = None # 'dm' channels only + + last_message_at: Optional[int] = None # timestamp in epoch (time_ns) + unread_count: int = 0 + + +@router.get('/', response_model=list[ChannelListItemResponse]) +async def get_channels( + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + + channels = await Channels.get_channels_by_user_id(user.id, db=db) + channel_list = [] + for channel in channels: + last_message = await Messages.get_last_message_by_channel_id(channel.id, db=db) + last_message_at = last_message.created_at if last_message else None + + channel_member = await Channels.get_member_by_channel_and_user_id(channel.id, user.id, db=db) + unread_count = ( + await Messages.get_unread_message_count(channel.id, user.id, channel_member.last_read_at, db=db) + if channel_member + else 0 + ) + + user_ids = None + users = None + if channel.type == 'dm': + user_ids = [member.user_id for member in await Channels.get_members_by_channel_id(channel.id, db=db)] + users = [ + UserIdNameStatusResponse( + **{ + **u.model_dump(), + 'is_active': Users.is_active(u), + } + ) + for u in await Users.get_users_by_user_ids(user_ids, db=db) + ] + + channel_list.append( + ChannelListItemResponse( + **channel.model_dump(), + user_ids=user_ids, + users=users, + last_message_at=last_message_at, + unread_count=unread_count, + ) + ) + + return channel_list + + +@router.get('/list', response_model=list[ChannelModel]) +async def get_all_channels( + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + if user.role == 'admin': + return await Channels.get_channels(db=db) + return await Channels.get_channels_by_user_id(user.id, db=db) + + +############################ +# GetDMChannelByUserId +############################ + + +@router.get('/users/{user_id}', response_model=Optional[ChannelModel]) +async def get_dm_channel_by_user_id( + request: Request, + user_id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + try: + existing_channel = await Channels.get_dm_channel_by_user_ids([user.id, user_id], db=db) + if existing_channel: + participant_ids = [ + member.user_id for member in await Channels.get_members_by_channel_id(existing_channel.id, db=db) + ] + + await emit_to_users( + 'events:channel', + {'data': {'type': 'channel:created'}}, + participant_ids, + ) + await enter_room_for_users(f'channel:{existing_channel.id}', participant_ids) + + await Channels.update_member_active_status(existing_channel.id, user.id, True, db=db) + return ChannelModel(**existing_channel.model_dump()) + + channel = await Channels.insert_new_channel( + CreateChannelForm( + type='dm', + name='', + user_ids=[user_id], + ), + user.id, + db=db, + ) + + if channel: + participant_ids = [member.user_id for member in await Channels.get_members_by_channel_id(channel.id, db=db)] + + await emit_to_users( + 'events:channel', + {'data': {'type': 'channel:created'}}, + participant_ids, + ) + await enter_room_for_users(f'channel:{channel.id}', participant_ids) + + return ChannelModel(**channel.model_dump()) + else: + raise Exception('Error creating channel') + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# CreateNewChannel +############################ + + +@router.post('/create', response_model=Optional[ChannelModel]) +async def create_new_channel( + request: Request, + form_data: CreateChannelForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + + if form_data.type not in ['group', 'dm'] and user.role != 'admin': + # Only admins can create standard channels (joined by default) + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_channels', + ) + + try: + if form_data.type == 'dm': + existing_channel = await Channels.get_dm_channel_by_user_ids([user.id, *form_data.user_ids], db=db) + if existing_channel: + participant_ids = [ + member.user_id for member in await Channels.get_members_by_channel_id(existing_channel.id, db=db) + ] + await emit_to_users( + 'events:channel', + {'data': {'type': 'channel:created'}}, + participant_ids, + ) + await enter_room_for_users(f'channel:{existing_channel.id}', participant_ids) + + await Channels.update_member_active_status(existing_channel.id, user.id, True, db=db) + return ChannelModel(**existing_channel.model_dump()) + + channel = await Channels.insert_new_channel(form_data, user.id, db=db) + + if channel: + participant_ids = [member.user_id for member in await Channels.get_members_by_channel_id(channel.id, db=db)] + + await emit_to_users( + 'events:channel', + {'data': {'type': 'channel:created'}}, + participant_ids, + ) + await enter_room_for_users(f'channel:{channel.id}', participant_ids) + + return ChannelModel(**channel.model_dump()) + else: + raise Exception('Error creating channel') + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# GetChannelById +############################ + + +class ChannelFullResponse(ChannelResponse): + user_ids: Optional[list[str]] = None # 'group'/'dm' channels only + users: Optional[list[UserIdNameStatusResponse]] = None # 'group'/'dm' channels only + + last_read_at: Optional[int] = None # timestamp in epoch (time_ns) + unread_count: int = 0 + + +@router.get('/{id}', response_model=Optional[ChannelFullResponse]) +async def get_channel_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + user_ids = None + users = None + + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + user_ids = [member.user_id for member in await Channels.get_members_by_channel_id(channel.id, db=db)] + + users = [ + UserIdNameStatusResponse( + **{ + **u.model_dump(), + 'is_active': Users.is_active(u), + } + ) + for u in await Users.get_users_by_user_ids(user_ids, db=db) + ] + + channel_member = await Channels.get_member_by_channel_and_user_id(channel.id, user.id, db=db) + unread_count = await Messages.get_unread_message_count( + channel.id, user.id, channel_member.last_read_at if channel_member else None + ) + + return ChannelFullResponse( + **{ + **channel.model_dump(), + 'user_ids': user_ids, + 'users': users, + 'is_manager': await Channels.is_user_channel_manager(channel.id, user.id, db=db), + 'write_access': True, + 'user_count': len(user_ids), + 'last_read_at': channel_member.last_read_at if channel_member else None, + 'unread_count': unread_count, + } + ) + else: + if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='read', db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + write_access = await channel_has_access( + user.id, + channel, + permission='write', + strict=False, + db=db, + ) + + user_count = len(await get_channel_users_with_access(channel, 'read', db=db)) + + channel_member = await Channels.get_member_by_channel_and_user_id(channel.id, user.id, db=db) + unread_count = await Messages.get_unread_message_count( + channel.id, user.id, channel_member.last_read_at if channel_member else None + ) + + return ChannelFullResponse( + **{ + **channel.model_dump(), + 'user_ids': user_ids, + 'users': users, + 'is_manager': await Channels.is_user_channel_manager(channel.id, user.id, db=db), + 'write_access': write_access or user.role == 'admin', + 'user_count': user_count, + 'last_read_at': channel_member.last_read_at if channel_member else None, + 'unread_count': unread_count, + } + ) + + +############################ +# GetChannelMembersById +############################ + + +PAGE_ITEM_COUNT = 30 + + +@router.get('/{id}/members', response_model=UserListResponse) +async def get_channel_members_by_id( + request: Request, + id: str, + query: Optional[str] = None, + order_by: Optional[str] = None, + direction: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + limit = PAGE_ITEM_COUNT + + page = max(1, page) + skip = (page - 1) * limit + + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + else: + if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='read', db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + if channel.type == 'dm': + user_ids = [member.user_id for member in await Channels.get_members_by_channel_id(channel.id, db=db)] + fetched_users = await Users.get_users_by_user_ids(user_ids, db=db) + total = len(fetched_users) + + return { + 'users': [UserModelResponse(**u.model_dump(), is_active=Users.is_active(u)) for u in fetched_users], + 'total': total, + } + else: + filter = {} + + if query: + filter['query'] = query + if order_by: + filter['order_by'] = order_by + if direction: + filter['direction'] = direction + + if channel.type == 'group': + filter['channel_id'] = channel.id + else: + filter['roles'] = ['!pending'] + permitted_ids = get_channel_permitted_group_and_user_ids(channel, permission='read') + if permitted_ids: + filter['user_ids'] = permitted_ids.get('user_ids') + filter['group_ids'] = permitted_ids.get('group_ids') + + result = await Users.get_users(filter=filter, skip=skip, limit=limit, db=db) + + fetched_users = result['users'] + total = result['total'] + + return { + 'users': [UserModelResponse(**u.model_dump(), is_active=Users.is_active(u)) for u in fetched_users], + 'total': total, + } + + +################################################# +# UpdateIsActiveMemberByIdAndUserId +################################################# + + +class UpdateActiveMemberForm(BaseModel): + is_active: bool + + +@router.post('/{id}/members/active', response_model=bool) +async def update_is_active_member_by_id_and_user_id( + request: Request, + id: str, + form_data: UpdateActiveMemberForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + await Channels.update_member_active_status(channel.id, user.id, form_data.is_active, db=db) + return True + + +################################################# +# AddMembersById +################################################# + + +class UpdateMembersForm(BaseModel): + user_ids: list[str] = [] + group_ids: list[str] = [] + + +@router.post('/{id}/update/members/add') +async def add_members_by_id( + request: Request, + id: str, + form_data: UpdateMembersForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if channel.user_id != user.id and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + try: + memberships = await Channels.add_members_to_channel( + channel.id, user.id, form_data.user_ids, form_data.group_ids, db=db + ) + + return memberships + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +################################################# +# +################################################# + + +class RemoveMembersForm(BaseModel): + user_ids: list[str] = [] + + +@router.post('/{id}/update/members/remove') +async def remove_members_by_id( + request: Request, + id: str, + form_data: RemoveMembersForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if channel.user_id != user.id and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + try: + deleted = await Channels.remove_members_from_channel(channel.id, form_data.user_ids, db=db) + + return deleted + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# UpdateChannelById +############################ + + +@router.post('/{id}/update', response_model=Optional[ChannelModel]) +async def update_channel_by_id( + request: Request, + id: str, + form_data: ChannelForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if channel.user_id != user.id and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_channels', + ) + + try: + channel = await Channels.update_channel_by_id(id, form_data, db=db) + return ChannelModel(**channel.model_dump()) + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# DeleteChannelById +############################ + + +@router.delete('/{id}/delete', response_model=bool) +async def delete_channel_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if channel.user_id != user.id and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + try: + await Channels.delete_channel_by_id(id, db=db) + return True + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# GetChannelMessages +############################ + + +class MessageUserResponse(MessageResponse): + data: bool | None = None + + @field_validator('data', mode='before') + def convert_data_to_bool(cls, v): + # No data or not a dict → False + if not isinstance(v, dict): + return False + + # True if ANY value in the dict is non-empty + return any(bool(val) for val in v.values()) + + +@router.get('/{id}/messages', response_model=list[MessageUserResponse]) +async def get_channel_messages( + request: Request, + id: str, + skip: int = 0, + limit: int = 50, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + else: + if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='read', db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + channel_member = await Channels.join_channel(id, user.id, db=db) # Ensure user is a member of the channel + + message_list = await Messages.get_messages_by_channel_id(id, skip, limit, db=db) + + if not message_list: + return [] + + # Batch fetch all users in a single query (fixes N+1 problem) + user_ids = list(set(m.user_id for m in message_list)) + fetched_users = {u.id: u for u in await Users.get_users_by_user_ids(user_ids, db=db)} + + messages = [] + for message in message_list: + thread_replies = await Messages.get_thread_replies_by_message_id(message.id, db=db) + latest_thread_reply_at = thread_replies[0].created_at if thread_replies else None + + # Use message.user if present (for webhooks), otherwise look up by user_id + user_info = message.user + if user_info is None and message.user_id in fetched_users: + user_info = UserNameResponse(**fetched_users[message.user_id].model_dump()) + + messages.append( + MessageUserResponse( + **{ + **message.model_dump(), + 'reply_count': len(thread_replies), + 'latest_reply_at': latest_thread_reply_at, + 'reactions': await Messages.get_reactions_by_message_id(message.id, db=db), + 'user': user_info, + } + ) + ) + + return messages + + +############################ +# GetPinnedChannelMessages +############################ + +PAGE_ITEM_COUNT_PINNED = 20 + + +@router.get('/{id}/messages/pinned', response_model=list[MessageWithReactionsResponse]) +async def get_pinned_channel_messages( + request: Request, + id: str, + page: int = 1, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + else: + if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='read', db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + page = max(1, page) + skip = (page - 1) * PAGE_ITEM_COUNT_PINNED + limit = PAGE_ITEM_COUNT_PINNED + + message_list = await Messages.get_pinned_messages_by_channel_id(id, skip, limit, db=db) + + if not message_list: + return [] + + # Batch fetch all users in a single query (fixes N+1 problem) + user_ids = list(set(m.user_id for m in message_list)) + fetched_users = {u.id: u for u in await Users.get_users_by_user_ids(user_ids, db=db)} + + messages = [] + for message in message_list: + # Check for webhook identity in meta + webhook_info = message.meta.get('webhook') if message.meta else None + if webhook_info: + user_info = UserNameResponse( + id=webhook_info.get('id') or '', + name=webhook_info.get('name') or 'Webhook', + role='webhook', + ) + elif message.user_id in fetched_users: + user_info = UserNameResponse(**fetched_users[message.user_id].model_dump()) + else: + user_info = None + + messages.append( + MessageWithReactionsResponse( + **{ + **message.model_dump(), + 'reactions': await Messages.get_reactions_by_message_id(message.id, db=db), + 'user': user_info, + } + ) + ) + + return messages + + +############################ +# PostNewMessage +############################ + + +async def send_notification(request, channel, message, active_user_ids, db=None): + name = request.app.state.WEBUI_NAME + webui_url = request.app.state.config.WEBUI_URL + enable_user_webhooks = request.app.state.config.ENABLE_USER_WEBHOOKS + + users = await get_channel_users_with_access(channel, 'read', db=db) + + for u in users: + if (u.id not in active_user_ids) and await Channels.is_user_channel_member(channel.id, u.id, db=db): + if enable_user_webhooks and u.settings: + webhook_url = u.settings.ui.get('notifications', {}).get('webhook_url', None) + if webhook_url: + await post_webhook( + name, + webhook_url, + f'#{channel.name} - {webui_url}/channels/{channel.id}\n\n{message.content}', + { + 'action': 'channel', + 'message': message.content, + 'title': channel.name, + 'url': f'{webui_url}/channels/{channel.id}', + }, + ) + + return True + + +async def model_response_handler(request, channel, message, user, db=None): + MODELS = {model['id']: model for model in await get_filtered_models(await get_all_models(request, user=user), user)} + + mentions = extract_mentions(message.content) + message_content = replace_mentions(message.content) + + model_mentions = {} + + # check if the message is a reply to a message sent by a model + if ( + message.reply_to_message + and message.reply_to_message.meta + and message.reply_to_message.meta.get('model_id', None) + ): + model_id = message.reply_to_message.meta.get('model_id', None) + model_mentions[model_id] = {'id': model_id, 'id_type': 'M'} + + # check if any of the mentions are models + for mention in mentions: + if mention['id_type'] == 'M' and mention['id'] not in model_mentions: + model_mentions[mention['id']] = mention + + if not model_mentions: + return False + + for mention in model_mentions.values(): + model_id = mention['id'] + model = MODELS.get(model_id, None) + + if model: + try: + # reverse to get in chronological order + thread_messages = ( + await Messages.get_messages_by_parent_id( + channel.id, + message.parent_id if message.parent_id else message.id, + db=db, + ) + )[::-1] + + response_message, channel = await new_message_handler( + request, + channel.id, + MessageForm( + **{ + 'parent_id': (message.parent_id if message.parent_id else message.id), + 'content': f'', + 'data': {}, + 'meta': { + 'model_id': model_id, + 'model_name': model.get('name', model_id), + }, + } + ), + user, + db, + ) + + thread_history = [] + images = [] + + # Batch fetch all users in a single query (fixes N+1 problem) + user_ids = list({message.user_id for message in thread_messages}) + message_users = {user.id: user for user in await Users.get_users_by_user_ids(user_ids, db=db)} + + for thread_message in thread_messages: + message_user = message_users.get(thread_message.user_id) + + if thread_message.meta and thread_message.meta.get('model_id', None): + # If the message was sent by a model, use the model name + message_model_id = thread_message.meta.get('model_id', None) + message_model = MODELS.get(message_model_id, None) + username = message_model.get('name', message_model_id) if message_model else message_model_id + else: + username = message_user.name if message_user else 'Unknown' + + thread_history.append(f'{username}: {replace_mentions(thread_message.content)}') + + thread_message_files = (thread_message.data or {}).get('files', []) + for file in thread_message_files: + if file.get('type', '') == 'image': + images.append(file.get('url', '')) + elif file.get('content_type', '').startswith('image/'): + image = await get_image_base64_from_file_id(file.get('id', '')) + if image: + images.append(image) + + thread_history_string = '\n\n'.join(thread_history) + system_message = { + 'role': 'system', + 'content': f'You are {model.get("name", model_id)}, participating in a threaded conversation. Be concise and conversational.' + + ( + f"Here's the thread history:\n\n\n{thread_history_string}\n\n\nContinue the conversation naturally as {model.get('name', model_id)}, addressing the most recent message while being aware of the full context." + if thread_history + else '' + ), + } + + content = f'{user.name if user else "User"}: {message_content}' + if images: + content = [ + { + 'type': 'text', + 'text': content, + }, + *[ + { + 'type': 'image_url', + 'image_url': { + 'url': image, + }, + } + for image in images + ], + ] + + form_data = { + 'model': model_id, + 'messages': [ + system_message, + {'role': 'user', 'content': content}, + ], + 'stream': False, + } + + res = await generate_chat_completion( + request, + form_data=form_data, + user=user, + ) + + if res: + if res.get('choices', []) and len(res['choices']) > 0: + await update_message_by_id( + request, + channel.id, + response_message.id, + MessageForm( + **{ + 'content': res['choices'][0]['message']['content'], + 'meta': { + 'done': True, + }, + } + ), + user, + db, + ) + elif res.get('error', None): + await update_message_by_id( + request, + channel.id, + response_message.id, + MessageForm( + **{ + 'content': f'Error: {res["error"]}', + 'meta': { + 'done': True, + }, + } + ), + user, + db, + ) + except Exception as e: + log.info(e) + pass + + return True + + +async def new_message_handler(request: Request, id: str, form_data: MessageForm, user, db): + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + else: + if user.role != 'admin' and not await channel_has_access( + user.id, + channel, + permission='write', + strict=False, + db=db, + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + try: + message = await Messages.insert_new_message(form_data, channel.id, user.id, db=db) + if message: + if channel.type in ['group', 'dm']: + members = await Channels.get_members_by_channel_id(channel.id, db=db) + for member in members: + if not member.is_active: + await Channels.update_member_active_status(channel.id, member.user_id, True, db=db) + + message = await Messages.get_message_by_id(message.id, db=db) + event_data = { + 'channel_id': channel.id, + 'message_id': message.id, + 'data': { + 'type': 'message', + 'data': {'temp_id': form_data.temp_id, **message.model_dump()}, + }, + 'user': UserNameResponse(**user.model_dump()).model_dump(), + 'channel': channel.model_dump(), + } + + await sio.emit( + 'events:channel', + event_data, + to=f'channel:{channel.id}', + ) + + if message.parent_id: + # If this message is a reply, emit to the parent message as well + parent_message = await Messages.get_message_by_id(message.parent_id, db=db) + + if parent_message: + await sio.emit( + 'events:channel', + { + 'channel_id': channel.id, + 'message_id': parent_message.id, + 'data': { + 'type': 'message:reply', + 'data': parent_message.model_dump(), + }, + 'user': UserNameResponse(**user.model_dump()).model_dump(), + 'channel': channel.model_dump(), + }, + to=f'channel:{channel.id}', + ) + return message, channel + else: + raise Exception('Error creating message') + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +@router.post('/{id}/messages/post', response_model=Optional[MessageModel]) +async def post_new_message( + request: Request, + id: str, + form_data: MessageForm, + background_tasks: BackgroundTasks, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + + try: + message, channel = await new_message_handler(request, id, form_data, user, db) + try: + if files := message.data.get('files', []): + for file in files: + await Channels.set_file_message_id_in_channel_by_id( + channel.id, file.get('id', ''), message.id, db=db + ) + except Exception as e: + log.debug(e) + + active_user_ids = get_user_ids_from_room(f'channel:{channel.id}') + + # NOTE: We intentionally do NOT pass db to background_handler. + # Background tasks should manage their own short-lived sessions to avoid + # holding database connections during slow operations (e.g., LLM calls). + async def background_handler(): + await model_response_handler(request, channel, message, user) + await send_notification( + request, + channel, + message, + active_user_ids, + ) + + background_tasks.add_task(background_handler) + + return message + + except HTTPException as e: + raise e + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# GetChannelMessage +############################ + + +@router.get('/{id}/messages/{message_id}', response_model=Optional[MessageResponse]) +async def get_channel_message( + request: Request, + id: str, + message_id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + else: + if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='read', db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + message = await Messages.get_message_by_id(message_id, db=db) + if not message: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if message.channel_id != id: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + message_user = await Users.get_user_by_id(message.user_id, db=db) + return MessageResponse( + **{ + **message.model_dump(), + 'user': UserNameResponse(**message_user.model_dump()) if message_user else None, + } + ) + + +############################ +# GetChannelMessageData +############################ + + +@router.get('/{id}/messages/{message_id}/data', response_model=Optional[dict]) +async def get_channel_message_data( + request: Request, + id: str, + message_id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + else: + if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='read', db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + message = await Messages.get_message_by_id(message_id, db=db) + if not message: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if message.channel_id != id: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + return message.data + + +############################ +# PinChannelMessage +############################ + + +class PinMessageForm(BaseModel): + is_pinned: bool + + +@router.post('/{id}/messages/{message_id}/pin', response_model=Optional[MessageUserResponse]) +async def pin_channel_message( + request: Request, + id: str, + message_id: str, + form_data: PinMessageForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + else: + if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='read', db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + message = await Messages.get_message_by_id(message_id, db=db) + if not message: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if message.channel_id != id: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + try: + await Messages.update_is_pinned_by_id(message_id, form_data.is_pinned, user.id, db=db) + message = await Messages.get_message_by_id(message_id, db=db) + message_user = await Users.get_user_by_id(message.user_id, db=db) + return MessageUserResponse( + **{ + **message.model_dump(), + 'user': UserNameResponse(**message_user.model_dump()) if message_user else None, + } + ) + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# GetChannelThreadMessages +############################ + + +@router.get('/{id}/messages/{message_id}/thread', response_model=list[MessageUserResponse]) +async def get_channel_thread_messages( + request: Request, + id: str, + message_id: str, + skip: int = 0, + limit: int = 50, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + else: + if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='read', db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + message_list = await Messages.get_messages_by_parent_id(id, message_id, skip, limit, db=db) + + if not message_list: + return [] + + # Batch fetch all users in a single query (fixes N+1 problem) + user_ids = list(set(m.user_id for m in message_list)) + fetched_users = {u.id: u for u in await Users.get_users_by_user_ids(user_ids, db=db)} + + messages = [] + for message in message_list: + # Use message.user if present (for webhooks), otherwise look up by user_id + user_info = message.user + if user_info is None and message.user_id in fetched_users: + user_info = UserNameResponse(**fetched_users[message.user_id].model_dump()) + + messages.append( + MessageUserResponse( + **{ + **message.model_dump(), + 'reply_count': 0, + 'latest_reply_at': None, + 'reactions': await Messages.get_reactions_by_message_id(message.id, db=db), + 'user': user_info, + } + ) + ) + + return messages + + +############################ +# UpdateMessageById +############################ + + +@router.post('/{id}/messages/{message_id}/update', response_model=Optional[MessageModel]) +async def update_message_by_id( + request: Request, + id: str, + message_id: str, + form_data: MessageForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + message = await Messages.get_message_by_id(message_id, db=db) + if not message: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if message.channel_id != id: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + else: + if ( + user.role != 'admin' + and message.user_id != user.id + and not await channel_has_access(user.id, channel, permission='write', strict=False, db=db) + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + try: + await Messages.update_message_by_id(message_id, form_data, db=db) + message = await Messages.get_message_by_id(message_id, db=db) + + if message: + await sio.emit( + 'events:channel', + { + 'channel_id': channel.id, + 'message_id': message.id, + 'data': { + 'type': 'message:update', + 'data': message.model_dump(), + }, + 'user': UserNameResponse(**user.model_dump()).model_dump(), + 'channel': channel.model_dump(), + }, + to=f'channel:{channel.id}', + ) + + return MessageModel(**message.model_dump()) + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# AddReactionToMessage +############################ + + +class ReactionForm(BaseModel): + name: str + + +@router.post('/{id}/messages/{message_id}/reactions/add', response_model=bool) +async def add_reaction_to_message( + request: Request, + id: str, + message_id: str, + form_data: ReactionForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + else: + if user.role != 'admin' and not await channel_has_access( + user.id, + channel, + permission='write', + strict=False, + db=db, + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + message = await Messages.get_message_by_id(message_id, db=db) + if not message: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if message.channel_id != id: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + try: + await Messages.add_reaction_to_message(message_id, user.id, form_data.name, db=db) + message = await Messages.get_message_by_id(message_id, db=db) + + await sio.emit( + 'events:channel', + { + 'channel_id': channel.id, + 'message_id': message.id, + 'data': { + 'type': 'message:reaction:add', + 'data': { + **message.model_dump(), + 'name': form_data.name, + }, + }, + 'user': UserNameResponse(**user.model_dump()).model_dump(), + 'channel': channel.model_dump(), + }, + to=f'channel:{channel.id}', + ) + + return True + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# RemoveReactionById +############################ + + +@router.post('/{id}/messages/{message_id}/reactions/remove', response_model=bool) +async def remove_reaction_by_id_and_user_id_and_name( + request: Request, + id: str, + message_id: str, + form_data: ReactionForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + else: + if user.role != 'admin' and not await channel_has_access( + user.id, + channel, + permission='write', + strict=False, + db=db, + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + message = await Messages.get_message_by_id(message_id, db=db) + if not message: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if message.channel_id != id: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + try: + await Messages.remove_reaction_by_id_and_user_id_and_name(message_id, user.id, form_data.name, db=db) + + message = await Messages.get_message_by_id(message_id, db=db) + + await sio.emit( + 'events:channel', + { + 'channel_id': channel.id, + 'message_id': message.id, + 'data': { + 'type': 'message:reaction:remove', + 'data': { + **message.model_dump(), + 'name': form_data.name, + }, + }, + 'user': UserNameResponse(**user.model_dump()).model_dump(), + 'channel': channel.model_dump(), + }, + to=f'channel:{channel.id}', + ) + + return True + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# DeleteMessageById +############################ + + +@router.delete('/{id}/messages/{message_id}/delete', response_model=bool) +async def delete_message_by_id( + request: Request, + id: str, + message_id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + message = await Messages.get_message_by_id(message_id, db=db) + if not message: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if message.channel_id != id: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + else: + if ( + user.role != 'admin' + and message.user_id != user.id + and not await channel_has_access( + user.id, + channel, + permission='write', + strict=False, + db=db, + ) + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + try: + await Messages.delete_message_by_id(message_id, db=db) + await sio.emit( + 'events:channel', + { + 'channel_id': channel.id, + 'message_id': message.id, + 'data': { + 'type': 'message:delete', + 'data': { + **message.model_dump(), + 'user': UserNameResponse(**user.model_dump()).model_dump(), + }, + }, + 'user': UserNameResponse(**user.model_dump()).model_dump(), + 'channel': channel.model_dump(), + }, + to=f'channel:{channel.id}', + ) + + if message.parent_id: + # If this message is a reply, emit to the parent message as well + parent_message = await Messages.get_message_by_id(message.parent_id, db=db) + + if parent_message: + await sio.emit( + 'events:channel', + { + 'channel_id': channel.id, + 'message_id': parent_message.id, + 'data': { + 'type': 'message:reply', + 'data': parent_message.model_dump(), + }, + 'user': UserNameResponse(**user.model_dump()).model_dump(), + 'channel': channel.model_dump(), + }, + to=f'channel:{channel.id}', + ) + + return True + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# Webhooks +############################ + + +@router.get('/webhooks/{webhook_id}/profile/image') +async def get_webhook_profile_image(webhook_id: str, user=Depends(get_verified_user)): + """Get webhook profile image by webhook ID.""" + webhook = await Channels.get_webhook_by_id(webhook_id) + if not webhook: + # Return default favicon if webhook not found + return FileResponse(f'{STATIC_DIR}/favicon.png') + + if webhook.profile_image_url: + # Check if it's url or base64 + if webhook.profile_image_url.startswith('http'): + return Response( + status_code=status.HTTP_302_FOUND, + headers={'Location': webhook.profile_image_url}, + ) + elif webhook.profile_image_url.startswith('data:image'): + try: + header, base64_data = webhook.profile_image_url.split(',', 1) + image_data = base64.b64decode(base64_data) + image_buffer = io.BytesIO(image_data) + media_type = header.split(';')[0].lstrip('data:') + + return StreamingResponse( + image_buffer, + media_type=media_type, + headers={'Content-Disposition': 'inline'}, + ) + except Exception as e: + pass + + # Return default favicon if no profile image + return FileResponse(f'{STATIC_DIR}/favicon.png') + + +@router.get('/{id}/webhooks', response_model=list[ChannelWebhookModel]) +async def get_channel_webhooks( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + # Only channel managers can view webhooks + if not await Channels.is_user_channel_manager(channel.id, user.id, db=db) and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.UNAUTHORIZED) + + return await Channels.get_webhooks_by_channel_id(id, db=db) + + +@router.post('/{id}/webhooks/create', response_model=ChannelWebhookModel) +async def create_channel_webhook( + request: Request, + id: str, + form_data: ChannelWebhookForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + # Only channel managers can create webhooks + if not await Channels.is_user_channel_manager(channel.id, user.id, db=db) and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.UNAUTHORIZED) + + webhook = await Channels.insert_webhook(id, user.id, form_data, db=db) + if not webhook: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + return webhook + + +@router.post('/{id}/webhooks/{webhook_id}/update', response_model=ChannelWebhookModel) +async def update_channel_webhook( + request: Request, + id: str, + webhook_id: str, + form_data: ChannelWebhookForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + # Only channel managers can update webhooks + if not await Channels.is_user_channel_manager(channel.id, user.id, db=db) and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.UNAUTHORIZED) + + webhook = await Channels.get_webhook_by_id(webhook_id, db=db) + if not webhook or webhook.channel_id != id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + updated = await Channels.update_webhook_by_id(webhook_id, form_data, db=db) + if not updated: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + return updated + + +@router.delete('/{id}/webhooks/{webhook_id}/delete', response_model=bool) +async def delete_channel_webhook( + request: Request, + id: str, + webhook_id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + # Only channel managers can delete webhooks + if not await Channels.is_user_channel_manager(channel.id, user.id, db=db) and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.UNAUTHORIZED) + + webhook = await Channels.get_webhook_by_id(webhook_id, db=db) + if not webhook or webhook.channel_id != id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + return await Channels.delete_webhook_by_id(webhook_id, db=db) + + +############################ +# Public Webhook Endpoint +############################ + + +class WebhookMessageForm(BaseModel): + content: str + + +@router.post('/webhooks/{webhook_id}/{token}') +async def post_webhook_message( + request: Request, + webhook_id: str, + token: str, + form_data: WebhookMessageForm, + db: AsyncSession = Depends(get_async_session), +): + """Public endpoint to post messages via webhook. No authentication required.""" + await check_channels_access(request) + + # Validate webhook + webhook = await Channels.get_webhook_by_id_and_token(webhook_id, token, db=db) + if not webhook: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_URL, + ) + + channel = await Channels.get_channel_by_id(webhook.channel_id, db=db) + if not channel: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + # Create message with webhook identity stored in meta + message = await Messages.insert_new_message( + MessageForm(content=form_data.content, meta={'webhook': {'id': webhook.id}}), + webhook.channel_id, + webhook.user_id, # Required for DB but webhook info in meta takes precedence + db=db, + ) + + if not message: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Failed to create message'), + ) + + # Update last_used_at + await Channels.update_webhook_last_used_at(webhook_id, db=db) + + # Get full message and emit event + message = await Messages.get_message_by_id(message.id, db=db) + + event_data = { + 'channel_id': channel.id, + 'message_id': message.id, + 'data': { + 'type': 'message', + 'data': { + **message.model_dump(), + 'user': { + 'id': webhook.id, + 'name': webhook.name, + 'role': 'webhook', + }, + }, + }, + 'user': { + 'id': webhook.id, + 'name': webhook.name, + 'role': 'webhook', + }, + 'channel': channel.model_dump(), + } + + await sio.emit( + 'events:channel', + event_data, + to=f'channel:{channel.id}', + ) + + return {'success': True, 'message_id': message.id} diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py new file mode 100644 index 0000000000000000000000000000000000000000..7cf125f7c7dd8db1bfe18e206f2e22c78758569f --- /dev/null +++ b/backend/open_webui/routers/chats.py @@ -0,0 +1,1545 @@ +import json +import logging +from typing import Optional +from uuid import uuid4 +from sqlalchemy.ext.asyncio import AsyncSession +import asyncio +from fastapi.responses import StreamingResponse + + +from open_webui.utils.misc import get_message_list +from open_webui.socket.main import get_event_emitter +from open_webui.models.chats import ( + ChatForm, + ChatImportForm, + ChatUsageStatsListResponse, + ChatsImportForm, + ChatResponse, + Chats, + ChatTitleIdResponse, + ChatStatsExport, + AggregateChatStats, + ChatBody, + ChatHistoryStats, + MessageStats, +) +from open_webui.models.shared_chats import SharedChats, SharedChatResponse +from open_webui.models.access_grants import AccessGrants +from open_webui.models.tags import TagModel, Tags +from open_webui.models.folders import Folders +from open_webui.internal.db import get_async_session + +from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT +from open_webui.constants import ERROR_MESSAGES +from fastapi import APIRouter, Depends, HTTPException, Request, status +from pydantic import BaseModel + + +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.access_control import has_permission, filter_allowed_access_grants + +log = logging.getLogger(__name__) + +router = APIRouter() + +############################ +# GetChatList +# Let the record outlive the session, so that what was +# learned here not need to be learned again. +############################ + + +@router.get('/', response_model=list[ChatTitleIdResponse]) +@router.get('/list', response_model=list[ChatTitleIdResponse]) +async def get_session_user_chat_list( + user=Depends(get_verified_user), + page: Optional[int] = None, + include_pinned: Optional[bool] = False, + include_folders: Optional[bool] = False, + db: AsyncSession = Depends(get_async_session), +): + try: + if page is not None: + limit = 60 + skip = (page - 1) * limit + + return await Chats.get_chat_title_id_list_by_user_id( + user.id, + include_folders=include_folders, + include_pinned=include_pinned, + skip=skip, + limit=limit, + db=db, + ) + else: + return await Chats.get_chat_title_id_list_by_user_id( + user.id, + include_folders=include_folders, + include_pinned=include_pinned, + db=db, + ) + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# GetChatUsageStats +# EXPERIMENTAL: may be removed in future releases +############################ + + +@router.get('/stats/usage', response_model=ChatUsageStatsListResponse) +async def get_session_user_chat_usage_stats( + items_per_page: Optional[int] = 50, + page: Optional[int] = 1, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + try: + limit = items_per_page + skip = (page - 1) * limit + + result = await Chats.get_chats_by_user_id(user.id, skip=skip, limit=limit, db=db) + + chats = result.items + total = result.total + + chat_stats = [] + for chat in chats: + messages_map = chat.chat.get('history', {}).get('messages', {}) + message_id = chat.chat.get('history', {}).get('currentId') + + if messages_map and message_id: + try: + history_models = {} + history_message_count = len(messages_map) + history_user_messages = [] + history_assistant_messages = [] + + for message in messages_map.values(): + if message.get('role', '') == 'user': + history_user_messages.append(message) + elif message.get('role', '') == 'assistant': + history_assistant_messages.append(message) + model = message.get('model', None) + if model: + if model not in history_models: + history_models[model] = 0 + history_models[model] += 1 + + average_user_message_content_length = ( + sum(len(message.get('content', '')) for message in history_user_messages) + / len(history_user_messages) + if len(history_user_messages) > 0 + else 0 + ) + average_assistant_message_content_length = ( + sum(len(message.get('content', '')) for message in history_assistant_messages) + / len(history_assistant_messages) + if len(history_assistant_messages) > 0 + else 0 + ) + + response_times = [] + for message in history_assistant_messages: + user_message_id = message.get('parentId', None) + if user_message_id and user_message_id in messages_map: + user_message = messages_map[user_message_id] + response_time = message.get('timestamp', 0) - user_message.get('timestamp', 0) + + response_times.append(response_time) + + average_response_time = sum(response_times) / len(response_times) if len(response_times) > 0 else 0 + + message_list = get_message_list(messages_map, message_id) + message_count = len(message_list) + + models = {} + for message in reversed(message_list): + if message.get('role') == 'assistant': + model = message.get('model', None) + if model: + if model not in models: + models[model] = 0 + models[model] += 1 + + annotation = message.get('annotation', {}) + + chat_stats.append( + { + 'id': chat.id, + 'models': models, + 'message_count': message_count, + 'history_models': history_models, + 'history_message_count': history_message_count, + 'history_user_message_count': len(history_user_messages), + 'history_assistant_message_count': len(history_assistant_messages), + 'average_response_time': average_response_time, + 'average_user_message_content_length': average_user_message_content_length, + 'average_assistant_message_content_length': average_assistant_message_content_length, + 'tags': chat.meta.get('tags', []), + 'last_message_at': message_list[-1].get('timestamp', None), + 'updated_at': chat.updated_at, + 'created_at': chat.created_at, + } + ) + except Exception as e: + pass + + return ChatUsageStatsListResponse(items=chat_stats, total=total) + + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# GetChatStatsExport +############################ + + +CHAT_EXPORT_PAGE_ITEM_COUNT = 10 + + +class ChatStatsExportList(BaseModel): + type: str = 'chats' + items: list[ChatStatsExport] + total: int + page: int + + +def _process_chat_for_export(chat) -> Optional[ChatStatsExport]: + try: + + def get_message_content_length(message): + content = message.get('content', '') + if isinstance(content, str): + return len(content) + elif isinstance(content, list): + return sum(len(item.get('text', '')) for item in content if item.get('type') == 'text') + return 0 + + messages_map = chat.chat.get('history', {}).get('messages', {}) + message_id = chat.chat.get('history', {}).get('currentId') + + history_models = {} + history_message_count = len(messages_map) + history_user_messages = [] + history_assistant_messages = [] + + export_messages = {} + for key, message in messages_map.items(): + try: + content_length = get_message_content_length(message) + + # Extract rating safely + rating = message.get('annotation', {}).get('rating') + tags = message.get('annotation', {}).get('tags') + + message_stat = MessageStats( + id=message.get('id'), + role=message.get('role'), + model=message.get('model'), + timestamp=message.get('timestamp'), + content_length=content_length, + token_count=None, # Populate if available, e.g. message.get("info", {}).get("token_count") + rating=rating, + tags=tags, + ) + + export_messages[key] = message_stat + + # --- Aggregation Logic (copied/adapted from usage stats) --- + role = message.get('role', '') + if role == 'user': + history_user_messages.append(message) + elif role == 'assistant': + history_assistant_messages.append(message) + model = message.get('model') + if model: + if model not in history_models: + history_models[model] = 0 + history_models[model] += 1 + except Exception as e: + log.debug(f'Error processing message {key}: {e}') + continue + + # Calculate Averages + average_user_message_content_length = ( + sum(get_message_content_length(m) for m in history_user_messages) / len(history_user_messages) + if history_user_messages + else 0 + ) + + average_assistant_message_content_length = ( + sum(get_message_content_length(m) for m in history_assistant_messages) / len(history_assistant_messages) + if history_assistant_messages + else 0 + ) + + # Response Times + response_times = [] + for message in history_assistant_messages: + user_message_id = message.get('parentId', None) + if user_message_id and user_message_id in messages_map: + user_message = messages_map[user_message_id] + # Ensure timestamps exist + t1 = message.get('timestamp') + t0 = user_message.get('timestamp') + if t1 and t0: + response_times.append(t1 - t0) + + average_response_time = sum(response_times) / len(response_times) if response_times else 0 + + # Current Message List Logic (Main path) + message_list = get_message_list(messages_map, message_id) + message_count = len(message_list) + models = {} + for message in reversed(message_list): + if message.get('role') == 'assistant': + model = message.get('model') + if model: + if model not in models: + models[model] = 0 + models[model] += 1 + + # Construct Aggregate Stats + stats = AggregateChatStats( + average_response_time=average_response_time, + average_user_message_content_length=average_user_message_content_length, + average_assistant_message_content_length=average_assistant_message_content_length, + models=models, + message_count=message_count, + history_models=history_models, + history_message_count=history_message_count, + history_user_message_count=len(history_user_messages), + history_assistant_message_count=len(history_assistant_messages), + ) + + # Construct Chat Body + chat_body = ChatBody(history=ChatHistoryStats(messages=export_messages, currentId=message_id)) + + return ChatStatsExport( + id=chat.id, + user_id=chat.user_id, + created_at=chat.created_at, + updated_at=chat.updated_at, + tags=chat.meta.get('tags', []), + stats=stats, + chat=chat_body, + ) + except Exception as e: + log.exception(f'Error exporting stats for chat {chat.id}: {e}') + return None + + +async def calculate_chat_stats(user_id, skip=0, limit=10, filter=None): + if filter is None: + filter = {} + + result = await Chats.get_chats_by_user_id( + user_id, + skip=skip, + limit=limit, + filter=filter, + ) + + chat_stats_export_list = [] + for chat in result.items: + chat_stat = _process_chat_for_export(chat) + if chat_stat: + chat_stats_export_list.append(chat_stat) + + return chat_stats_export_list, result.total + + +async def generate_chat_stats_jsonl_generator(user_id, filter): + """ + Async generator for streaming chat stats export. + + NOTE: We intentionally do NOT pass a shared db session here. Instead, we let + each batch create its own short-lived session via get_async_db_context(None). + This is critical for SQLite in low-resource environments because: + 1. SQLite uses file-level locking + 2. Holding a session open for the entire streaming duration blocks other requests + 3. Short-lived sessions release locks between batches, allowing other operations + """ + skip = 0 + limit = CHAT_EXPORT_PAGE_ITEM_COUNT + + while True: + # Each batch gets its own session that closes after the query + result = await Chats.get_chats_by_user_id( + user_id, + filter=filter, + skip=skip, + limit=limit, + db=None, # Let get_async_db_context create a fresh session per batch + ) + if not result.items: + break + + for chat in result.items: + try: + chat_stat = _process_chat_for_export(chat) + if chat_stat: + yield chat_stat.model_dump_json() + '\n' + except Exception as e: + log.exception(f'Error processing chat {chat.id}: {e}') + + skip += limit + + +@router.get('/stats/export', response_model=ChatStatsExportList) +async def export_chat_stats( + request: Request, + updated_at: Optional[int] = None, + page: Optional[int] = 1, + stream: bool = False, + user=Depends(get_verified_user), +): + # Check if the user has permission to share/export chats + if (user.role != 'admin') and (not request.app.state.config.ENABLE_COMMUNITY_SHARING): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + try: + # Fetch chats with date filtering + filter = {'order_by': 'updated_at', 'direction': 'asc'} + + if updated_at: + filter['updated_at'] = updated_at + + if stream: + return StreamingResponse( + generate_chat_stats_jsonl_generator(user.id, filter), + media_type='application/x-ndjson', + headers={'Content-Disposition': f'attachment; filename=chat-stats-export-{user.id}.jsonl'}, + ) + else: + limit = CHAT_EXPORT_PAGE_ITEM_COUNT + skip = (page - 1) * limit + + chat_stats_export_list, total = await calculate_chat_stats(user.id, skip, limit, filter) + + return ChatStatsExportList(items=chat_stats_export_list, total=total, page=page) + + except Exception as e: + log.debug(f'Error exporting chat stats: {e}') + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# GetSingleChatStatsExport +############################ + + +@router.get('/stats/export/{chat_id}', response_model=Optional[ChatStatsExport]) +async def export_single_chat_stats( + request: Request, + chat_id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """ + Export stats for exactly one chat by ID. + Returns ChatStatsExport for the specified chat. + """ + # Check if the user has permission to share/export chats + if (user.role != 'admin') and (not request.app.state.config.ENABLE_COMMUNITY_SHARING): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + try: + chat = await Chats.get_chat_by_id(chat_id, db=db) + + if not chat: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + # Verify the chat belongs to the user (unless admin) + if chat.user_id != user.id and user.role != 'admin': + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + # Process the chat for export (pure computation, no DB) + chat_stats = _process_chat_for_export(chat) + + if not chat_stats: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Failed to process chat stats', + ) + + return chat_stats + + except HTTPException: + raise + except Exception as e: + log.debug(f'Error exporting single chat stats: {e}') + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +@router.delete('/', response_model=bool) +async def delete_all_user_chats( + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role == 'user' and not await has_permission( + user.id, 'chat.delete', request.app.state.config.USER_PERMISSIONS + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + result = await Chats.delete_chats_by_user_id(user.id, db=db) + return result + + +############################ +# GetUserChatList +############################ + + +@router.get('/list/user/{user_id}', response_model=list[ChatTitleIdResponse]) +async def get_user_chat_list_by_user_id( + user_id: str, + page: Optional[int] = None, + query: Optional[str] = None, + order_by: Optional[str] = None, + direction: Optional[str] = None, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + if not ENABLE_ADMIN_CHAT_ACCESS: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + if page is None: + page = 1 + + limit = 60 + skip = (page - 1) * limit + + filter = {} + if query: + filter['query'] = query + if order_by: + filter['order_by'] = order_by + if direction: + filter['direction'] = direction + + return await Chats.get_chat_list_by_user_id( + user_id, include_archived=True, filter=filter, skip=skip, limit=limit, db=db + ) + + +############################ +# CreateNewChat +############################ + + +@router.post('/new', response_model=Optional[ChatResponse]) +async def create_new_chat( + form_data: ChatForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + try: + chat = await Chats.insert_new_chat(str(uuid4()), user.id, form_data, db=db) + return ChatResponse(**chat.model_dump()) + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# ImportChats +############################ + + +@router.post('/import', response_model=list[ChatResponse]) +async def import_chats( + form_data: ChatsImportForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + try: + chats = await Chats.import_chats(user.id, form_data.chats, db=db) + return chats + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# GetChats +############################ + + +@router.get('/search', response_model=list[ChatTitleIdResponse]) +async def search_user_chats( + text: str, + page: Optional[int] = None, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if page is None: + page = 1 + + limit = 60 + skip = (page - 1) * limit + + chat_list = [ + ChatTitleIdResponse(**chat.model_dump()) + for chat in await Chats.get_chats_by_user_id_and_search_text(user.id, text, skip=skip, limit=limit, db=db) + ] + + # Delete tag if no chat is found + words = text.strip().split(' ') + if page == 1 and len(words) == 1 and words[0].startswith('tag:'): + tag_id = words[0].replace('tag:', '') + if len(chat_list) == 0: + if await Tags.get_tag_by_name_and_user_id(tag_id, user.id, db=db): + log.debug(f'deleting tag: {tag_id}') + await Tags.delete_tag_by_name_and_user_id(tag_id, user.id, db=db) + + return chat_list + + +############################ +# GetChatsByFolderId +############################ + + +@router.get('/folder/{folder_id}', response_model=list[ChatResponse]) +async def get_chats_by_folder_id( + folder_id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + folder_ids = [folder_id] + children_folders = await Folders.get_children_folders_by_id_and_user_id(folder_id, user.id, db=db) + if children_folders: + folder_ids.extend([folder.id for folder in children_folders]) + + return [ + ChatResponse(**chat.model_dump()) + for chat in await Chats.get_chats_by_folder_ids_and_user_id(folder_ids, user.id, db=db) + ] + + +@router.get('/folder/{folder_id}/list') +async def get_chat_list_by_folder_id( + folder_id: str, + page: Optional[int] = 1, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + try: + limit = 10 + skip = (page - 1) * limit + + chats = await Chats.get_chats_by_folder_id_and_user_id(folder_id, user.id, skip=skip, limit=limit, db=db) + return [ + {'title': chat.title, 'id': chat.id, 'updated_at': chat.updated_at, 'last_read_at': chat.last_read_at} + for chat in chats + ] + + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# GetPinnedChats +############################ + + +@router.get('/pinned', response_model=list[ChatTitleIdResponse]) +async def get_user_pinned_chats(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + return await Chats.get_pinned_chats_by_user_id(user.id, db=db) + + +############################ +# GetChats +############################ + + +@router.get('/all', response_model=list[ChatResponse]) +async def get_user_chats(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + result = await Chats.get_chats_by_user_id(user.id, db=db) + return [ChatResponse(**chat.model_dump()) for chat in result.items] + + +############################ +# GetArchivedChats +############################ + + +@router.get('/all/archived', response_model=list[ChatResponse]) +async def get_user_archived_chats(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + return [ChatResponse(**chat.model_dump()) for chat in await Chats.get_archived_chats_by_user_id(user.id, db=db)] + + +############################ +# GetAllTags +############################ + + +@router.get('/all/tags', response_model=list[TagModel]) +async def get_all_user_tags(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + try: + tags = await Tags.get_tags_by_user_id(user.id, db=db) + return tags + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# GetAllChatsInDB +############################ + + +@router.get('/all/db', response_model=list[ChatResponse]) +async def get_all_user_chats_in_db(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + if not ENABLE_ADMIN_EXPORT: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + return [ChatResponse(**chat.model_dump()) for chat in await Chats.get_chats(db=db)] + + +############################ +# GetArchivedChats +############################ + + +@router.get('/archived', response_model=list[ChatTitleIdResponse]) +async def get_archived_session_user_chat_list( + page: Optional[int] = None, + query: Optional[str] = None, + order_by: Optional[str] = None, + direction: Optional[str] = None, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if page is None: + page = 1 + + limit = 60 + skip = (page - 1) * limit + + filter = {} + if query: + filter['query'] = query + if order_by: + filter['order_by'] = order_by + if direction: + filter['direction'] = direction + + return await Chats.get_archived_chat_list_by_user_id( + user.id, + filter=filter, + skip=skip, + limit=limit, + db=db, + ) + + +############################ +# ArchiveAllChats +############################ + + +@router.post('/archive/all', response_model=bool) +async def archive_all_chats(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + return await Chats.archive_all_chats_by_user_id(user.id, db=db) + + +############################ +# UnarchiveAllChats +############################ + + +@router.post('/unarchive/all', response_model=bool) +async def unarchive_all_chats(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + return await Chats.unarchive_all_chats_by_user_id(user.id, db=db) + + +############################ +# GetSharedChats +############################ + + +@router.get('/shared', response_model=list[SharedChatResponse]) +async def get_shared_session_user_chat_list( + page: Optional[int] = None, + query: Optional[str] = None, + order_by: Optional[str] = None, + direction: Optional[str] = None, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if page is None: + page = 1 + + limit = 60 + skip = (page - 1) * limit + + filter = {} + if query: + filter['query'] = query + if order_by: + filter['order_by'] = order_by + if direction: + filter['direction'] = direction + + return await SharedChats.get_by_user_id( + user.id, + filter=filter, + skip=skip, + limit=limit, + db=db, + ) + + +############################ +# GetSharedChatById +############################ + + +@router.get('/share/{share_id}', response_model=Optional[ChatResponse]) +async def get_shared_chat_by_id( + share_id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + if user.role == 'pending': + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND) + + if user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS: + chat = await Chats.get_chat_by_id(share_id, db=db) + else: + chat = await Chats.get_chat_by_share_id(share_id, db=db) + + if not chat: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND) + + # Look up the original chat_id to check access grants + shared = await SharedChats.get_by_id(share_id, db=db) + if shared: + has_grant = await AccessGrants.has_access( + user_id=user.id, + resource_type='shared_chat', + resource_id=shared.chat_id, + permission='read', + db=db, + ) + if not has_grant: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + return ChatResponse(**chat.model_dump()) + + +############################ +# GetChatsByTags +############################ + + +class TagForm(BaseModel): + name: str + + +class TagFilterForm(TagForm): + skip: Optional[int] = 0 + limit: Optional[int] = 50 + + +@router.post('/tags', response_model=list[ChatTitleIdResponse]) +async def get_user_chat_list_by_tag_name( + form_data: TagFilterForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + chats = await Chats.get_chat_list_by_user_id_and_tag_name( + user.id, form_data.name, form_data.skip, form_data.limit, db=db + ) + if len(chats) == 0: + await Tags.delete_tag_by_name_and_user_id(form_data.name, user.id, db=db) + + return chats + + +############################ +# GetChatById +############################ + + +@router.get('/{id}', response_model=Optional[ChatResponse]) +async def get_chat_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + + if not chat: + # Check if user has access via access grants (shared_chat grants) + if user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS: + chat = await Chats.get_chat_by_id(id, db=db) + else: + has_grant = await AccessGrants.has_access( + user_id=user.id, + resource_type='shared_chat', + resource_id=id, + permission='read', + db=db, + ) + if has_grant: + chat = await Chats.get_chat_by_id(id, db=db) + + if chat: + return ChatResponse(**chat.model_dump()) + + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND) + + +############################ +# UpdateChatById +############################ + + +@router.post('/{id}', response_model=Optional[ChatResponse]) +async def update_chat_by_id( + id: str, + form_data: ChatForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if chat: + updated_chat = {**chat.chat, **form_data.chat} + chat = await Chats.update_chat_by_id(id, updated_chat, db=db) + return ChatResponse(**chat.model_dump()) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + +############################ +# UpdateChatMessageById +############################ +class MessageForm(BaseModel): + content: str + + +@router.post('/{id}/messages/{message_id}', response_model=Optional[ChatResponse]) +async def update_chat_message_by_id( + id: str, + message_id: str, + form_data: MessageForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + chat = await Chats.get_chat_by_id(id, db=db) + + if not chat: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + if chat.user_id != user.id and user.role != 'admin': + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + chat = await Chats.upsert_message_to_chat_by_id_and_message_id( + id, + message_id, + { + 'content': form_data.content, + }, + ) + + event_emitter = await get_event_emitter( + { + 'user_id': chat.user_id, + 'chat_id': id, + 'message_id': message_id, + }, + False, + ) + + if event_emitter: + await event_emitter( + { + 'type': 'chat:message', + 'data': { + 'chat_id': id, + 'message_id': message_id, + 'content': form_data.content, + }, + } + ) + + return ChatResponse(**chat.model_dump()) + + +############################ +# SendChatMessageEventById +############################ +class EventForm(BaseModel): + type: str + data: dict + + +@router.post('/{id}/messages/{message_id}/event', response_model=Optional[bool]) +async def send_chat_message_event_by_id( + id: str, + message_id: str, + form_data: EventForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + chat = await Chats.get_chat_by_id(id, db=db) + + if not chat: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + if chat.user_id != user.id and user.role != 'admin': + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + event_emitter = await get_event_emitter( + { + 'user_id': chat.user_id, + 'chat_id': id, + 'message_id': message_id, + } + ) + + try: + if event_emitter: + await event_emitter(form_data.model_dump()) + else: + return False + return True + except Exception: + return False + + +############################ +# DeleteChatById +############################ + + +@router.delete('/{id}', response_model=bool) +async def delete_chat_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role == 'admin': + chat = await Chats.get_chat_by_id(id, db=db) + if not chat: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + await Chats.delete_orphan_tags_for_user(chat.meta.get('tags', []), user.id, threshold=1, db=db) + + result = await Chats.delete_chat_by_id(id, db=db) + + return result + else: + if not await has_permission(user.id, 'chat.delete', request.app.state.config.USER_PERMISSIONS): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if not chat: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + await Chats.delete_orphan_tags_for_user(chat.meta.get('tags', []), user.id, threshold=1, db=db) + + result = await Chats.delete_chat_by_id_and_user_id(id, user.id, db=db) + return result + + +############################ +# GetPinnedStatusById +############################ + + +@router.get('/{id}/pinned', response_model=Optional[bool]) +async def get_pinned_status_by_id( + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if chat: + return chat.pinned + else: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# PinChatById +############################ + + +@router.post('/{id}/pin', response_model=Optional[ChatResponse]) +async def pin_chat_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if chat: + chat = await Chats.toggle_chat_pinned_by_id(id, db=db) + return chat + else: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# CloneChat +############################ + + +class CloneForm(BaseModel): + title: Optional[str] = None + + +@router.post('/{id}/clone', response_model=Optional[ChatResponse]) +async def clone_chat_by_id( + form_data: CloneForm, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if chat: + updated_chat = { + **chat.chat, + 'originalChatId': chat.id, + 'branchPointMessageId': chat.chat['history']['currentId'], + 'title': form_data.title if form_data.title else f'Clone of {chat.title}', + } + + chats = await Chats.import_chats( + user.id, + [ + ChatImportForm( + **{ + 'chat': updated_chat, + 'meta': chat.meta, + 'pinned': chat.pinned, + 'folder_id': chat.folder_id, + } + ) + ], + db=db, + ) + + if chats: + chat = chats[0] + return ChatResponse(**chat.model_dump()) + else: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DEFAULT(), + ) + else: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# CloneSharedChatById +############################ + + +@router.post('/{id}/clone/shared', response_model=Optional[ChatResponse]) +async def clone_shared_chat_by_id( + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + if user.role == 'admin': + chat = await Chats.get_chat_by_id(id, db=db) + else: + chat = await Chats.get_chat_by_share_id(id, db=db) + + if not chat: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + # Enforce access grants + shared = await SharedChats.get_by_id(id, db=db) + if shared and user.role != 'admin': + has_grant = await AccessGrants.has_access( + user_id=user.id, + resource_type='shared_chat', + resource_id=shared.chat_id, + permission='read', + db=db, + ) + if not has_grant: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + updated_chat = { + **chat.chat, + 'originalChatId': chat.id, + 'branchPointMessageId': chat.chat['history']['currentId'], + 'title': f'Clone of {chat.title}', + } + + chats = await Chats.import_chats( + user.id, + [ + ChatImportForm( + **{ + 'chat': updated_chat, + 'meta': chat.meta, + 'pinned': chat.pinned, + 'folder_id': chat.folder_id, + } + ) + ], + db=db, + ) + + if chats: + chat = chats[0] + return ChatResponse(**chat.model_dump()) + else: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + +############################ +# ArchiveChat +############################ + + +@router.post('/{id}/archive', response_model=Optional[ChatResponse]) +async def archive_chat_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if chat: + chat = await Chats.toggle_chat_archive_by_id(id, db=db) + + tag_ids = chat.meta.get('tags', []) + if chat.archived: + # Archived chats are excluded from count — clean up orphans + await Chats.delete_orphan_tags_for_user(tag_ids, user.id, db=db) + else: + # Unarchived — ensure tag rows exist + await Tags.ensure_tags_exist(tag_ids, user.id, db=db) + + return ChatResponse(**chat.model_dump()) + else: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# ShareChatById +############################ + + +@router.post('/{id}/share', response_model=Optional[ChatResponse]) +async def share_chat_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if (user.role != 'admin') and ( + not await has_permission(user.id, 'chat.share', request.app.state.config.USER_PERMISSIONS) + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + + if chat: + if chat.share_id: + # Re-snapshot existing share + shared = await SharedChats.update(chat.share_id, db=db) + if shared: + # Re-fetch the original chat to return + chat = await Chats.get_chat_by_id(id, db=db) + return ChatResponse(**chat.model_dump()) + + # Create new share + shared = await SharedChats.create(id, user.id, db=db) + if not shared: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DEFAULT(), + ) + # Set share_id on the original chat + chat = await Chats.update_chat_share_id_by_id(id, shared.id, db=db) + if not chat: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DEFAULT(), + ) + return ChatResponse(**chat.model_dump()) + + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + +############################ +# DeleteSharedChatById +############################ + + +@router.delete('/{id}/share', response_model=Optional[bool]) +async def delete_shared_chat_by_id( + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if chat: + if not chat.share_id: + return False + + await SharedChats.delete_by_chat_id(id, db=db) + await Chats.update_chat_share_id_by_id(id, None, db=db) + + # Revoke all access grants for this shared chat + await AccessGrants.set_access_grants('shared_chat', id, [], db=db) + + return True + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + +############################ +# UpdateSharedChatAccessById +############################ + + +class ChatAccessGrantsForm(BaseModel): + access_grants: list[dict] + + +@router.post('/shared/{id}/access/update', response_model=Optional[ChatResponse]) +async def update_shared_chat_access_by_id( + request: Request, + id: str, + form_data: ChatAccessGrantsForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if not chat: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if chat.user_id != user.id and user.role != 'admin': + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_chats', + ) + + await AccessGrants.set_access_grants('shared_chat', id, form_data.access_grants, db=db) + + return ChatResponse(**chat.model_dump()) + + +############################ +# GetSharedChatAccessById +############################ + + +@router.get('/shared/{id}/access', response_model=list) +async def get_shared_chat_access_by_id( + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if not chat: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if chat.user_id != user.id and user.role != 'admin': + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + grants = await AccessGrants.get_grants_by_resource('shared_chat', id, db=db) + return [ + { + 'id': g.id, + 'principal_type': g.principal_type, + 'principal_id': g.principal_id, + 'permission': g.permission, + } + for g in grants + ] + + +############################ +# UpdateChatFolderIdById +############################ + + +class ChatFolderIdForm(BaseModel): + folder_id: Optional[str] = None + + +@router.post('/{id}/folder', response_model=Optional[ChatResponse]) +async def update_chat_folder_id_by_id( + id: str, + form_data: ChatFolderIdForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if chat: + chat = await Chats.update_chat_folder_id_by_id_and_user_id(id, user.id, form_data.folder_id, db=db) + return ChatResponse(**chat.model_dump()) + else: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# GetChatTagsById +############################ + + +@router.get('/{id}/tags', response_model=list[TagModel]) +async def get_chat_tags_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if chat: + tags = chat.meta.get('tags', []) + return await Tags.get_tags_by_ids_and_user_id(tags, user.id, db=db) + else: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND) + + +############################ +# AddChatTagById +############################ + + +@router.post('/{id}/tags', response_model=list[TagModel]) +async def add_tag_by_id_and_tag_name( + id: str, + form_data: TagForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if chat: + tags = chat.meta.get('tags', []) + tag_id = form_data.name.replace(' ', '_').lower() + + if tag_id == 'none': + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Tag name cannot be 'None'"), + ) + + if tag_id not in tags: + await Chats.add_chat_tag_by_id_and_user_id_and_tag_name(id, user.id, form_data.name, db=db) + + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + tags = chat.meta.get('tags', []) + return await Tags.get_tags_by_ids_and_user_id(tags, user.id, db=db) + else: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# DeleteChatTagById +############################ + + +@router.delete('/{id}/tags', response_model=list[TagModel]) +async def delete_tag_by_id_and_tag_name( + id: str, + form_data: TagForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if chat: + await Chats.delete_tag_by_id_and_user_id_and_tag_name(id, user.id, form_data.name, db=db) + + if await Chats.count_chats_by_tag_name_and_user_id(form_data.name, user.id, db=db) == 0: + await Tags.delete_tag_by_name_and_user_id(form_data.name, user.id, db=db) + + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + tags = chat.meta.get('tags', []) + return await Tags.get_tags_by_ids_and_user_id(tags, user.id, db=db) + else: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND) + + +############################ +# DeleteAllTagsById +############################ + + +@router.delete('/{id}/tags/all', response_model=Optional[bool]) +async def delete_all_tags_by_id( + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if chat: + old_tags = chat.meta.get('tags', []) + await Chats.delete_all_tags_by_id_and_user_id(id, user.id, db=db) + await Chats.delete_orphan_tags_for_user(old_tags, user.id, db=db) + + return True + else: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND) diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py new file mode 100644 index 0000000000000000000000000000000000000000..02b16d8e5b817dc1bb3f55d701ea0fb012ab0d4e --- /dev/null +++ b/backend/open_webui/routers/configs.py @@ -0,0 +1,664 @@ +import logging +import copy +from fastapi import APIRouter, Depends, Request, HTTPException +from pydantic import BaseModel, ConfigDict +import aiohttp + +from typing import Optional + +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.config import get_config, save_config, async_save_config +from open_webui.config import BannerModel + +from open_webui.utils.tools import ( + get_tool_server_data, + get_tool_server_url, + set_tool_servers, + set_terminal_servers, +) +from open_webui.utils.mcp.client import MCPClient +from open_webui.models.oauth_sessions import OAuthSessions + + +from open_webui.utils.oauth import ( + get_discovery_urls, + get_oauth_client_info_with_dynamic_client_registration, + get_oauth_client_info_with_static_credentials, + encrypt_data, + decrypt_data, + resolve_oauth_client_info, + OAuthClientInformationFull, +) +from mcp.shared.auth import OAuthMetadata + +router = APIRouter() + +log = logging.getLogger(__name__) + + +############################ +# ImportConfig +# Thy configuration come, thy settings be done, +# in production as it is in development. +############################ + + +class ImportConfigForm(BaseModel): + config: dict + + +@router.post('/import', response_model=dict) +async def import_config(form_data: ImportConfigForm, user=Depends(get_admin_user)): + await async_save_config(form_data.config) + return get_config() + + +############################ +# ExportConfig +############################ + + +@router.get('/export', response_model=dict) +async def export_config(user=Depends(get_admin_user)): + return get_config() + + +############################ +# Connections Config +############################ + + +class ConnectionsConfigForm(BaseModel): + ENABLE_DIRECT_CONNECTIONS: bool + ENABLE_BASE_MODELS_CACHE: bool + + +@router.get('/connections', response_model=ConnectionsConfigForm) +async def get_connections_config(request: Request, user=Depends(get_admin_user)): + return { + 'ENABLE_DIRECT_CONNECTIONS': request.app.state.config.ENABLE_DIRECT_CONNECTIONS, + 'ENABLE_BASE_MODELS_CACHE': request.app.state.config.ENABLE_BASE_MODELS_CACHE, + } + + +@router.post('/connections', response_model=ConnectionsConfigForm) +async def set_connections_config( + request: Request, + form_data: ConnectionsConfigForm, + user=Depends(get_admin_user), +): + request.app.state.config.ENABLE_DIRECT_CONNECTIONS = form_data.ENABLE_DIRECT_CONNECTIONS + request.app.state.config.ENABLE_BASE_MODELS_CACHE = form_data.ENABLE_BASE_MODELS_CACHE + + return { + 'ENABLE_DIRECT_CONNECTIONS': request.app.state.config.ENABLE_DIRECT_CONNECTIONS, + 'ENABLE_BASE_MODELS_CACHE': request.app.state.config.ENABLE_BASE_MODELS_CACHE, + } + + +class OAuthClientRegistrationForm(BaseModel): + url: str + client_id: str + client_name: Optional[str] = None + client_secret: Optional[str] = None + + +@router.post('/oauth/clients/register') +async def register_oauth_client( + request: Request, + form_data: OAuthClientRegistrationForm, + type: Optional[str] = None, + user=Depends(get_admin_user), +): + try: + oauth_client_id = form_data.client_id + if type: + oauth_client_id = f'{type}:{form_data.client_id}' + + if form_data.client_secret: + # Static credentials: skip dynamic registration, build from provided credentials + oauth_client_info = await get_oauth_client_info_with_static_credentials( + request, + oauth_client_id, + form_data.url, + oauth_client_id=form_data.client_id, + oauth_client_secret=form_data.client_secret, + ) + else: + oauth_client_info = await get_oauth_client_info_with_dynamic_client_registration( + request, oauth_client_id, form_data.url + ) + return { + 'status': True, + 'oauth_client_info': encrypt_data(oauth_client_info.model_dump(mode='json')), + } + except Exception as e: + log.debug(f'Failed to register OAuth client: {e}') + raise HTTPException( + status_code=400, + detail=f'Failed to register OAuth client', + ) + + +############################ +# ToolServers Config +############################ + + +class ToolServerConnection(BaseModel): + url: str + path: str + type: Optional[str] = 'openapi' # openapi, mcp + auth_type: Optional[str] + headers: Optional[dict | str] = None + key: Optional[str] + config: Optional[dict] + + model_config = ConfigDict(extra='allow') + + +class ToolServersConfigForm(BaseModel): + TOOL_SERVER_CONNECTIONS: list[ToolServerConnection] + + +@router.get('/tool_servers', response_model=ToolServersConfigForm) +async def get_tool_servers_config(request: Request, user=Depends(get_admin_user)): + return { + 'TOOL_SERVER_CONNECTIONS': request.app.state.config.TOOL_SERVER_CONNECTIONS, + } + + +@router.post('/tool_servers', response_model=ToolServersConfigForm) +async def set_tool_servers_config( + request: Request, + form_data: ToolServersConfigForm, + user=Depends(get_admin_user), +): + for connection in request.app.state.config.TOOL_SERVER_CONNECTIONS: + server_type = connection.get('type', 'openapi') + auth_type = connection.get('auth_type', 'none') + + if auth_type in ('oauth_2.1', 'oauth_2.1_static'): + # Remove existing OAuth clients for tool servers + server_id = connection.get('info', {}).get('id') + client_key = f'{server_type}:{server_id}' + + try: + request.app.state.oauth_client_manager.remove_client(client_key) + except Exception: + pass + + # Set new tool server connections + request.app.state.config.TOOL_SERVER_CONNECTIONS = [ + connection.model_dump() for connection in form_data.TOOL_SERVER_CONNECTIONS + ] + + await set_tool_servers(request) + + for connection in request.app.state.config.TOOL_SERVER_CONNECTIONS: + server_type = connection.get('type', 'openapi') + if server_type == 'mcp': + server_id = connection.get('info', {}).get('id') + auth_type = connection.get('auth_type', 'none') + + if auth_type in ('oauth_2.1', 'oauth_2.1_static') and server_id: + try: + oauth_client_info = resolve_oauth_client_info(connection) + request.app.state.oauth_client_manager.add_client( + f'{server_type}:{server_id}', + OAuthClientInformationFull(**oauth_client_info), + ) + except Exception as e: + log.debug(f'Failed to add OAuth client for MCP tool server: {e}') + continue + + return { + 'TOOL_SERVER_CONNECTIONS': request.app.state.config.TOOL_SERVER_CONNECTIONS, + } + + +class TerminalServerConnection(BaseModel): + id: Optional[str] = '' + name: Optional[str] = '' + + enabled: Optional[bool] = True + + url: str + path: Optional[str] = '/openapi.json' + + key: Optional[str] = '' + auth_type: Optional[str] = 'bearer' + + config: Optional[dict] = None + + # Orchestrator policy fields + server_type: Optional[str] = None # "orchestrator", "terminal" + policy_id: Optional[str] = None + policy: Optional[dict] = None # cached policy data + + model_config = ConfigDict(extra='allow') + + +class TerminalServersConfigForm(BaseModel): + TERMINAL_SERVER_CONNECTIONS: list[TerminalServerConnection] + + +@router.get('/terminal_servers') +async def get_terminal_servers_config(request: Request, user=Depends(get_admin_user)): + return { + 'TERMINAL_SERVER_CONNECTIONS': request.app.state.config.TERMINAL_SERVER_CONNECTIONS, + } + + +@router.post('/terminal_servers') +async def set_terminal_servers_config( + request: Request, + form_data: TerminalServersConfigForm, + user=Depends(get_admin_user), +): + request.app.state.config.TERMINAL_SERVER_CONNECTIONS = [ + connection.model_dump() for connection in form_data.TERMINAL_SERVER_CONNECTIONS + ] + + await set_terminal_servers(request) + + return { + 'TERMINAL_SERVER_CONNECTIONS': request.app.state.config.TERMINAL_SERVER_CONNECTIONS, + } + + +@router.post('/terminal_servers/verify') +async def verify_terminal_server_connection( + request: Request, form_data: TerminalServerConnection, user=Depends(get_admin_user) +): + """ + Verify the connection to a terminal server by detecting its type. + + Tries GET {url}/api/v1/policies (orchestrator) then GET {url}/api/config + (plain terminal). Returns ``{status: true, type: "orchestrator"|"terminal"}``. + """ + base_url = (form_data.url or '').rstrip('/') + if not base_url: + raise HTTPException(status_code=400, detail='Terminal server URL is required') + + headers = {} + if form_data.auth_type == 'bearer' and form_data.key: + headers['Authorization'] = f'Bearer {form_data.key}' + + try: + async with aiohttp.ClientSession( + trust_env=True, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) as session: + # Orchestrators expose a policies API; plain terminals don't. + try: + async with session.get( + f'{base_url}/api/v1/policies', headers=headers, ssl=AIOHTTP_CLIENT_SESSION_SSL + ) as resp: + if resp.ok: + return {'status': True, 'type': 'orchestrator'} + except Exception: + pass + + # Fall back to open-terminal config endpoint. + try: + async with session.get( + f'{base_url}/api/config', headers=headers, ssl=AIOHTTP_CLIENT_SESSION_SSL + ) as resp: + if resp.ok: + return {'status': True, 'type': 'terminal'} + except Exception: + pass + + except Exception as e: + log.debug(f'Failed to connect to the terminal server: {e}') + + raise HTTPException(status_code=400, detail='Failed to connect to the terminal server') + + +class TerminalServerPolicyForm(BaseModel): + url: str + key: Optional[str] = '' + auth_type: Optional[str] = 'bearer' + policy_id: str + policy_data: dict + + +@router.post('/terminal_servers/policy') +async def put_terminal_server_policy( + request: Request, form_data: TerminalServerPolicyForm, user=Depends(get_admin_user) +): + """ + Proxy a policy PUT to an orchestrator terminal server. + """ + base_url = (form_data.url or '').rstrip('/') + if not base_url: + raise HTTPException(status_code=400, detail='Terminal server URL is required') + + headers = {'Content-Type': 'application/json'} + if form_data.auth_type == 'bearer' and form_data.key: + headers['Authorization'] = f'Bearer {form_data.key}' + + try: + async with aiohttp.ClientSession( + trust_env=True, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) as session: + policy_url = f'{base_url}/api/v1/policies/{form_data.policy_id}' + async with session.put( + policy_url, headers=headers, json=form_data.policy_data, ssl=AIOHTTP_CLIENT_SESSION_SSL + ) as resp: + if resp.ok: + return await resp.json() + detail = await resp.text() + raise HTTPException(status_code=resp.status, detail=detail) + except HTTPException: + raise + except Exception as e: + log.debug(f'Failed to save policy to terminal server: {e}') + raise HTTPException(status_code=400, detail='Failed to save policy to terminal server') + + +@router.post('/tool_servers/verify') +async def verify_tool_servers_config(request: Request, form_data: ToolServerConnection, user=Depends(get_admin_user)): + """ + Verify the connection to the tool server. + """ + try: + if form_data.type == 'mcp': + if form_data.auth_type in ('oauth_2.1', 'oauth_2.1_static'): + discovery_urls = await get_discovery_urls(form_data.url) + for discovery_url in discovery_urls: + log.debug(f'Trying to fetch OAuth 2.1 discovery document from {discovery_url}') + async with aiohttp.ClientSession( + trust_env=True, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) as session: + async with session.get( + discovery_url, ssl=AIOHTTP_CLIENT_SESSION_SSL + ) as oauth_server_metadata_response: + if oauth_server_metadata_response.status == 200: + try: + oauth_server_metadata = OAuthMetadata.model_validate( + await oauth_server_metadata_response.json() + ) + return { + 'status': True, + 'oauth_server_metadata': oauth_server_metadata.model_dump(mode='json'), + } + except Exception as e: + log.info(f'Failed to parse OAuth 2.1 discovery document: {e}') + raise HTTPException( + status_code=400, + detail=f'Failed to parse OAuth 2.1 discovery document from {discovery_url}', + ) + + raise HTTPException( + status_code=400, + detail=f'Failed to fetch OAuth 2.1 discovery document from {discovery_urls}', + ) + else: + try: + client = MCPClient() + headers = None + + token = None + if form_data.auth_type == 'bearer': + token = form_data.key + elif form_data.auth_type == 'session': + token = request.state.token.credentials + elif form_data.auth_type == 'system_oauth': + oauth_token = None + try: + if request.cookies.get('oauth_session_id', None): + oauth_token = await request.app.state.oauth_manager.get_oauth_token( + user.id, + request.cookies.get('oauth_session_id', None), + ) + + if oauth_token: + token = oauth_token.get('access_token', '') + except Exception as e: + pass + if token: + headers = {'Authorization': f'Bearer {token}'} + + if form_data.headers and isinstance(form_data.headers, dict): + if headers is None: + headers = {} + headers.update(form_data.headers) + + await client.connect(form_data.url, headers=headers) + specs = await client.list_tool_specs() + return { + 'status': True, + 'specs': specs, + } + except Exception as e: + log.debug(f'Failed to create MCP client: {e}') + raise HTTPException( + status_code=400, + detail=f'Failed to create MCP client', + ) + finally: + if client: + await client.disconnect() + else: # openapi + token = None + headers = None + if form_data.auth_type == 'bearer': + token = form_data.key + elif form_data.auth_type == 'session': + token = request.state.token.credentials + elif form_data.auth_type == 'system_oauth': + try: + if request.cookies.get('oauth_session_id', None): + oauth_token = await request.app.state.oauth_manager.get_oauth_token( + user.id, + request.cookies.get('oauth_session_id', None), + ) + + if oauth_token: + token = oauth_token.get('access_token', '') + + except Exception as e: + pass + + if token: + headers = {'Authorization': f'Bearer {token}'} + + if form_data.headers and isinstance(form_data.headers, dict): + if headers is None: + headers = {} + headers.update(form_data.headers) + + url = get_tool_server_url(form_data.url, form_data.path) + return await get_tool_server_data(url, headers=headers) + except HTTPException as e: + raise e + except Exception as e: + log.debug(f'Failed to connect to the tool server: {e}') + raise HTTPException( + status_code=400, + detail=f'Failed to connect to the tool server', + ) + + +############################ +# CodeInterpreterConfig +############################ +class CodeInterpreterConfigForm(BaseModel): + ENABLE_CODE_EXECUTION: bool + CODE_EXECUTION_ENGINE: str + CODE_EXECUTION_JUPYTER_URL: Optional[str] + CODE_EXECUTION_JUPYTER_AUTH: Optional[str] + CODE_EXECUTION_JUPYTER_AUTH_TOKEN: Optional[str] + CODE_EXECUTION_JUPYTER_AUTH_PASSWORD: Optional[str] + CODE_EXECUTION_JUPYTER_TIMEOUT: Optional[int] + ENABLE_CODE_INTERPRETER: bool + CODE_INTERPRETER_ENGINE: str + CODE_INTERPRETER_PROMPT_TEMPLATE: Optional[str] + CODE_INTERPRETER_JUPYTER_URL: Optional[str] + CODE_INTERPRETER_JUPYTER_AUTH: Optional[str] + CODE_INTERPRETER_JUPYTER_AUTH_TOKEN: Optional[str] + CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD: Optional[str] + CODE_INTERPRETER_JUPYTER_TIMEOUT: Optional[int] + + +@router.get('/code_execution', response_model=CodeInterpreterConfigForm) +async def get_code_execution_config(request: Request, user=Depends(get_admin_user)): + return { + 'ENABLE_CODE_EXECUTION': request.app.state.config.ENABLE_CODE_EXECUTION, + 'CODE_EXECUTION_ENGINE': request.app.state.config.CODE_EXECUTION_ENGINE, + 'CODE_EXECUTION_JUPYTER_URL': request.app.state.config.CODE_EXECUTION_JUPYTER_URL, + 'CODE_EXECUTION_JUPYTER_AUTH': request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH, + 'CODE_EXECUTION_JUPYTER_AUTH_TOKEN': request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN, + 'CODE_EXECUTION_JUPYTER_AUTH_PASSWORD': request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, + 'CODE_EXECUTION_JUPYTER_TIMEOUT': request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT, + 'ENABLE_CODE_INTERPRETER': request.app.state.config.ENABLE_CODE_INTERPRETER, + 'CODE_INTERPRETER_ENGINE': request.app.state.config.CODE_INTERPRETER_ENGINE, + 'CODE_INTERPRETER_PROMPT_TEMPLATE': request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE, + 'CODE_INTERPRETER_JUPYTER_URL': request.app.state.config.CODE_INTERPRETER_JUPYTER_URL, + 'CODE_INTERPRETER_JUPYTER_AUTH': request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH, + 'CODE_INTERPRETER_JUPYTER_AUTH_TOKEN': request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, + 'CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD': request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, + 'CODE_INTERPRETER_JUPYTER_TIMEOUT': request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT, + } + + +@router.post('/code_execution', response_model=CodeInterpreterConfigForm) +async def set_code_execution_config( + request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user) +): + request.app.state.config.ENABLE_CODE_EXECUTION = form_data.ENABLE_CODE_EXECUTION + + request.app.state.config.CODE_EXECUTION_ENGINE = form_data.CODE_EXECUTION_ENGINE + request.app.state.config.CODE_EXECUTION_JUPYTER_URL = form_data.CODE_EXECUTION_JUPYTER_URL + request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH = form_data.CODE_EXECUTION_JUPYTER_AUTH + request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = form_data.CODE_EXECUTION_JUPYTER_AUTH_TOKEN + request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = form_data.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD + request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT = form_data.CODE_EXECUTION_JUPYTER_TIMEOUT + + request.app.state.config.ENABLE_CODE_INTERPRETER = form_data.ENABLE_CODE_INTERPRETER + request.app.state.config.CODE_INTERPRETER_ENGINE = form_data.CODE_INTERPRETER_ENGINE + request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = form_data.CODE_INTERPRETER_PROMPT_TEMPLATE + + request.app.state.config.CODE_INTERPRETER_JUPYTER_URL = form_data.CODE_INTERPRETER_JUPYTER_URL + + request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH = form_data.CODE_INTERPRETER_JUPYTER_AUTH + + request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = form_data.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN + request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = form_data.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD + request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT = form_data.CODE_INTERPRETER_JUPYTER_TIMEOUT + + return { + 'ENABLE_CODE_EXECUTION': request.app.state.config.ENABLE_CODE_EXECUTION, + 'CODE_EXECUTION_ENGINE': request.app.state.config.CODE_EXECUTION_ENGINE, + 'CODE_EXECUTION_JUPYTER_URL': request.app.state.config.CODE_EXECUTION_JUPYTER_URL, + 'CODE_EXECUTION_JUPYTER_AUTH': request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH, + 'CODE_EXECUTION_JUPYTER_AUTH_TOKEN': request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN, + 'CODE_EXECUTION_JUPYTER_AUTH_PASSWORD': request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, + 'CODE_EXECUTION_JUPYTER_TIMEOUT': request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT, + 'ENABLE_CODE_INTERPRETER': request.app.state.config.ENABLE_CODE_INTERPRETER, + 'CODE_INTERPRETER_ENGINE': request.app.state.config.CODE_INTERPRETER_ENGINE, + 'CODE_INTERPRETER_PROMPT_TEMPLATE': request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE, + 'CODE_INTERPRETER_JUPYTER_URL': request.app.state.config.CODE_INTERPRETER_JUPYTER_URL, + 'CODE_INTERPRETER_JUPYTER_AUTH': request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH, + 'CODE_INTERPRETER_JUPYTER_AUTH_TOKEN': request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, + 'CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD': request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, + 'CODE_INTERPRETER_JUPYTER_TIMEOUT': request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT, + } + + +############################ +# SetDefaultModels +############################ +class ModelsConfigForm(BaseModel): + DEFAULT_MODELS: Optional[str] + DEFAULT_PINNED_MODELS: Optional[str] + MODEL_ORDER_LIST: Optional[list[str]] + DEFAULT_MODEL_METADATA: Optional[dict] = None + DEFAULT_MODEL_PARAMS: Optional[dict] = None + + +@router.get('/models/defaults') +async def get_models_defaults(request: Request, user=Depends(get_verified_user)): + return { + 'DEFAULT_MODEL_METADATA': request.app.state.config.DEFAULT_MODEL_METADATA, + } + + +@router.get('/models', response_model=ModelsConfigForm) +async def get_models_config(request: Request, user=Depends(get_admin_user)): + return { + 'DEFAULT_MODELS': request.app.state.config.DEFAULT_MODELS, + 'DEFAULT_PINNED_MODELS': request.app.state.config.DEFAULT_PINNED_MODELS, + 'MODEL_ORDER_LIST': request.app.state.config.MODEL_ORDER_LIST, + 'DEFAULT_MODEL_METADATA': request.app.state.config.DEFAULT_MODEL_METADATA, + 'DEFAULT_MODEL_PARAMS': request.app.state.config.DEFAULT_MODEL_PARAMS, + } + + +@router.post('/models', response_model=ModelsConfigForm) +async def set_models_config(request: Request, form_data: ModelsConfigForm, user=Depends(get_admin_user)): + request.app.state.config.DEFAULT_MODELS = form_data.DEFAULT_MODELS + request.app.state.config.DEFAULT_PINNED_MODELS = form_data.DEFAULT_PINNED_MODELS + request.app.state.config.MODEL_ORDER_LIST = form_data.MODEL_ORDER_LIST + request.app.state.config.DEFAULT_MODEL_METADATA = form_data.DEFAULT_MODEL_METADATA + request.app.state.config.DEFAULT_MODEL_PARAMS = form_data.DEFAULT_MODEL_PARAMS + return { + 'DEFAULT_MODELS': request.app.state.config.DEFAULT_MODELS, + 'DEFAULT_PINNED_MODELS': request.app.state.config.DEFAULT_PINNED_MODELS, + 'MODEL_ORDER_LIST': request.app.state.config.MODEL_ORDER_LIST, + 'DEFAULT_MODEL_METADATA': request.app.state.config.DEFAULT_MODEL_METADATA, + 'DEFAULT_MODEL_PARAMS': request.app.state.config.DEFAULT_MODEL_PARAMS, + } + + +class PromptSuggestion(BaseModel): + title: list[str] + content: str + + +class SetDefaultSuggestionsForm(BaseModel): + suggestions: list[PromptSuggestion] + + +@router.post('/suggestions', response_model=list[PromptSuggestion]) +async def set_default_suggestions( + request: Request, + form_data: SetDefaultSuggestionsForm, + user=Depends(get_admin_user), +): + data = form_data.model_dump() + request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS = data['suggestions'] + return request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS + + +############################ +# SetBanners +############################ + + +class SetBannersForm(BaseModel): + banners: list[BannerModel] + + +@router.post('/banners', response_model=list[BannerModel]) +async def set_banners( + request: Request, + form_data: SetBannersForm, + user=Depends(get_admin_user), +): + data = form_data.model_dump() + request.app.state.config.BANNERS = data['banners'] + return request.app.state.config.BANNERS + + +@router.get('/banners', response_model=list[BannerModel]) +async def get_banners( + request: Request, + user=Depends(get_verified_user), +): + return request.app.state.config.BANNERS diff --git a/backend/open_webui/routers/evaluations.py b/backend/open_webui/routers/evaluations.py new file mode 100644 index 0000000000000000000000000000000000000000..072c7fa7322b53fd38a9ce3d608caf2ae95658a6 --- /dev/null +++ b/backend/open_webui/routers/evaluations.py @@ -0,0 +1,429 @@ +from typing import Optional +import logging +from fastapi import APIRouter, Depends, HTTPException, status, Request +from fastapi.concurrency import run_in_threadpool +from pydantic import BaseModel + +from open_webui.models.users import Users, UserModel +from open_webui.models.feedbacks import ( + FeedbackIdResponse, + FeedbackModel, + FeedbackResponse, + FeedbackForm, + FeedbackUserResponse, + FeedbackListResponse, + LeaderboardFeedbackData, + ModelHistoryEntry, + ModelHistoryResponse, + Feedbacks, +) + +from open_webui.constants import ERROR_MESSAGES +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.internal.db import get_async_session +from sqlalchemy.ext.asyncio import AsyncSession + +log = logging.getLogger(__name__) + + +router = APIRouter() + + +# 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. + +import os + +EMBEDDING_MODEL_NAME = os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2') +_embedding_model = None + + +def _get_embedding_model(): + global _embedding_model + if _embedding_model is None: + try: + from sentence_transformers import SentenceTransformer + + _embedding_model = SentenceTransformer(EMBEDDING_MODEL_NAME) + except Exception as e: + log.error(f'Embedding model load failed: {e}') + return _embedding_model + + +def _calculate_elo(feedbacks: list[LeaderboardFeedbackData], similarities: dict = None) -> dict: + """ + Calculate Elo ratings for models based on user feedback. + + Each feedback represents a comparison where a user rated one model + against its opponents (sibling_model_ids). Rating=1 means the model won, + rating=-1 means it lost. + + The Elo system adjusts ratings based on: + - Current rating difference (upsets cause bigger swings) + - Optional similarity weights (for query-based filtering) + + Returns: {model_id: {"rating": float, "won": int, "lost": int}} + """ + K_FACTOR = 32 # Standard Elo K-factor for rating volatility + model_stats = {} + + def get_or_create_stats(model_id): + if model_id not in model_stats: + model_stats[model_id] = {'rating': 1000.0, 'won': 0, 'lost': 0} + return model_stats[model_id] + + for feedback in feedbacks: + data = feedback.data or {} + winner_id = data.get('model_id') + rating_value = str(data.get('rating', '')) + if not winner_id or rating_value not in ('1', '-1'): + continue + + won = rating_value == '1' + weight = similarities.get(feedback.id, 1.0) if similarities else 1.0 + + for opponent_id in data.get('sibling_model_ids') or []: + winner = get_or_create_stats(winner_id) + opponent = get_or_create_stats(opponent_id) + expected = 1 / (1 + 10 ** ((opponent['rating'] - winner['rating']) / 400)) + + winner['rating'] += K_FACTOR * ((1 if won else 0) - expected) * weight + opponent['rating'] += K_FACTOR * ((0 if won else 1) - (1 - expected)) * weight + + if won: + winner['won'] += 1 + opponent['lost'] += 1 + else: + winner['lost'] += 1 + opponent['won'] += 1 + + return model_stats + + +def _get_top_tags(feedbacks: list[LeaderboardFeedbackData], limit: int = 5) -> dict: + """ + Count tag occurrences per model and return the most frequent ones. + + Each feedback can have tags describing the conversation topic. + This aggregates those tags per model to show what topics each model + is commonly used for. + + Returns: {model_id: [{"tag": str, "count": int}, ...]} + """ + from collections import defaultdict + + tag_counts = defaultdict(lambda: defaultdict(int)) + + for feedback in feedbacks: + data = feedback.data or {} + model_id = data.get('model_id') + if model_id: + for tag in data.get('tags', []): + tag_counts[model_id][tag] += 1 + + return { + model_id: [{'tag': tag, 'count': count} for tag, count in sorted(tags.items(), key=lambda x: -x[1])[:limit]] + for model_id, tags in tag_counts.items() + } + + +def _compute_similarities(feedbacks: list[LeaderboardFeedbackData], query: str) -> dict: + """ + Compute how relevant each feedback is to a search query. + + Uses embeddings to find semantic similarity between the query and + each feedback's tags. Higher similarity means the feedback is more + relevant to what the user searched for. + + This is used to weight Elo calculations - feedbacks matching the + query have more influence on the final rankings. + + Returns: {feedback_id: similarity_score (0-1)} + """ + import numpy as np + + embedding_model = _get_embedding_model() + if not embedding_model: + return {} + + all_tags = list({tag for feedback in feedbacks if feedback.data for tag in feedback.data.get('tags', [])}) + if not all_tags: + return {} + + try: + tag_embeddings = embedding_model.encode(all_tags) + query_embedding = embedding_model.encode([query])[0] + except Exception as e: + log.error(f'Embedding error: {e}') + return {} + + # Vectorized cosine similarity + tag_norms = np.linalg.norm(tag_embeddings, axis=1) + query_norm = np.linalg.norm(query_embedding) + similarities = np.dot(tag_embeddings, query_embedding) / (tag_norms * query_norm + 1e-9) + tag_similarity_map = dict(zip(all_tags, similarities.tolist())) + + return { + feedback.id: max( + (tag_similarity_map.get(tag, 0) for tag in (feedback.data or {}).get('tags', [])), + default=0, + ) + for feedback in feedbacks + } + + +class LeaderboardEntry(BaseModel): + model_id: str + rating: int + won: int + lost: int + count: int + top_tags: list[dict] + + +class LeaderboardResponse(BaseModel): + entries: list[LeaderboardEntry] + + +@router.get('/leaderboard', response_model=LeaderboardResponse) +async def get_leaderboard( + query: Optional[str] = None, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Get model leaderboard with Elo ratings. Query filters by tag similarity.""" + feedbacks = await Feedbacks.get_feedbacks_for_leaderboard(db=db) + + similarities = None + if query and query.strip(): + similarities = await run_in_threadpool(_compute_similarities, feedbacks, query.strip()) + + elo_stats = _calculate_elo(feedbacks, similarities) + tags_by_model = _get_top_tags(feedbacks) + + entries = sorted( + [ + LeaderboardEntry( + model_id=mid, + rating=round(s['rating']), + won=s['won'], + lost=s['lost'], + count=s['won'] + s['lost'], + top_tags=tags_by_model.get(mid, []), + ) + for mid, s in elo_stats.items() + ], + key=lambda e: e.rating, + reverse=True, + ) + + return LeaderboardResponse(entries=entries) + + +@router.get('/leaderboard/{model_id}/history', response_model=ModelHistoryResponse) +async def get_model_history( + model_id: str, + days: int = 30, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Get daily win/loss history for a specific model.""" + history = await Feedbacks.get_model_evaluation_history(model_id=model_id, days=days, db=db) + return ModelHistoryResponse(model_id=model_id, history=history) + + +############################ +# GetConfig +############################ + + +@router.get('/config') +async def get_config(request: Request, user=Depends(get_admin_user)): + return { + 'ENABLE_EVALUATION_ARENA_MODELS': request.app.state.config.ENABLE_EVALUATION_ARENA_MODELS, + 'EVALUATION_ARENA_MODELS': request.app.state.config.EVALUATION_ARENA_MODELS, + } + + +############################ +# UpdateConfig +############################ + + +class UpdateConfigForm(BaseModel): + ENABLE_EVALUATION_ARENA_MODELS: Optional[bool] = None + EVALUATION_ARENA_MODELS: Optional[list[dict]] = None + + +@router.post('/config') +async def update_config( + request: Request, + form_data: UpdateConfigForm, + user=Depends(get_admin_user), +): + config = request.app.state.config + if form_data.ENABLE_EVALUATION_ARENA_MODELS is not None: + config.ENABLE_EVALUATION_ARENA_MODELS = form_data.ENABLE_EVALUATION_ARENA_MODELS + if form_data.EVALUATION_ARENA_MODELS is not None: + config.EVALUATION_ARENA_MODELS = form_data.EVALUATION_ARENA_MODELS + return { + 'ENABLE_EVALUATION_ARENA_MODELS': config.ENABLE_EVALUATION_ARENA_MODELS, + 'EVALUATION_ARENA_MODELS': config.EVALUATION_ARENA_MODELS, + } + + +@router.get('/feedbacks/models', response_model=list[str]) +async def get_feedback_model_ids(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + return await Feedbacks.get_distinct_model_ids(db=db) + + +@router.get('/feedbacks/all', response_model=list[FeedbackResponse]) +async def get_all_feedbacks(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + feedbacks = await Feedbacks.get_all_feedbacks(db=db) + return feedbacks + + +@router.get('/feedbacks/all/ids', response_model=list[FeedbackIdResponse]) +async def get_all_feedback_ids(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + return await Feedbacks.get_all_feedback_ids(db=db) + + +@router.delete('/feedbacks/all') +async def delete_all_feedbacks(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + success = await Feedbacks.delete_all_feedbacks(db=db) + return success + + +@router.get('/feedbacks/all/export', response_model=list[FeedbackModel]) +async def export_all_feedbacks( + model_id: Optional[str] = None, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + feedbacks = await Feedbacks.get_all_feedbacks(db=db) + if model_id: + feedbacks = [f for f in feedbacks if f.data and f.data.get('model_id') == model_id] + return feedbacks + + +@router.get('/feedbacks/user', response_model=list[FeedbackUserResponse]) +async def get_feedbacks(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + feedbacks = await Feedbacks.get_feedbacks_by_user_id(user.id, db=db) + return feedbacks + + +@router.delete('/feedbacks', response_model=bool) +async def delete_feedbacks(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + success = await Feedbacks.delete_feedbacks_by_user_id(user.id, db=db) + return success + + +PAGE_ITEM_COUNT = 30 + + +@router.get('/feedbacks/list', response_model=FeedbackListResponse) +async def get_feedbacks( + order_by: Optional[str] = None, + direction: Optional[str] = None, + page: Optional[int] = 1, + model_id: Optional[str] = None, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + limit = PAGE_ITEM_COUNT + + page = max(1, page) + skip = (page - 1) * limit + + filter = {} + if order_by: + filter['order_by'] = order_by + if direction: + filter['direction'] = direction + if model_id: + filter['model_id'] = model_id + + result = await Feedbacks.get_feedback_items(filter=filter, skip=skip, limit=limit, db=db) + return result + + +@router.post('/feedback', response_model=FeedbackModel) +async def create_feedback( + request: Request, + form_data: FeedbackForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + feedback = await Feedbacks.insert_new_feedback(user_id=user.id, form_data=form_data, db=db) + if not feedback: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + return feedback + + +@router.get('/feedback/{id}', response_model=FeedbackModel) +async def get_feedback_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + if user.role == 'admin': + feedback = await Feedbacks.get_feedback_by_id(id=id, db=db) + else: + feedback = await Feedbacks.get_feedback_by_id_and_user_id(id=id, user_id=user.id, db=db) + + if not feedback: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + return feedback + + +@router.post('/feedback/{id}', response_model=FeedbackModel) +async def update_feedback_by_id( + id: str, + form_data: FeedbackForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role == 'admin': + feedback = await Feedbacks.update_feedback_by_id(id=id, form_data=form_data, db=db) + else: + feedback = await Feedbacks.update_feedback_by_id_and_user_id(id=id, user_id=user.id, form_data=form_data, db=db) + + if not feedback: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + return feedback + + +@router.delete('/feedback/{id}') +async def delete_feedback_by_id( + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + if user.role == 'admin': + success = await Feedbacks.delete_feedback_by_id(id=id, db=db) + else: + success = await Feedbacks.delete_feedback_by_id_and_user_id(id=id, user_id=user.id, db=db) + + if not success: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + return success diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py new file mode 100644 index 0000000000000000000000000000000000000000..7ca1c2e73fb6d4f27923688ea7f37c706a06c120 --- /dev/null +++ b/backend/open_webui/routers/files.py @@ -0,0 +1,821 @@ +import logging +import os +import uuid +import json +from pathlib import Path +from typing import Optional +from urllib.parse import quote +import asyncio + +from fastapi import ( + BackgroundTasks, + APIRouter, + Depends, + File, + Form, + HTTPException, + Request, + UploadFile, + status, + Query, +) + +from fastapi.responses import FileResponse, StreamingResponse +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import get_async_session, get_async_db_context + +from open_webui.constants import ERROR_MESSAGES +from open_webui.retrieval.vector.async_client import ASYNC_VECTOR_DB_CLIENT + +from open_webui.models.channels import Channels +from open_webui.models.users import Users +from open_webui.models.files import ( + FileForm, + FileListResponse, + FileModel, + FileModelResponse, + Files, +) +from open_webui.models.chats import Chats +from open_webui.models.knowledge import Knowledges +from open_webui.models.groups import Groups +from open_webui.models.access_grants import AccessGrants + + +from open_webui.routers.retrieval import ProcessFileForm, process_file +from open_webui.routers.audio import transcribe + +from open_webui.storage.provider import Storage + + +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL, STORAGE_LOCAL_CACHE, STORAGE_PROVIDER, UPLOAD_DIR +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.misc import strict_match_mime_type +from pydantic import BaseModel + +log = logging.getLogger(__name__) + +router = APIRouter() + + +from open_webui.utils.access_control.files import has_access_to_file + +############################ +# 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: + """Check if a file is likely a text file by reading a chunk and validating UTF-8. + + This catches files whose extensions are mis-mapped by mimetypes/browsers + (e.g. TypeScript .ts → video/mp2t) without maintaining an extension whitelist. + """ + try: + resolved = Storage.get_file(file_path) + with open(resolved, 'rb') as f: + chunk = f.read(chunk_size) + if not chunk: + return False + # Null bytes are a strong indicator of binary content + if b'\x00' in chunk: + return False + chunk.decode('utf-8') + return True + except (UnicodeDecodeError, Exception): + return False + + +def _cleanup_local_cache(file_path: str) -> None: + """Remove the local cached copy of a cloud-stored file after processing.""" + if STORAGE_LOCAL_CACHE or STORAGE_PROVIDER == 'local': + return + try: + local_filename = os.path.basename(file_path) + local_path = os.path.join(UPLOAD_DIR, local_filename) + if os.path.isfile(local_path): + os.remove(local_path) + log.debug(f'Cleaned up local cache: {local_path}') + except OSError as e: + log.warning(f'Failed to clean up local cache for {file_path}: {e}') + + +async def process_uploaded_file( + request, + file, + file_path, + file_item, + file_metadata, + user, + db: Optional[AsyncSession] = None, +): + async def _process_handler(db_session): + try: + content_type = file.content_type + + # Detect mis-labeled text files (e.g. .ts → video/mp2t) + if content_type and content_type.startswith(('image/', 'video/')): + if _is_text_file(file_path): + content_type = 'text/plain' + + if content_type: + stt_supported_content_types = getattr(request.app.state.config, 'STT_SUPPORTED_CONTENT_TYPES', []) + + if strict_match_mime_type(stt_supported_content_types, content_type): + file_path_processed = await asyncio.to_thread(Storage.get_file, file_path) + result = transcribe(request, file_path_processed, file_metadata, user) + + await process_file( + request, + ProcessFileForm(file_id=file_item.id, content=result.get('text', '')), + user=user, + db=db_session, + ) + elif (not content_type.startswith(('image/', 'video/'))) or ( + request.app.state.config.CONTENT_EXTRACTION_ENGINE == 'external' + ): + await process_file( + request, + ProcessFileForm(file_id=file_item.id), + user=user, + db=db_session, + ) + else: + raise Exception(f'File type {content_type} is not supported for processing') + else: + log.info(f'File type {file.content_type} is not provided, but trying to process anyway') + await process_file( + request, + ProcessFileForm(file_id=file_item.id), + user=user, + db=db_session, + ) + + except Exception as e: + log.error(f'Error processing file: {file_item.id}') + await Files.update_file_data_by_id( + file_item.id, + { + 'status': 'failed', + 'error': str(e.detail) if hasattr(e, 'detail') else str(e), + }, + db=db_session, + ) + + try: + if db: + await _process_handler(db) + else: + async with get_async_db_context() as db_session: + await _process_handler(db_session) + finally: + _cleanup_local_cache(file_path) + + +@router.post('/', response_model=FileModelResponse) +async def upload_file( + request: Request, + background_tasks: BackgroundTasks, + file: UploadFile = File(...), + metadata: Optional[dict | str] = Form(None), + process: bool = Query(True), + process_in_background: bool = Query(True), + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + return await upload_file_handler( + request, + file=file, + metadata=metadata, + process=process, + process_in_background=process_in_background, + user=user, + background_tasks=background_tasks, + db=db, + ) + + +async def upload_file_handler( + request: Request, + file: UploadFile = File(...), + metadata: Optional[dict | str] = Form(None), + process: bool = Query(True), + process_in_background: bool = Query(True), + user=Depends(get_verified_user), + background_tasks: Optional[BackgroundTasks] = None, + db: Optional[AsyncSession] = None, +): + log.info(f'file.content_type: {file.content_type} {process}') + + if isinstance(metadata, str): + try: + metadata = json.loads(metadata) + except json.JSONDecodeError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Invalid metadata format'), + ) + file_metadata = metadata if metadata else {} + + try: + unsanitized_filename = file.filename + filename = os.path.basename(unsanitized_filename) + + file_extension = os.path.splitext(filename)[1] + # Remove the leading dot from the file extension and lowercase it + file_extension = file_extension[1:].lower() if file_extension else '' + + if process and request.app.state.config.ALLOWED_FILE_EXTENSIONS: + request.app.state.config.ALLOWED_FILE_EXTENSIONS = [ + ext for ext in request.app.state.config.ALLOWED_FILE_EXTENSIONS if ext + ] + + if file_extension not in request.app.state.config.ALLOWED_FILE_EXTENSIONS: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(f'File type {file_extension} is not allowed'), + ) + + # replace filename with uuid + id = str(uuid.uuid4()) + name = filename + filename = f'{id}_{filename}' + contents, file_path = await asyncio.to_thread( + Storage.upload_file, + file.file, + filename, + { + 'OpenWebUI-User-Email': user.email, + 'OpenWebUI-User-Id': user.id, + 'OpenWebUI-User-Name': user.name, + 'OpenWebUI-File-Id': id, + }, + ) + + file_item = await Files.insert_new_file( + user.id, + FileForm( + **{ + 'id': id, + 'filename': name, + 'path': file_path, + 'data': { + **({'status': 'pending'} if process else {}), + }, + 'meta': { + 'name': name, + 'content_type': (file.content_type if isinstance(file.content_type, str) else None), + 'size': len(contents), + 'data': file_metadata, + }, + } + ), + db=db, + ) + + if 'channel_id' in file_metadata: + channel = await Channels.get_channel_by_id_and_user_id(file_metadata['channel_id'], user.id, db=db) + if channel: + await Channels.add_file_to_channel_by_id(channel.id, file_item.id, user.id, db=db) + + if process: + if background_tasks and process_in_background: + background_tasks.add_task( + process_uploaded_file, + request, + file, + file_path, + file_item, + file_metadata, + user, + ) + return {'status': True, **file_item.model_dump()} + else: + await process_uploaded_file( + request, + file, + file_path, + file_item, + file_metadata, + user, + db=db, + ) + return {'status': True, **file_item.model_dump()} + else: + if file_item: + return file_item + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error uploading file'), + ) + + except HTTPException as e: + raise e + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error uploading file'), + ) + + +############################ +# List Files +############################ + + +PAGE_SIZE = 50 + + +@router.get('/', response_model=FileListResponse) +async def list_files( + user=Depends(get_verified_user), + page: int = Query(1, ge=1, description='Page number (1-indexed)'), + content: bool = Query(True), + db: AsyncSession = Depends(get_async_session), +): + skip = (page - 1) * PAGE_SIZE + user_id = None if (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) else user.id + + result = await Files.get_file_list(user_id=user_id, skip=skip, limit=PAGE_SIZE, db=db) + + if not content: + for file in result.items: + if file.data and 'content' in file.data: + del file.data['content'] + + return result + + +############################ +# Search Files +############################ + + +@router.get('/search', response_model=list[FileModelResponse]) +async def search_files( + filename: str = Query( + ..., + description="Filename pattern to search for. Supports wildcards such as '*.txt'", + ), + content: bool = Query(True), + skip: int = Query(0, ge=0, description='Number of files to skip'), + limit: int = Query(100, ge=1, le=1000, description='Maximum number of files to return'), + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """ + Search for files by filename with support for wildcard patterns. + Uses SQL-based filtering with pagination for better performance. + """ + # Determine user_id: null for admin with bypass (search all), user.id otherwise + user_id = None if (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) else user.id + + # Use optimized database query with pagination + files = await Files.search_files( + user_id=user_id, + filename=filename, + skip=skip, + limit=limit, + db=db, + ) + + if not files: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='No files found matching the pattern.', + ) + + if not content: + for file in files: + if file.data and 'content' in file.data: + del file.data['content'] + + return files + + +############################ +# Delete All Files +############################ + + +@router.delete('/all') +async def delete_all_files(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + result = await Files.delete_all_files(db=db) + if result: + try: + await asyncio.to_thread(Storage.delete_all_files) + await ASYNC_VECTOR_DB_CLIENT.reset() + except Exception as e: + log.exception(e) + log.error('Error deleting files') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error deleting files'), + ) + return {'message': 'All files deleted successfully'} + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error deleting files'), + ) + + +############################ +# Get File By Id +############################ + + +@router.get('/{id}', response_model=Optional[FileModel]) +async def get_file_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + file = await Files.get_file_by_id(id, db=db) + + if not file: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if file.user_id == user.id or user.role == 'admin' or await has_access_to_file(id, 'read', user, db=db): + return file + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +@router.get('/{id}/process/status') +async def get_file_process_status( + id: str, + stream: bool = Query(False), + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + file = await Files.get_file_by_id(id, db=db) + + if not file: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if file.user_id == user.id or user.role == 'admin' or await has_access_to_file(id, 'read', user, db=db): + if stream: + MAX_FILE_PROCESSING_DURATION = 3600 * 2 + + async def event_stream(file_id): + # NOTE: We intentionally do NOT capture the request's db session here. + # Each poll creates its own short-lived session to avoid holding a + # connection for hours. A WebSocket push would be more efficient. + for _ in range(MAX_FILE_PROCESSING_DURATION): + file_item = await Files.get_file_by_id(file_id) # Creates own session + if file_item: + data = file_item.model_dump().get('data', {}) + status = data.get('status') + + if status: + event = {'status': status} + if status == 'failed': + event['error'] = data.get('error') + + yield f'data: {json.dumps(event)}\n\n' + if status in ('completed', 'failed'): + break + else: + # Legacy + break + else: + yield f'data: {json.dumps({"status": "not_found"})}\n\n' + break + + await asyncio.sleep(1) + + return StreamingResponse( + event_stream(file.id), + media_type='text/event-stream', + ) + else: + return {'status': file.data.get('status', 'pending')} + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# Get File Data Content By Id +############################ + + +@router.get('/{id}/data/content') +async def get_file_data_content_by_id( + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + file = await Files.get_file_by_id(id, db=db) + + if not file: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if file.user_id == user.id or user.role == 'admin' or await has_access_to_file(id, 'read', user, db=db): + return {'content': file.data.get('content', '')} + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# Update File Data Content By Id +############################ + + +class ContentForm(BaseModel): + content: str + + +@router.post('/{id}/data/content/update') +async def update_file_data_content_by_id( + request: Request, + id: str, + form_data: ContentForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + file = await Files.get_file_by_id(id, db=db) + + if not file: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if file.user_id == user.id or user.role == 'admin' or await has_access_to_file(id, 'write', user, db=db): + try: + await process_file( + request, + ProcessFileForm(file_id=id, content=form_data.content), + user=user, + db=db, + ) + file = await Files.get_file_by_id(id=id, db=db) + except Exception as e: + log.exception(e) + log.error(f'Error processing file: {file.id}') + + # Propagate content change to all knowledge collections referencing + # this file. Without this the old embeddings remain in the knowledge + # collection and RAG returns both stale and current data (#20558). + knowledges = await Knowledges.get_knowledges_by_file_id(id, db=db) + for knowledge in knowledges: + try: + # Remove old embeddings for this file from the KB collection + await ASYNC_VECTOR_DB_CLIENT.delete(collection_name=knowledge.id, filter={'file_id': id}) + # Re-add from the now-updated file-{file_id} collection + await process_file( + request, + ProcessFileForm(file_id=id, collection_name=knowledge.id), + user=user, + db=db, + ) + except Exception as e: + log.warning(f'Failed to update knowledge {knowledge.id} after content change for file {id}: {e}') + + return {'content': file.data.get('content', '')} + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# Get File Content By Id +############################ + + +@router.get('/{id}/content') +async def get_file_content_by_id( + id: str, + user=Depends(get_verified_user), + attachment: bool = Query(False), + db: AsyncSession = Depends(get_async_session), +): + file = await Files.get_file_by_id(id, db=db) + + if not file: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if file.user_id == user.id or user.role == 'admin' or await has_access_to_file(id, 'read', user, db=db): + try: + file_path = await asyncio.to_thread(Storage.get_file, file.path) + file_path = Path(file_path) + + # Check if the file already exists in the cache + if file_path.is_file(): + # Handle Unicode filenames + filename = file.meta.get('name', file.filename) + encoded_filename = quote(filename) # RFC5987 encoding + + content_type = file.meta.get('content_type') + filename = file.meta.get('name', file.filename) + encoded_filename = quote(filename) + headers = {} + + if attachment: + headers['Content-Disposition'] = f"attachment; filename*=UTF-8''{encoded_filename}" + else: + if content_type == 'application/pdf' or filename.lower().endswith('.pdf'): + headers['Content-Disposition'] = f"inline; filename*=UTF-8''{encoded_filename}" + content_type = 'application/pdf' + elif content_type != 'text/plain': + headers['Content-Disposition'] = f"attachment; filename*=UTF-8''{encoded_filename}" + + return FileResponse(file_path, headers=headers, media_type=content_type) + + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + except HTTPException as e: + raise e + except Exception as e: + log.exception(e) + log.error('Error getting file content') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error getting file content'), + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +@router.get('/{id}/content/html') +async def get_html_file_content_by_id( + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + file = await Files.get_file_by_id(id, db=db) + + if not file: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + file_user = await Users.get_user_by_id(file.user_id, db=db) + if not file_user or file_user.role != 'admin': + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if file.user_id == user.id or user.role == 'admin' or await has_access_to_file(id, 'read', user, db=db): + try: + file_path = await asyncio.to_thread(Storage.get_file, file.path) + file_path = Path(file_path) + + # Check if the file already exists in the cache + if file_path.is_file(): + log.info(f'file_path: {file_path}') + return FileResponse(file_path) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + except HTTPException as e: + raise e + except Exception as e: + log.exception(e) + log.error('Error getting file content') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error getting file content'), + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +@router.get('/{id}/content/{file_name}') +async def get_file_content_by_id( + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + file = await Files.get_file_by_id(id, db=db) + + if not file: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if file.user_id == user.id or user.role == 'admin' or await has_access_to_file(id, 'read', user, db=db): + file_path = file.path + + # Handle Unicode filenames + filename = file.meta.get('name', file.filename) + encoded_filename = quote(filename) # RFC5987 encoding + headers = {'Content-Disposition': f"attachment; filename*=UTF-8''{encoded_filename}"} + + if file_path: + file_path = await asyncio.to_thread(Storage.get_file, file_path) + file_path = Path(file_path) + + # Check if the file already exists in the cache + if file_path.is_file(): + return FileResponse(file_path, headers=headers) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + else: + # File path doesn’t exist, return the content as .txt if possible + file_content = file.data.get('content', '') + file_name = file.filename + + # Create a generator that encodes the file content + def generator(): + yield file_content.encode('utf-8') + + return StreamingResponse( + generator(), + media_type='text/plain', + headers=headers, + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# Delete File By Id +############################ + + +@router.delete('/{id}') +async def delete_file_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + file = await Files.get_file_by_id(id, db=db) + + if not file: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if file.user_id == user.id or user.role == 'admin' or await has_access_to_file(id, 'write', user, db=db): + # Clean up KB associations and embeddings before deleting + knowledges = await Knowledges.get_knowledges_by_file_id(id, db=db) + for knowledge in knowledges: + # Remove KB-file relationship + await Knowledges.remove_file_from_knowledge_by_id(knowledge.id, id, db=db) + # Clean KB embeddings (same logic as /knowledge/{id}/file/remove) + try: + await ASYNC_VECTOR_DB_CLIENT.delete(collection_name=knowledge.id, filter={'file_id': id}) + if file.hash: + await ASYNC_VECTOR_DB_CLIENT.delete(collection_name=knowledge.id, filter={'hash': file.hash}) + except Exception as e: + log.debug(f'KB embedding cleanup for {knowledge.id}: {e}') + + result = await Files.delete_file_by_id(id, db=db) + if result: + try: + await asyncio.to_thread(Storage.delete_file, file.path) + await ASYNC_VECTOR_DB_CLIENT.delete(collection_name=f'file-{id}') + except Exception as e: + log.exception(e) + log.error('Error deleting files') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error deleting files'), + ) + return {'message': 'File deleted successfully'} + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error deleting file'), + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) diff --git a/backend/open_webui/routers/folders.py b/backend/open_webui/routers/folders.py new file mode 100644 index 0000000000000000000000000000000000000000..ebd0c0cb17b1451574c244a7f51f50d71c00c924 --- /dev/null +++ b/backend/open_webui/routers/folders.py @@ -0,0 +1,329 @@ +import logging +import os +import shutil +import uuid +from pathlib import Path +from typing import Optional +from pydantic import BaseModel +import mimetypes + + +from open_webui.models.folders import ( + FolderForm, + FolderUpdateForm, + FolderModel, + FolderNameIdResponse, + Folders, +) +from open_webui.models.chats import Chats +from open_webui.models.files import Files +from open_webui.models.knowledge import Knowledges + + +from open_webui.config import UPLOAD_DIR +from open_webui.constants import ERROR_MESSAGES +from open_webui.internal.db import get_async_session +from sqlalchemy.ext.asyncio import AsyncSession + + +from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status, Request +from fastapi.responses import FileResponse, StreamingResponse + + +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.access_control import has_permission + +log = logging.getLogger(__name__) + + +router = APIRouter() + + +############################ +# Get Folders +############################ + + +@router.get('/', response_model=list[FolderNameIdResponse]) +async def get_folders( + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if request.app.state.config.ENABLE_FOLDERS is False: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + if user.role != 'admin' and not await has_permission( + user.id, + 'features.folders', + request.app.state.config.USER_PERMISSIONS, + db=db, + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + folders = await Folders.get_folders_by_user_id(user.id, db=db) + + # Verify folder data integrity + folder_list = [] + for folder in folders: + if folder.parent_id and not await Folders.get_folder_by_id_and_user_id(folder.parent_id, user.id, db=db): + folder = await Folders.update_folder_parent_id_by_id_and_user_id(folder.id, user.id, None, db=db) + + if folder.data: + if 'files' in folder.data: + valid_files = [] + for file in folder.data['files']: + if file.get('type') == 'file': + if await Files.check_access_by_user_id(file.get('id'), user.id, 'read', db=db): + valid_files.append(file) + elif file.get('type') == 'collection': + if await Knowledges.check_access_by_user_id(file.get('id'), user.id, 'read', db=db): + valid_files.append(file) + else: + valid_files.append(file) + + folder.data['files'] = valid_files + await Folders.update_folder_by_id_and_user_id( + folder.id, user.id, FolderUpdateForm(data=folder.data), db=db + ) + + folder_list.append(FolderNameIdResponse(**folder.model_dump())) + + return folder_list + + +############################ +# Create Folder +############################ + + +@router.post('/') +async def create_folder( + form_data: FolderForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + folder = await Folders.get_folder_by_parent_id_and_user_id_and_name( + form_data.parent_id, user.id, form_data.name, db=db + ) + + if folder: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Folder already exists'), + ) + + try: + folder = await Folders.insert_new_folder(user.id, form_data, form_data.parent_id, db=db) + return folder + except Exception as e: + log.exception(e) + log.error('Error creating folder') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error creating folder'), + ) + + +############################ +# Get Folders By Id +############################ + + +@router.get('/{id}', response_model=Optional[FolderModel]) +async def get_folder_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + folder = await Folders.get_folder_by_id_and_user_id(id, user.id, db=db) + if folder: + return folder + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# Update Folder Name By Id +############################ + + +@router.post('/{id}/update') +async def update_folder_name_by_id( + id: str, + form_data: FolderUpdateForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + folder = await Folders.get_folder_by_id_and_user_id(id, user.id, db=db) + if folder: + if form_data.name is not None: + # Check if folder with same name exists + existing_folder = await Folders.get_folder_by_parent_id_and_user_id_and_name( + folder.parent_id, user.id, form_data.name, db=db + ) + if existing_folder and existing_folder.id != id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Folder already exists'), + ) + + try: + folder = await Folders.update_folder_by_id_and_user_id(id, user.id, form_data, db=db) + return folder + except Exception as e: + log.exception(e) + log.error(f'Error updating folder: {id}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error updating folder'), + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# Update Folder Parent Id By Id +############################ + + +class FolderParentIdForm(BaseModel): + parent_id: Optional[str] = None + + +@router.post('/{id}/update/parent') +async def update_folder_parent_id_by_id( + id: str, + form_data: FolderParentIdForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + folder = await Folders.get_folder_by_id_and_user_id(id, user.id, db=db) + if folder: + existing_folder = await Folders.get_folder_by_parent_id_and_user_id_and_name( + form_data.parent_id, user.id, folder.name, db=db + ) + + if existing_folder: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Folder already exists'), + ) + + try: + folder = await Folders.update_folder_parent_id_by_id_and_user_id(id, user.id, form_data.parent_id, db=db) + return folder + except Exception as e: + log.exception(e) + log.error(f'Error updating folder: {id}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error updating folder'), + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# Update Folder Is Expanded By Id +############################ + + +class FolderIsExpandedForm(BaseModel): + is_expanded: bool + + +@router.post('/{id}/update/expanded') +async def update_folder_is_expanded_by_id( + id: str, + form_data: FolderIsExpandedForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + folder = await Folders.get_folder_by_id_and_user_id(id, user.id, db=db) + if folder: + try: + folder = await Folders.update_folder_is_expanded_by_id_and_user_id( + id, user.id, form_data.is_expanded, db=db + ) + return folder + except Exception as e: + log.exception(e) + log.error(f'Error updating folder: {id}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error updating folder'), + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# Delete Folder By Id +############################ + + +@router.delete('/{id}') +async def delete_folder_by_id( + request: Request, + id: str, + delete_contents: Optional[bool] = True, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if await Chats.count_chats_by_folder_id_and_user_id(id, user.id, db=db): + chat_delete_permission = await has_permission( + user.id, 'chat.delete', request.app.state.config.USER_PERMISSIONS, db=db + ) + if user.role != 'admin' and not chat_delete_permission: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + folders = [] + folders.append(await Folders.get_folder_by_id_and_user_id(id, user.id, db=db)) + while folders: + folder = folders.pop() + if folder: + try: + folder_ids = await Folders.delete_folder_by_id_and_user_id(folder.id, user.id, db=db) + + for folder_id in folder_ids: + if delete_contents: + await Chats.delete_chats_by_user_id_and_folder_id(user.id, folder_id, db=db) + else: + await Chats.move_chats_by_user_id_and_folder_id(user.id, folder_id, None, db=db) + + return True + except Exception as e: + log.exception(e) + log.error(f'Error deleting folder: {id}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error deleting folder'), + ) + finally: + # Get all subfolders + subfolders = await Folders.get_folders_by_parent_id_and_user_id(folder.id, user.id, db=db) + folders.extend(subfolders) + + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) diff --git a/backend/open_webui/routers/functions.py b/backend/open_webui/routers/functions.py new file mode 100644 index 0000000000000000000000000000000000000000..f40cd1ab828aad467c1db838fc753b4acdfd40f3 --- /dev/null +++ b/backend/open_webui/routers/functions.py @@ -0,0 +1,562 @@ +import os +import re + +import logging +import aiohttp +from pathlib import Path +from typing import Optional + +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT +from open_webui.models.functions import ( + FunctionForm, + FunctionModel, + FunctionResponse, + FunctionUserResponse, + FunctionWithValvesModel, + Functions, +) +from open_webui.utils.plugin import ( + load_function_module_by_id, + replace_imports, + get_function_module_from_cache, + resolve_valves_schema_options, +) +from open_webui.config import CACHE_DIR +from open_webui.constants import ERROR_MESSAGES +from fastapi import APIRouter, Depends, HTTPException, Request, status +from open_webui.utils.auth import get_admin_user, get_verified_user +from pydantic import BaseModel, HttpUrl +from open_webui.internal.db import get_async_session +from sqlalchemy.ext.asyncio import AsyncSession + +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. +############################ + + +@router.get('/', response_model=list[FunctionResponse]) +async def get_functions(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + return await Functions.get_functions(db=db) + + +@router.get('/list', response_model=list[FunctionUserResponse]) +async def get_function_list(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + return await Functions.get_function_list(db=db) + + +############################ +# ExportFunctions +############################ + + +@router.get('/export', response_model=list[FunctionModel | FunctionWithValvesModel]) +async def get_functions( + include_valves: bool = False, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + return await Functions.get_functions(include_valves=include_valves, db=db) + + +############################ +# LoadFunctionFromLink +############################ + + +class LoadUrlForm(BaseModel): + url: HttpUrl + + +def github_url_to_raw_url(url: str) -> str: + # Handle 'tree' (folder) URLs (add main.py at the end) + m1 = re.match(r'https://github\.com/([^/]+)/([^/]+)/tree/([^/]+)/(.*)', url) + if m1: + org, repo, branch, path = m1.groups() + return f'https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path.rstrip("/")}/main.py' + + # Handle 'blob' (file) URLs + m2 = re.match(r'https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)', url) + if m2: + org, repo, branch, path = m2.groups() + return f'https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path}' + + # No match; return as-is + return url + + +@router.post('/load/url', response_model=Optional[dict]) +async def load_function_from_url(request: Request, form_data: LoadUrlForm, user=Depends(get_admin_user)): + # NOTE: This is NOT a SSRF vulnerability: + # This endpoint is admin-only (see get_admin_user), meant for *trusted* internal use, + # and does NOT accept untrusted user input. Access is enforced by authentication. + + url = str(form_data.url) + if not url: + raise HTTPException(status_code=400, detail='Please enter a valid URL') + + url = github_url_to_raw_url(url) + url_parts = url.rstrip('/').split('/') + + file_name = url_parts[-1] + function_name = ( + file_name[:-3] + if (file_name.endswith('.py') and (not file_name.startswith(('main.py', 'index.py', '__init__.py')))) + else url_parts[-2] + if len(url_parts) > 1 + else 'function' + ) + + try: + async with aiohttp.ClientSession( + trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + ) as session: + async with session.get( + url, headers={'Content-Type': 'application/json'}, ssl=AIOHTTP_CLIENT_SESSION_SSL + ) as resp: + if resp.status != 200: + raise HTTPException(status_code=resp.status, detail='Failed to fetch the function') + data = await resp.text() + if not data: + raise HTTPException(status_code=400, detail='No data received from the URL') + return { + 'name': function_name, + 'content': data, + } + except Exception as e: + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.DEFAULT(e)) + + +############################ +# SyncFunctions +############################ + + +class SyncFunctionsForm(BaseModel): + functions: list[FunctionWithValvesModel] = [] + + +@router.post('/sync', response_model=list[FunctionWithValvesModel]) +async def sync_functions( + request: Request, + form_data: SyncFunctionsForm, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + try: + for function in form_data.functions: + function.content = replace_imports(function.content) + function_module, function_type, frontmatter = await load_function_module_by_id( + function.id, + content=function.content, + ) + + if hasattr(function_module, 'Valves') and function.valves: + Valves = function_module.Valves + try: + Valves(**{k: v for k, v in function.valves.items() if v is not None}) + except Exception as e: + log.exception(f'Error validating valves for function {function.id}: {e}') + raise e + + return await Functions.sync_functions(user.id, form_data.functions, db=db) + except Exception as e: + log.exception(f'Failed to load a function: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +############################ +# CreateNewFunction +############################ + + +@router.post('/create', response_model=Optional[FunctionResponse]) +async def create_new_function( + request: Request, + form_data: FunctionForm, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + if not form_data.id.isidentifier(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Only alphanumeric characters and underscores are allowed in the id', + ) + + form_data.id = form_data.id.lower() + + function = await Functions.get_function_by_id(form_data.id, db=db) + if function is None: + try: + form_data.content = replace_imports(form_data.content) + function_module, function_type, frontmatter = await load_function_module_by_id( + form_data.id, + content=form_data.content, + ) + form_data.meta.manifest = frontmatter + + FUNCTIONS = request.app.state.FUNCTIONS + FUNCTIONS[form_data.id] = function_module + + function = await Functions.insert_new_function(user.id, function_type, form_data, db=db) + + function_cache_dir = CACHE_DIR / 'functions' / form_data.id + function_cache_dir.mkdir(parents=True, exist_ok=True) + + if function_type == 'filter' and getattr(function_module, 'toggle', None): + await Functions.update_function_metadata_by_id(form_data.id, {'toggle': True}, db=db) + + if function: + return function + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error creating function'), + ) + except Exception as e: + log.exception(f'Failed to create a new function: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ID_TAKEN, + ) + + +############################ +# GetFunctionById +############################ + + +@router.get('/id/{id}', response_model=Optional[FunctionModel]) +async def get_function_by_id(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + function = await Functions.get_function_by_id(id, db=db) + + if function: + return function + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# ToggleFunctionById +############################ + + +@router.post('/id/{id}/toggle', response_model=Optional[FunctionModel]) +async def toggle_function_by_id(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + function = await Functions.get_function_by_id(id, db=db) + if function: + function = await Functions.update_function_by_id(id, {'is_active': not function.is_active}, db=db) + + if function: + return function + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error updating function'), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# ToggleGlobalById +############################ + + +@router.post('/id/{id}/toggle/global', response_model=Optional[FunctionModel]) +async def toggle_global_by_id(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + function = await Functions.get_function_by_id(id, db=db) + if function: + function = await Functions.update_function_by_id(id, {'is_global': not function.is_global}, db=db) + + if function: + return function + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error updating function'), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# UpdateFunctionById +############################ + + +@router.post('/id/{id}/update', response_model=Optional[FunctionModel]) +async def update_function_by_id( + request: Request, + id: str, + form_data: FunctionForm, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + try: + form_data.content = replace_imports(form_data.content) + function_module, function_type, frontmatter = await load_function_module_by_id(id, content=form_data.content) + form_data.meta.manifest = frontmatter + + FUNCTIONS = request.app.state.FUNCTIONS + FUNCTIONS[id] = function_module + + updated = {**form_data.model_dump(exclude={'id'}), 'type': function_type} + log.debug(updated) + + function = await Functions.update_function_by_id(id, updated, db=db) + + if function_type == 'filter' and getattr(function_module, 'toggle', None): + await Functions.update_function_metadata_by_id(id, {'toggle': True}, db=db) + + if function: + return function + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error updating function'), + ) + + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +############################ +# DeleteFunctionById +############################ + + +@router.delete('/id/{id}/delete', response_model=bool) +async def delete_function_by_id( + request: Request, + id: str, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + result = await Functions.delete_function_by_id(id, db=db) + + if result: + FUNCTIONS = request.app.state.FUNCTIONS + if id in FUNCTIONS: + del FUNCTIONS[id] + + return result + + +############################ +# GetFunctionValves +############################ + + +@router.get('/id/{id}/valves', response_model=Optional[dict]) +async def get_function_valves_by_id( + id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session) +): + function = await Functions.get_function_by_id(id, db=db) + if function: + try: + valves = await Functions.get_function_valves_by_id(id, db=db) + return valves + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# GetFunctionValvesSpec +############################ + + +@router.get('/id/{id}/valves/spec', response_model=Optional[dict]) +async def get_function_valves_spec_by_id( + request: Request, + id: str, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + function = await Functions.get_function_by_id(id, db=db) + if function: + function_module, function_type, frontmatter = await get_function_module_from_cache(request, id) + + if hasattr(function_module, 'Valves'): + Valves = function_module.Valves + schema = Valves.schema() + # Resolve dynamic options for select dropdowns + schema = resolve_valves_schema_options(Valves, schema, user) + return schema + return None + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# UpdateFunctionValves +############################ + + +@router.post('/id/{id}/valves/update', response_model=Optional[dict]) +async def update_function_valves_by_id( + request: Request, + id: str, + form_data: dict, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + function = await Functions.get_function_by_id(id, db=db) + if function: + function_module, function_type, frontmatter = await get_function_module_from_cache(request, id) + + if hasattr(function_module, 'Valves'): + Valves = function_module.Valves + + try: + form_data = {k: v for k, v in form_data.items() if v is not None} + valves = Valves(**form_data) + + valves_dict = valves.model_dump(exclude_unset=True) + await Functions.update_function_valves_by_id(id, valves_dict, db=db) + return valves_dict + except Exception as e: + log.exception(f'Error updating function values by id {id}: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# FunctionUserValves +############################ + + +@router.get('/id/{id}/valves/user', response_model=Optional[dict]) +async def get_function_user_valves_by_id( + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + function = await Functions.get_function_by_id(id, db=db) + if function: + try: + user_valves = await Functions.get_user_valves_by_id_and_user_id(id, user.id, db=db) + return user_valves + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +@router.get('/id/{id}/valves/user/spec', response_model=Optional[dict]) +async def get_function_user_valves_spec_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + function = await Functions.get_function_by_id(id, db=db) + if function: + function_module, function_type, frontmatter = await get_function_module_from_cache(request, id) + + if hasattr(function_module, 'UserValves'): + UserValves = function_module.UserValves + schema = UserValves.schema() + # Resolve dynamic options for select dropdowns + schema = resolve_valves_schema_options(UserValves, schema, user) + return schema + return None + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +@router.post('/id/{id}/valves/user/update', response_model=Optional[dict]) +async def update_function_user_valves_by_id( + request: Request, + id: str, + form_data: dict, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + function = await Functions.get_function_by_id(id, db=db) + + if function: + function_module, function_type, frontmatter = await get_function_module_from_cache(request, id) + + if hasattr(function_module, 'UserValves'): + UserValves = function_module.UserValves + + try: + form_data = {k: v for k, v in form_data.items() if v is not None} + user_valves = UserValves(**form_data) + user_valves_dict = user_valves.model_dump(exclude_unset=True) + await Functions.update_user_valves_by_id_and_user_id(id, user.id, user_valves_dict, db=db) + return user_valves_dict + except Exception as e: + log.exception(f'Error updating function user valves by id {id}: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) diff --git a/backend/open_webui/routers/groups.py b/backend/open_webui/routers/groups.py new file mode 100644 index 0000000000000000000000000000000000000000..c45690fc3aeccc488f8ec00fbba145ade7ebafb7 --- /dev/null +++ b/backend/open_webui/routers/groups.py @@ -0,0 +1,278 @@ +import os +from pathlib import Path +from typing import Optional +import logging + +from open_webui.models.users import Users, UserInfoResponse +from open_webui.models.groups import ( + Groups, + GroupForm, + GroupInfoResponse, + GroupUpdateForm, + GroupResponse, + UserIdsForm, +) + +from open_webui.config import CACHE_DIR +from open_webui.constants import ERROR_MESSAGES +from fastapi import APIRouter, Depends, HTTPException, Request, status + +from open_webui.internal.db import get_async_session +from sqlalchemy.ext.asyncio import AsyncSession + +from open_webui.utils.auth import get_admin_user, get_verified_user + +log = logging.getLogger(__name__) + +router = APIRouter() + +############################ +# GetFunctions +############################ + + +@router.get('/', response_model=list[GroupResponse]) +async def get_groups( + share: Optional[bool] = None, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + filter = {} + + # Admins can share to all groups regardless of share setting + if user.role != 'admin': + filter['member_id'] = user.id + if share is not None: + filter['share'] = share + + groups = await Groups.get_groups(filter=filter, db=db) + + return groups + + +############################ +# CreateNewGroup +############################ + + +@router.post('/create', response_model=Optional[GroupResponse]) +async def create_new_group( + form_data: GroupForm, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + try: + group = await Groups.insert_new_group(user.id, form_data, db=db) + if group: + return GroupResponse( + **group.model_dump(), + member_count=await Groups.get_group_member_count_by_id(group.id, db=db), + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error creating group'), + ) + except Exception as e: + log.exception(f'Error creating a new group: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +############################ +# GetGroupById +############################ + + +@router.get('/id/{id}', response_model=Optional[GroupResponse]) +async def get_group_by_id(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + group = await Groups.get_group_by_id(id, db=db) + if group: + return GroupResponse( + **group.model_dump(), + member_count=await Groups.get_group_member_count_by_id(group.id, db=db), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +@router.get('/id/{id}/info', response_model=Optional[GroupInfoResponse]) +async def get_group_info_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + group = await Groups.get_group_by_id(id, db=db) + if group: + return GroupInfoResponse( + **group.model_dump(), + member_count=await Groups.get_group_member_count_by_id(group.id, db=db), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# ExportGroupById +############################ + + +class GroupExportResponse(GroupResponse): + user_ids: list[str] = [] + pass + + +@router.get('/id/{id}/export', response_model=Optional[GroupExportResponse]) +async def export_group_by_id(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + group = await Groups.get_group_by_id(id, db=db) + if group: + return GroupExportResponse( + **group.model_dump(), + member_count=await Groups.get_group_member_count_by_id(group.id, db=db), + user_ids=await Groups.get_group_user_ids_by_id(group.id, db=db), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# GetUsersInGroupById +############################ + + +@router.post('/id/{id}/users', response_model=list[UserInfoResponse]) +async def get_users_in_group(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + try: + users = await Users.get_users_by_group_id(id, db=db) + return users + except Exception as e: + log.exception(f'Error adding users to group {id}: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +############################ +# UpdateGroupById +############################ + + +@router.post('/id/{id}/update', response_model=Optional[GroupResponse]) +async def update_group_by_id( + id: str, + form_data: GroupUpdateForm, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + try: + group = await Groups.update_group_by_id(id, form_data, db=db) + if group: + return GroupResponse( + **group.model_dump(), + member_count=await Groups.get_group_member_count_by_id(group.id, db=db), + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error updating group'), + ) + except Exception as e: + log.exception(f'Error updating group {id}: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +############################ +# AddUserToGroupByUserIdAndGroupId +############################ + + +@router.post('/id/{id}/users/add', response_model=Optional[GroupResponse]) +async def add_user_to_group( + id: str, + form_data: UserIdsForm, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + try: + if form_data.user_ids: + form_data.user_ids = await Users.get_valid_user_ids(form_data.user_ids, db=db) + + group = await Groups.add_users_to_group(id, form_data.user_ids, db=db) + if group: + return GroupResponse( + **group.model_dump(), + member_count=await Groups.get_group_member_count_by_id(group.id, db=db), + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error adding users to group'), + ) + except Exception as e: + log.exception(f'Error adding users to group {id}: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +@router.post('/id/{id}/users/remove', response_model=Optional[GroupResponse]) +async def remove_users_from_group( + id: str, + form_data: UserIdsForm, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + try: + group = await Groups.remove_users_from_group(id, form_data.user_ids, db=db) + if group: + return GroupResponse( + **group.model_dump(), + member_count=await Groups.get_group_member_count_by_id(group.id, db=db), + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error removing users from group'), + ) + except Exception as e: + log.exception(f'Error removing users from group {id}: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +############################ +# DeleteGroupById +############################ + + +@router.delete('/id/{id}/delete', response_model=bool) +async def delete_group_by_id(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + try: + result = await Groups.delete_group_by_id(id, db=db) + if result: + return result + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error deleting group'), + ) + except Exception as e: + log.exception(f'Error deleting group {id}: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) diff --git a/backend/open_webui/routers/images.py b/backend/open_webui/routers/images.py new file mode 100644 index 0000000000000000000000000000000000000000..7ef4938b2e718d4e14a868c0f66f61b7ea21272f --- /dev/null +++ b/backend/open_webui/routers/images.py @@ -0,0 +1,1077 @@ +import asyncio +import base64 +import uuid +import io +import json +import logging +import mimetypes +import re +from pathlib import Path +from typing import Optional + +from urllib.parse import quote +import aiohttp + +from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile +from fastapi.responses import FileResponse + +from open_webui.config import ( + CACHE_DIR, + IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN, + IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN, +) +from open_webui.constants import ERROR_MESSAGES +from open_webui.retrieval.web.utils import validate_url +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, ENABLE_FORWARD_USER_INFO_HEADERS +from open_webui.utils.session_pool import get_session + +from open_webui.models.chats import Chats +from open_webui.routers.files import upload_file_handler, get_file_content_by_id +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.access_control import has_permission +from open_webui.utils.headers import include_user_info_headers +from open_webui.internal.db import get_async_session +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.utils.images.comfyui import ( + ComfyUICreateImageForm, + ComfyUIEditImageForm, + ComfyUIWorkflow, + comfyui_upload_image, + comfyui_create_image, + comfyui_edit_image, +) +from pydantic import BaseModel + +log = logging.getLogger(__name__) + +# An image can lie as easily as it can illuminate. Let what +# is generated here be honest about what it shows. +IMAGE_CACHE_DIR = CACHE_DIR / 'image' / 'generations' +IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True) + +router = APIRouter() + + +async def set_image_model(request: Request, model: str): + log.info(f'Setting image model to {model}') + request.app.state.config.IMAGE_GENERATION_MODEL = model + if request.app.state.config.IMAGE_GENERATION_ENGINE in ['', 'automatic1111']: + api_auth = get_automatic1111_api_auth(request) + + try: + session = await get_session() + async with session.get( + url=f'{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options', + headers={'authorization': api_auth}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + options = await r.json() + if model != options['sd_model_checkpoint']: + options['sd_model_checkpoint'] = model + async with session.post( + url=f'{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options', + json=options, + headers={'authorization': api_auth}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + except Exception as e: + log.debug(f'{e}') + + return request.app.state.config.IMAGE_GENERATION_MODEL + + +async def get_image_model(request): + if request.app.state.config.IMAGE_GENERATION_ENGINE == 'openai': + return ( + request.app.state.config.IMAGE_GENERATION_MODEL + if request.app.state.config.IMAGE_GENERATION_MODEL + else 'dall-e-2' + ) + elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'gemini': + return ( + request.app.state.config.IMAGE_GENERATION_MODEL + if request.app.state.config.IMAGE_GENERATION_MODEL + else 'imagen-3.0-generate-002' + ) + elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'comfyui': + return ( + request.app.state.config.IMAGE_GENERATION_MODEL if request.app.state.config.IMAGE_GENERATION_MODEL else '' + ) + elif ( + request.app.state.config.IMAGE_GENERATION_ENGINE == 'automatic1111' + or request.app.state.config.IMAGE_GENERATION_ENGINE == '' + ): + try: + session = await get_session() + async with session.get( + url=f'{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options', + headers={'authorization': get_automatic1111_api_auth(request)}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + options = await r.json() + return options['sd_model_checkpoint'] + except Exception as e: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e)) + + +class ImagesConfig(BaseModel): + ENABLE_IMAGE_GENERATION: bool + ENABLE_IMAGE_PROMPT_GENERATION: bool + + IMAGE_GENERATION_ENGINE: str + IMAGE_GENERATION_MODEL: str + IMAGE_SIZE: Optional[str] + IMAGE_STEPS: Optional[int] + + IMAGES_OPENAI_API_BASE_URL: str + IMAGES_OPENAI_API_KEY: str + IMAGES_OPENAI_API_VERSION: str + IMAGES_OPENAI_API_PARAMS: Optional[dict | str] + + AUTOMATIC1111_BASE_URL: str + AUTOMATIC1111_API_AUTH: Optional[dict | str] + AUTOMATIC1111_PARAMS: Optional[dict | str] + + COMFYUI_BASE_URL: str + COMFYUI_API_KEY: str + COMFYUI_WORKFLOW: str + COMFYUI_WORKFLOW_NODES: list[dict] + + IMAGES_GEMINI_API_BASE_URL: str + IMAGES_GEMINI_API_KEY: str + IMAGES_GEMINI_ENDPOINT_METHOD: str + + ENABLE_IMAGE_EDIT: bool + IMAGE_EDIT_ENGINE: str + IMAGE_EDIT_MODEL: str + IMAGE_EDIT_SIZE: Optional[str] + + IMAGES_EDIT_OPENAI_API_BASE_URL: str + IMAGES_EDIT_OPENAI_API_KEY: str + IMAGES_EDIT_OPENAI_API_VERSION: str + IMAGES_EDIT_GEMINI_API_BASE_URL: str + IMAGES_EDIT_GEMINI_API_KEY: str + IMAGES_EDIT_COMFYUI_BASE_URL: str + IMAGES_EDIT_COMFYUI_API_KEY: str + IMAGES_EDIT_COMFYUI_WORKFLOW: str + IMAGES_EDIT_COMFYUI_WORKFLOW_NODES: list[dict] + + +@router.get('/config', response_model=ImagesConfig) +async def get_config(request: Request, user=Depends(get_admin_user)): + return { + 'ENABLE_IMAGE_GENERATION': request.app.state.config.ENABLE_IMAGE_GENERATION, + 'ENABLE_IMAGE_PROMPT_GENERATION': request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION, + 'IMAGE_GENERATION_ENGINE': request.app.state.config.IMAGE_GENERATION_ENGINE, + 'IMAGE_GENERATION_MODEL': request.app.state.config.IMAGE_GENERATION_MODEL, + 'IMAGE_SIZE': request.app.state.config.IMAGE_SIZE, + 'IMAGE_STEPS': request.app.state.config.IMAGE_STEPS, + 'IMAGES_OPENAI_API_BASE_URL': request.app.state.config.IMAGES_OPENAI_API_BASE_URL, + 'IMAGES_OPENAI_API_KEY': request.app.state.config.IMAGES_OPENAI_API_KEY, + 'IMAGES_OPENAI_API_VERSION': request.app.state.config.IMAGES_OPENAI_API_VERSION, + 'IMAGES_OPENAI_API_PARAMS': request.app.state.config.IMAGES_OPENAI_API_PARAMS, + 'AUTOMATIC1111_BASE_URL': request.app.state.config.AUTOMATIC1111_BASE_URL, + 'AUTOMATIC1111_API_AUTH': request.app.state.config.AUTOMATIC1111_API_AUTH, + 'AUTOMATIC1111_PARAMS': request.app.state.config.AUTOMATIC1111_PARAMS, + 'COMFYUI_BASE_URL': request.app.state.config.COMFYUI_BASE_URL, + 'COMFYUI_API_KEY': request.app.state.config.COMFYUI_API_KEY, + 'COMFYUI_WORKFLOW': request.app.state.config.COMFYUI_WORKFLOW, + 'COMFYUI_WORKFLOW_NODES': request.app.state.config.COMFYUI_WORKFLOW_NODES, + 'IMAGES_GEMINI_API_BASE_URL': request.app.state.config.IMAGES_GEMINI_API_BASE_URL, + 'IMAGES_GEMINI_API_KEY': request.app.state.config.IMAGES_GEMINI_API_KEY, + 'IMAGES_GEMINI_ENDPOINT_METHOD': request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD, + 'ENABLE_IMAGE_EDIT': request.app.state.config.ENABLE_IMAGE_EDIT, + 'IMAGE_EDIT_ENGINE': request.app.state.config.IMAGE_EDIT_ENGINE, + 'IMAGE_EDIT_MODEL': request.app.state.config.IMAGE_EDIT_MODEL, + 'IMAGE_EDIT_SIZE': request.app.state.config.IMAGE_EDIT_SIZE, + 'IMAGES_EDIT_OPENAI_API_BASE_URL': request.app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL, + 'IMAGES_EDIT_OPENAI_API_KEY': request.app.state.config.IMAGES_EDIT_OPENAI_API_KEY, + 'IMAGES_EDIT_OPENAI_API_VERSION': request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION, + 'IMAGES_EDIT_GEMINI_API_BASE_URL': request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL, + 'IMAGES_EDIT_GEMINI_API_KEY': request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY, + 'IMAGES_EDIT_COMFYUI_BASE_URL': request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL, + 'IMAGES_EDIT_COMFYUI_API_KEY': request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY, + 'IMAGES_EDIT_COMFYUI_WORKFLOW': request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW, + 'IMAGES_EDIT_COMFYUI_WORKFLOW_NODES': request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES, + } + + +@router.post('/config/update') +async def update_config(request: Request, form_data: ImagesConfig, user=Depends(get_admin_user)): + request.app.state.config.ENABLE_IMAGE_GENERATION = form_data.ENABLE_IMAGE_GENERATION + + # Create Image + request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = form_data.ENABLE_IMAGE_PROMPT_GENERATION + + request.app.state.config.IMAGE_GENERATION_ENGINE = form_data.IMAGE_GENERATION_ENGINE + await set_image_model(request, form_data.IMAGE_GENERATION_MODEL) + if form_data.IMAGE_SIZE == 'auto' and not re.match( + IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN, form_data.IMAGE_GENERATION_MODEL + ): + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.INCORRECT_FORMAT( + f' (auto is only allowed with models matching {IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN}).' + ), + ) + + pattern = r'^\d+x\d+$' + if form_data.IMAGE_SIZE == 'auto' or form_data.IMAGE_SIZE == '' or re.match(pattern, form_data.IMAGE_SIZE): + request.app.state.config.IMAGE_SIZE = form_data.IMAGE_SIZE + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.INCORRECT_FORMAT(' (e.g., 512x512).'), + ) + + if form_data.IMAGE_STEPS >= 0: + request.app.state.config.IMAGE_STEPS = form_data.IMAGE_STEPS + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.INCORRECT_FORMAT(' (e.g., 50).'), + ) + + request.app.state.config.IMAGES_OPENAI_API_BASE_URL = form_data.IMAGES_OPENAI_API_BASE_URL + request.app.state.config.IMAGES_OPENAI_API_KEY = form_data.IMAGES_OPENAI_API_KEY + request.app.state.config.IMAGES_OPENAI_API_VERSION = form_data.IMAGES_OPENAI_API_VERSION + request.app.state.config.IMAGES_OPENAI_API_PARAMS = form_data.IMAGES_OPENAI_API_PARAMS + + request.app.state.config.AUTOMATIC1111_BASE_URL = form_data.AUTOMATIC1111_BASE_URL + request.app.state.config.AUTOMATIC1111_API_AUTH = form_data.AUTOMATIC1111_API_AUTH + request.app.state.config.AUTOMATIC1111_PARAMS = form_data.AUTOMATIC1111_PARAMS + + request.app.state.config.COMFYUI_BASE_URL = form_data.COMFYUI_BASE_URL.strip('/') + request.app.state.config.COMFYUI_API_KEY = form_data.COMFYUI_API_KEY + request.app.state.config.COMFYUI_WORKFLOW = form_data.COMFYUI_WORKFLOW + request.app.state.config.COMFYUI_WORKFLOW_NODES = form_data.COMFYUI_WORKFLOW_NODES + + request.app.state.config.IMAGES_GEMINI_API_BASE_URL = form_data.IMAGES_GEMINI_API_BASE_URL + request.app.state.config.IMAGES_GEMINI_API_KEY = form_data.IMAGES_GEMINI_API_KEY + request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD = form_data.IMAGES_GEMINI_ENDPOINT_METHOD + + # Edit Image + request.app.state.config.ENABLE_IMAGE_EDIT = form_data.ENABLE_IMAGE_EDIT + request.app.state.config.IMAGE_EDIT_ENGINE = form_data.IMAGE_EDIT_ENGINE + request.app.state.config.IMAGE_EDIT_MODEL = form_data.IMAGE_EDIT_MODEL + request.app.state.config.IMAGE_EDIT_SIZE = form_data.IMAGE_EDIT_SIZE + + request.app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL = form_data.IMAGES_EDIT_OPENAI_API_BASE_URL + request.app.state.config.IMAGES_EDIT_OPENAI_API_KEY = form_data.IMAGES_EDIT_OPENAI_API_KEY + request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION = form_data.IMAGES_EDIT_OPENAI_API_VERSION + + request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL = form_data.IMAGES_EDIT_GEMINI_API_BASE_URL + request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY = form_data.IMAGES_EDIT_GEMINI_API_KEY + + request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL = form_data.IMAGES_EDIT_COMFYUI_BASE_URL.strip('/') + request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY = form_data.IMAGES_EDIT_COMFYUI_API_KEY + request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW = form_data.IMAGES_EDIT_COMFYUI_WORKFLOW + request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = form_data.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES + + return { + 'ENABLE_IMAGE_GENERATION': request.app.state.config.ENABLE_IMAGE_GENERATION, + 'ENABLE_IMAGE_PROMPT_GENERATION': request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION, + 'IMAGE_GENERATION_ENGINE': request.app.state.config.IMAGE_GENERATION_ENGINE, + 'IMAGE_GENERATION_MODEL': request.app.state.config.IMAGE_GENERATION_MODEL, + 'IMAGE_SIZE': request.app.state.config.IMAGE_SIZE, + 'IMAGE_STEPS': request.app.state.config.IMAGE_STEPS, + 'IMAGES_OPENAI_API_BASE_URL': request.app.state.config.IMAGES_OPENAI_API_BASE_URL, + 'IMAGES_OPENAI_API_KEY': request.app.state.config.IMAGES_OPENAI_API_KEY, + 'IMAGES_OPENAI_API_VERSION': request.app.state.config.IMAGES_OPENAI_API_VERSION, + 'IMAGES_OPENAI_API_PARAMS': request.app.state.config.IMAGES_OPENAI_API_PARAMS, + 'AUTOMATIC1111_BASE_URL': request.app.state.config.AUTOMATIC1111_BASE_URL, + 'AUTOMATIC1111_API_AUTH': request.app.state.config.AUTOMATIC1111_API_AUTH, + 'AUTOMATIC1111_PARAMS': request.app.state.config.AUTOMATIC1111_PARAMS, + 'COMFYUI_BASE_URL': request.app.state.config.COMFYUI_BASE_URL, + 'COMFYUI_API_KEY': request.app.state.config.COMFYUI_API_KEY, + 'COMFYUI_WORKFLOW': request.app.state.config.COMFYUI_WORKFLOW, + 'COMFYUI_WORKFLOW_NODES': request.app.state.config.COMFYUI_WORKFLOW_NODES, + 'IMAGES_GEMINI_API_BASE_URL': request.app.state.config.IMAGES_GEMINI_API_BASE_URL, + 'IMAGES_GEMINI_API_KEY': request.app.state.config.IMAGES_GEMINI_API_KEY, + 'IMAGES_GEMINI_ENDPOINT_METHOD': request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD, + 'ENABLE_IMAGE_EDIT': request.app.state.config.ENABLE_IMAGE_EDIT, + 'IMAGE_EDIT_ENGINE': request.app.state.config.IMAGE_EDIT_ENGINE, + 'IMAGE_EDIT_MODEL': request.app.state.config.IMAGE_EDIT_MODEL, + 'IMAGE_EDIT_SIZE': request.app.state.config.IMAGE_EDIT_SIZE, + 'IMAGES_EDIT_OPENAI_API_BASE_URL': request.app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL, + 'IMAGES_EDIT_OPENAI_API_KEY': request.app.state.config.IMAGES_EDIT_OPENAI_API_KEY, + 'IMAGES_EDIT_OPENAI_API_VERSION': request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION, + 'IMAGES_EDIT_GEMINI_API_BASE_URL': request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL, + 'IMAGES_EDIT_GEMINI_API_KEY': request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY, + 'IMAGES_EDIT_COMFYUI_BASE_URL': request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL, + 'IMAGES_EDIT_COMFYUI_API_KEY': request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY, + 'IMAGES_EDIT_COMFYUI_WORKFLOW': request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW, + 'IMAGES_EDIT_COMFYUI_WORKFLOW_NODES': request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES, + } + + +def get_automatic1111_api_auth(request: Request): + if request.app.state.config.AUTOMATIC1111_API_AUTH is None: + return '' + else: + auth1111_byte_string = request.app.state.config.AUTOMATIC1111_API_AUTH.encode('utf-8') + auth1111_base64_encoded_bytes = base64.b64encode(auth1111_byte_string) + auth1111_base64_encoded_string = auth1111_base64_encoded_bytes.decode('utf-8') + return f'Basic {auth1111_base64_encoded_string}' + + +@router.get('/config/url/verify') +async def verify_url(request: Request, user=Depends(get_admin_user)): + if request.app.state.config.IMAGE_GENERATION_ENGINE == 'automatic1111': + try: + session = await get_session() + async with session.get( + url=f'{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options', + headers={'authorization': get_automatic1111_api_auth(request)}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + return True + except Exception: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL) + elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'comfyui': + headers = None + if request.app.state.config.COMFYUI_API_KEY: + headers = {'Authorization': f'Bearer {request.app.state.config.COMFYUI_API_KEY}'} + try: + session = await get_session() + async with session.get( + url=f'{request.app.state.config.COMFYUI_BASE_URL}/object_info', + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + return True + except Exception: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL) + else: + return True + + +@router.get('/models') +async def get_models(request: Request, user=Depends(get_verified_user)): + try: + if request.app.state.config.IMAGE_GENERATION_ENGINE == 'openai': + return [ + {'id': 'dall-e-2', 'name': 'DALL·E 2'}, + {'id': 'dall-e-3', 'name': 'DALL·E 3'}, + {'id': 'gpt-image-1', 'name': 'GPT-IMAGE 1'}, + {'id': 'gpt-image-1.5', 'name': 'GPT-IMAGE 1.5'}, + ] + elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'gemini': + return [ + {'id': 'imagen-3.0-generate-002', 'name': 'imagen-3.0 generate-002'}, + ] + elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'comfyui': + # TODO - get models from comfyui + headers = {'Authorization': f'Bearer {request.app.state.config.COMFYUI_API_KEY}'} + session = await get_session() + async with session.get( + url=f'{request.app.state.config.COMFYUI_BASE_URL}/object_info', + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + info = await r.json() + + workflow = json.loads(request.app.state.config.COMFYUI_WORKFLOW) + model_node_id = None + + for node in request.app.state.config.COMFYUI_WORKFLOW_NODES: + if node['type'] == 'model': + if node['node_ids']: + model_node_id = node['node_ids'][0] + break + + if model_node_id: + model_list_key = None + + log.info(workflow[model_node_id]['class_type']) + for key in info[workflow[model_node_id]['class_type']]['input']['required']: + if '_name' in key: + model_list_key = key + break + + if model_list_key: + return list( + map( + lambda model: {'id': model, 'name': model}, + info[workflow[model_node_id]['class_type']]['input']['required'][model_list_key][0], + ) + ) + else: + return list( + map( + lambda model: {'id': model, 'name': model}, + info['CheckpointLoaderSimple']['input']['required']['ckpt_name'][0], + ) + ) + elif ( + request.app.state.config.IMAGE_GENERATION_ENGINE == 'automatic1111' + or request.app.state.config.IMAGE_GENERATION_ENGINE == '' + ): + session = await get_session() + async with session.get( + url=f'{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models', + headers={'authorization': get_automatic1111_api_auth(request)}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + models = await r.json() + return list( + map( + lambda model: {'id': model['title'], 'name': model['model_name']}, + models, + ) + ) + except Exception as e: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e)) + + +class CreateImageForm(BaseModel): + model: Optional[str] = None + prompt: str + size: Optional[str] = None + n: int = 1 + steps: Optional[int] = None + negative_prompt: Optional[str] = None + + +GenerateImageForm = CreateImageForm # Alias for backward compatibility + + +async def get_image_data(data: str, headers=None): + try: + if data.startswith('http://') or data.startswith('https://'): + session = await get_session() + async with session.get( + data, + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + content_type = r.headers.get('content-type', '') + if content_type.split('/')[0] == 'image': + return await r.read(), content_type + else: + log.error('Url does not point to an image.') + return None, None + else: + if ',' in data: + header, encoded = data.split(',', 1) + mime_type = header.split(';')[0].lstrip('data:') + img_data = base64.b64decode(encoded) + else: + mime_type = 'image/png' + img_data = base64.b64decode(data) + return img_data, mime_type + except Exception as e: + log.exception(f'Error loading image data: {e}') + return None, None + + +async def upload_image(request, image_data, content_type, metadata, user, db=None): + image_format = mimetypes.guess_extension(content_type) + file = UploadFile( + file=io.BytesIO(image_data), + filename=f'generated-image{image_format}', # will be converted to a unique ID on upload_file + headers={ + 'content-type': content_type, + }, + ) + file_item = await upload_file_handler( + request, + file=file, + metadata=metadata, + process=False, + user=user, + ) + + if file_item and file_item.id: + # If chat_id and message_id are provided in metadata, link the file to the chat message + chat_id = metadata.get('chat_id') + message_id = metadata.get('message_id') + + if chat_id and message_id: + await Chats.insert_chat_files( + chat_id=chat_id, + message_id=message_id, + file_ids=[file_item.id], + user_id=user.id, + db=db, + ) + + url = request.app.url_path_for('get_file_content_by_id', id=file_item.id) + return file_item, url + + +@router.post('/generations') +async def generate_images(request: Request, form_data: CreateImageForm, user=Depends(get_verified_user)): + if not request.app.state.config.ENABLE_IMAGE_GENERATION: + raise HTTPException( + status_code=403, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + if user.role != 'admin' and not await has_permission( + user.id, 'features.image_generation', request.app.state.config.USER_PERMISSIONS + ): + raise HTTPException( + status_code=403, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + return await image_generations(request, form_data, user=user) + + +async def image_generations( + request: Request, + form_data: CreateImageForm, + metadata: Optional[dict] = None, + user=None, +): + # if IMAGE_SIZE = 'auto', default WidthxHeight to the 512x512 default + # This is only relevant when the user has set IMAGE_SIZE to 'auto' with an + # image model other than gpt-image-1, which is warned about on settings save + + size = '512x512' + if request.app.state.config.IMAGE_SIZE and 'x' in request.app.state.config.IMAGE_SIZE: + size = request.app.state.config.IMAGE_SIZE + + if form_data.size and 'x' in form_data.size: + size = form_data.size + + width, height = tuple(map(int, size.split('x'))) + + metadata = metadata or {} + + model = await get_image_model(request) + + try: + if request.app.state.config.IMAGE_GENERATION_ENGINE == 'openai': + headers = { + 'Authorization': f'Bearer {request.app.state.config.IMAGES_OPENAI_API_KEY}', + 'Content-Type': 'application/json', + } + + if ENABLE_FORWARD_USER_INFO_HEADERS: + headers = include_user_info_headers(headers, user) + + url = f'{request.app.state.config.IMAGES_OPENAI_API_BASE_URL}/images/generations' + if request.app.state.config.IMAGES_OPENAI_API_VERSION: + url = f'{url}?api-version={request.app.state.config.IMAGES_OPENAI_API_VERSION}' + + data = { + 'model': model, + 'prompt': form_data.prompt, + 'n': form_data.n, + **( + {'size': form_data.size or request.app.state.config.IMAGE_SIZE} + if (form_data.size or request.app.state.config.IMAGE_SIZE) + else {} + ), + **( + {} + if re.match( + IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN, + request.app.state.config.IMAGE_GENERATION_MODEL, + ) + else {'response_format': 'b64_json'} + ), + **( + {} + if not request.app.state.config.IMAGES_OPENAI_API_PARAMS + else request.app.state.config.IMAGES_OPENAI_API_PARAMS + ), + } + + session = await get_session() + async with session.post( + url=url, + json=data, + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + res = await r.json() + + images = [] + + for image in res['data']: + if image_url := image.get('url', None): + image_data, content_type = await get_image_data( + image_url, + {k: v for k, v in headers.items() if k != 'Content-Type'}, + ) + else: + image_data, content_type = await get_image_data(image['b64_json']) + + _, url = await upload_image(request, image_data, content_type, {**data, **metadata}, user) + images.append({'url': url}) + return images + + elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'gemini': + headers = { + 'Content-Type': 'application/json', + 'x-goog-api-key': request.app.state.config.IMAGES_GEMINI_API_KEY, + } + + data = {} + + if ( + request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD == '' + or request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD == 'predict' + ): + model = f'{model}:predict' + data = { + 'instances': {'prompt': form_data.prompt}, + 'parameters': { + 'sampleCount': form_data.n, + 'outputOptions': {'mimeType': 'image/png'}, + }, + } + + elif request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD == 'generateContent': + model = f'{model}:generateContent' + data = {'contents': [{'parts': [{'text': form_data.prompt}]}]} + + session = await get_session() + async with session.post( + url=f'{request.app.state.config.IMAGES_GEMINI_API_BASE_URL}/models/{model}', + json=data, + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + res = await r.json() + + images = [] + + if model.endswith(':predict'): + for image in res['predictions']: + image_data, content_type = await get_image_data(image['bytesBase64Encoded']) + _, url = await upload_image(request, image_data, content_type, {**data, **metadata}, user) + images.append({'url': url}) + elif model.endswith(':generateContent'): + for image in res['candidates']: + for part in image['content']['parts']: + if part.get('inlineData', {}).get('data'): + image_data, content_type = await get_image_data(part['inlineData']['data']) + _, url = await upload_image( + request, + image_data, + content_type, + {**data, **metadata}, + user, + ) + images.append({'url': url}) + + return images + + elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'comfyui': + data = { + 'prompt': form_data.prompt, + 'width': width, + 'height': height, + 'n': form_data.n, + } + + if request.app.state.config.IMAGE_STEPS is not None or form_data.steps is not None: + data['steps'] = form_data.steps if form_data.steps is not None else request.app.state.config.IMAGE_STEPS + + if form_data.negative_prompt is not None: + data['negative_prompt'] = form_data.negative_prompt + + form_data = ComfyUICreateImageForm( + **{ + 'workflow': ComfyUIWorkflow( + **{ + 'workflow': request.app.state.config.COMFYUI_WORKFLOW, + 'nodes': request.app.state.config.COMFYUI_WORKFLOW_NODES, + } + ), + **data, + } + ) + res = await comfyui_create_image( + model, + form_data, + str(uuid.uuid4()), + request.app.state.config.COMFYUI_BASE_URL, + request.app.state.config.COMFYUI_API_KEY, + ) + log.debug(f'res: {res}') + + images = [] + + for image in res['data']: + headers = None + if request.app.state.config.COMFYUI_API_KEY: + headers = {'Authorization': f'Bearer {request.app.state.config.COMFYUI_API_KEY}'} + + image_data, content_type = await get_image_data(image['url'], headers) + _, url = await upload_image( + request, + image_data, + content_type, + {**form_data.model_dump(exclude_none=True), **metadata}, + user, + ) + images.append({'url': url}) + return images + elif ( + request.app.state.config.IMAGE_GENERATION_ENGINE == 'automatic1111' + or request.app.state.config.IMAGE_GENERATION_ENGINE == '' + ): + if form_data.model: + await set_image_model(request, form_data.model) + + data = { + 'prompt': form_data.prompt, + 'batch_size': form_data.n, + 'width': width, + 'height': height, + } + + if request.app.state.config.IMAGE_STEPS is not None or form_data.steps is not None: + data['steps'] = form_data.steps if form_data.steps is not None else request.app.state.config.IMAGE_STEPS + + if form_data.negative_prompt is not None: + data['negative_prompt'] = form_data.negative_prompt + + if request.app.state.config.AUTOMATIC1111_PARAMS: + data = {**data, **request.app.state.config.AUTOMATIC1111_PARAMS} + + session = await get_session() + async with session.post( + url=f'{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img', + json=data, + headers={'authorization': get_automatic1111_api_auth(request)}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + res = await r.json() + log.debug(f'res: {res}') + + images = [] + + for image in res['images']: + image_data, content_type = await get_image_data(image) + _, url = await upload_image( + request, + image_data, + content_type, + {**data, 'info': res['info'], **metadata}, + user, + ) + images.append({'url': url}) + return images + except Exception as e: + error = e + if isinstance(e, aiohttp.ClientResponseError): + error = e.message + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error)) + + +class EditImageForm(BaseModel): + image: str | list[str] # base64-encoded image(s) or URL(s) + prompt: str + model: Optional[str] = None + size: Optional[str] = None + n: Optional[int] = None + negative_prompt: Optional[str] = None + background: Optional[str] = None + + +@router.post('/edit') +async def image_edits( + request: Request, + form_data: EditImageForm, + metadata: Optional[dict] = None, + user=Depends(get_verified_user), +): + size = None + width, height = None, None + metadata = metadata or {} + + if (request.app.state.config.IMAGE_EDIT_SIZE and 'x' in request.app.state.config.IMAGE_EDIT_SIZE) or ( + form_data.size and 'x' in form_data.size + ): + size = form_data.size if form_data.size else request.app.state.config.IMAGE_EDIT_SIZE + width, height = tuple(map(int, size.split('x'))) + + model = request.app.state.config.IMAGE_EDIT_MODEL if form_data.model is None else form_data.model + + try: + + async def load_url_image(data): + if data.startswith('data:'): + return data + + if data.startswith('http://') or data.startswith('https://'): + # Validate URL to prevent SSRF attacks against local/private networks + validate_url(data) + session = await get_session() + async with session.get(data, ssl=AIOHTTP_CLIENT_SESSION_SSL) as r: + r.raise_for_status() + + image_data = base64.b64encode(await r.read()).decode('utf-8') + return f'data:{r.headers["content-type"]};base64,{image_data}' + + else: + file_id = None + if data.startswith('/api/v1/files'): + file_id = data.split('/api/v1/files/')[1].split('/content')[0] + else: + file_id = data + + file_response = await get_file_content_by_id(file_id, user) + if isinstance(file_response, FileResponse): + file_path = file_response.path + + with open(file_path, 'rb') as f: + file_bytes = f.read() + image_data = base64.b64encode(file_bytes).decode('utf-8') + mime_type, _ = mimetypes.guess_type(file_path) + + return f'data:{mime_type};base64,{image_data}' + return data + + # Load image(s) from URL(s) if necessary + if isinstance(form_data.image, str): + form_data.image = await load_url_image(form_data.image) + elif isinstance(form_data.image, list): + # Load all images in parallel for better performance + form_data.image = list(await asyncio.gather(*[load_url_image(img) for img in form_data.image])) + except Exception as e: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e)) + + def get_image_file_item(base64_string, param_name='image'): + data = base64_string + header, encoded = data.split(',', 1) + mime_type = header.split(';')[0].lstrip('data:') + image_data = base64.b64decode(encoded) + return ( + param_name, + ( + f'{uuid.uuid4()}.png', + io.BytesIO(image_data), + mime_type if mime_type else 'image/png', + ), + ) + + try: + if request.app.state.config.IMAGE_EDIT_ENGINE == 'openai': + headers = { + 'Authorization': f'Bearer {request.app.state.config.IMAGES_EDIT_OPENAI_API_KEY}', + } + + if ENABLE_FORWARD_USER_INFO_HEADERS: + headers = include_user_info_headers(headers, user) + + data = { + 'model': model, + 'prompt': form_data.prompt, + **({'n': form_data.n} if form_data.n else {}), + **({'size': size} if size else {}), + **({'background': form_data.background} if form_data.background else {}), + **( + {} + if re.match( + IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN, + request.app.state.config.IMAGE_EDIT_MODEL, + ) + else {'response_format': 'b64_json'} + ), + } + + files = [] + if isinstance(form_data.image, str): + files = [get_image_file_item(form_data.image)] + elif isinstance(form_data.image, list): + for img in form_data.image: + files.append(get_image_file_item(img, 'image[]')) + + url_search_params = '' + if request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION: + url_search_params += f'?api-version={request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION}' + + # Build multipart form data for aiohttp + form = aiohttp.FormData() + for key, value in data.items(): + if isinstance(value, dict): + form.add_field(key, json.dumps(value)) + else: + form.add_field(key, str(value)) + for param_name, (filename, file_obj, content_type_val) in files: + form.add_field( + param_name, + file_obj, + filename=filename, + content_type=content_type_val, + ) + + session = await get_session() + async with session.post( + url=f'{request.app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL}/images/edits{url_search_params}', + headers=headers, + data=form, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + res = await r.json() + + images = [] + for image in res['data']: + if image_url := image.get('url', None): + image_data, content_type = await get_image_data( + image_url, + {k: v for k, v in headers.items() if k != 'Content-Type'}, + ) + else: + image_data, content_type = await get_image_data(image['b64_json']) + + _, url = await upload_image(request, image_data, content_type, {**data, **metadata}, user) + images.append({'url': url}) + return images + + elif request.app.state.config.IMAGE_EDIT_ENGINE == 'gemini': + headers = { + 'Content-Type': 'application/json', + 'x-goog-api-key': request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY, + } + + model = f'{model}:generateContent' + data = {'contents': [{'parts': [{'text': form_data.prompt}]}]} + + if isinstance(form_data.image, str): + data['contents'][0]['parts'].append( + { + 'inline_data': { + 'mime_type': 'image/png', + 'data': form_data.image.split(',', 1)[1], + } + } + ) + elif isinstance(form_data.image, list): + data['contents'][0]['parts'].extend( + [ + { + 'inline_data': { + 'mime_type': 'image/png', + 'data': image.split(',', 1)[1], + } + } + for image in form_data.image + ] + ) + + session = await get_session() + async with session.post( + url=f'{request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL}/models/{model}', + json=data, + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + res = await r.json() + + images = [] + for image in res['candidates']: + for part in image['content']['parts']: + if part.get('inlineData', {}).get('data'): + image_data, content_type = await get_image_data(part['inlineData']['data']) + _, url = await upload_image( + request, + image_data, + content_type, + {**data, **metadata}, + user, + ) + images.append({'url': url}) + + return images + + elif request.app.state.config.IMAGE_EDIT_ENGINE == 'comfyui': + try: + files = [] + if isinstance(form_data.image, str): + files = [get_image_file_item(form_data.image)] + elif isinstance(form_data.image, list): + for img in form_data.image: + files.append(get_image_file_item(img)) + + # Upload images to ComfyUI and get their names + comfyui_images = [] + for file_item in files: + res = await comfyui_upload_image( + file_item, + request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL, + request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY, + ) + comfyui_images.append(res.get('name', file_item[1][0])) + except Exception as e: + log.debug(f'Error uploading images to ComfyUI: {e}') + raise Exception('Failed to upload images to ComfyUI.') + + data = { + 'image': comfyui_images, + 'prompt': form_data.prompt, + **({'width': width} if width is not None else {}), + **({'height': height} if height is not None else {}), + **({'n': form_data.n} if form_data.n else {}), + } + + form_data = ComfyUIEditImageForm( + **{ + 'workflow': ComfyUIWorkflow( + **{ + 'workflow': request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW, + 'nodes': request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES, + } + ), + **data, + } + ) + res = await comfyui_edit_image( + model, + form_data, + str(uuid.uuid4()), + request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL, + request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY, + ) + log.debug(f'res: {res}') + + image_urls = set() + for image in res['data']: + image_urls.add(image['url']) + image_urls = list(image_urls) + + # Prioritize output type URLs if available + output_type_urls = [url for url in image_urls if 'type=output' in url] + if output_type_urls: + image_urls = output_type_urls + + log.debug(f'Image URLs: {image_urls}') + images = [] + + for image_url in image_urls: + headers = None + if request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY: + headers = {'Authorization': f'Bearer {request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY}'} + + image_data, content_type = await get_image_data(image_url, headers) + _, url = await upload_image( + request, + image_data, + content_type, + {**form_data.model_dump(exclude_none=True), **metadata}, + user, + ) + images.append({'url': url}) + + return images + except Exception as e: + error = e + if isinstance(e, aiohttp.ClientResponseError): + error = e.message + + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error)) diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py new file mode 100644 index 0000000000000000000000000000000000000000..f503169fc0f35e019e3f883bfef30632628609e1 --- /dev/null +++ b/backend/open_webui/routers/knowledge.py @@ -0,0 +1,1103 @@ +from typing import List, Optional +from pydantic import BaseModel +from fastapi import APIRouter, Depends, HTTPException, status, Request, Query +from fastapi.responses import StreamingResponse + +import logging +import io +import zipfile +from urllib.parse import quote + +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import get_async_session +from open_webui.models.groups import Groups +from open_webui.models.knowledge import ( + KnowledgeFileListResponse, + Knowledges, + KnowledgeForm, + KnowledgeResponse, + KnowledgeUserResponse, +) +from open_webui.models.files import Files, FileModel, FileMetadataResponse +from open_webui.retrieval.vector.async_client import ASYNC_VECTOR_DB_CLIENT +from open_webui.routers.retrieval import ( + process_file, + ProcessFileForm, + process_files_batch, + BatchProcessFilesForm, +) +from open_webui.storage.provider import Storage + +from open_webui.constants import ERROR_MESSAGES +from open_webui.utils.auth import get_verified_user, get_admin_user +from open_webui.utils.access_control import has_permission, filter_allowed_access_grants +from open_webui.models.access_grants import AccessGrants + + +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL +from open_webui.models.models import Models, ModelForm + +log = logging.getLogger(__name__) + +router = APIRouter() + +############################ +# getKnowledgeBases +############################ + +PAGE_ITEM_COUNT = 30 + +############################ +# Knowledge Base Embedding +############################ + +# Knowledge that sits unread serves no one. Let what is +# stored here find the ones who need it. +KNOWLEDGE_BASES_COLLECTION = 'knowledge-bases' + + +async def embed_knowledge_base_metadata( + request: Request, + knowledge_base_id: str, + name: str, + description: str, +) -> bool: + """Generate and store embedding for knowledge base.""" + try: + content = f'{name}\n\n{description}' if description else name + embedding = await request.app.state.EMBEDDING_FUNCTION(content) + await ASYNC_VECTOR_DB_CLIENT.upsert( + collection_name=KNOWLEDGE_BASES_COLLECTION, + items=[ + { + 'id': knowledge_base_id, + 'text': content, + 'vector': embedding, + 'metadata': { + 'knowledge_base_id': knowledge_base_id, + }, + } + ], + ) + return True + except Exception as e: + log.error(f'Failed to embed knowledge base {knowledge_base_id}: {e}') + return False + + +async def remove_knowledge_base_metadata_embedding(knowledge_base_id: str) -> bool: + """Remove knowledge base embedding.""" + try: + await ASYNC_VECTOR_DB_CLIENT.delete( + collection_name=KNOWLEDGE_BASES_COLLECTION, + ids=[knowledge_base_id], + ) + return True + except Exception as e: + log.debug(f'Failed to remove embedding for {knowledge_base_id}: {e}') + return False + + +class KnowledgeAccessResponse(KnowledgeUserResponse): + write_access: Optional[bool] = False + + +class KnowledgeAccessListResponse(BaseModel): + items: list[KnowledgeAccessResponse] + total: int + + +@router.get('/', response_model=KnowledgeAccessListResponse) +async def get_knowledge_bases( + page: Optional[int] = 1, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + page = max(page, 1) + limit = PAGE_ITEM_COUNT + skip = (page - 1) * limit + + filter = {} + groups = await Groups.get_groups_by_member_id(user.id, db=db) + user_group_ids = {group.id for group in groups} + + if not user.role == 'admin' or not BYPASS_ADMIN_ACCESS_CONTROL: + if groups: + filter['group_ids'] = [group.id for group in groups] + + filter['user_id'] = user.id + + result = await Knowledges.search_knowledge_bases(user.id, filter=filter, skip=skip, limit=limit, db=db) + + # Batch-fetch writable knowledge IDs in a single query instead of N has_access calls + knowledge_base_ids = [knowledge_base.id for knowledge_base in result.items] + writable_knowledge_base_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type='knowledge', + resource_ids=knowledge_base_ids, + permission='write', + user_group_ids=user_group_ids, + db=db, + ) + + return KnowledgeAccessListResponse( + items=[ + KnowledgeAccessResponse( + **knowledge_base.model_dump(), + write_access=( + user.id == knowledge_base.user_id + or (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or knowledge_base.id in writable_knowledge_base_ids + ), + ) + for knowledge_base in result.items + ], + total=result.total, + ) + + +@router.get('/search', response_model=KnowledgeAccessListResponse) +async def search_knowledge_bases( + query: Optional[str] = None, + view_option: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + page = max(page, 1) + limit = PAGE_ITEM_COUNT + skip = (page - 1) * limit + + filter = {} + if query: + filter['query'] = query + if view_option: + filter['view_option'] = view_option + + groups = await Groups.get_groups_by_member_id(user.id, db=db) + user_group_ids = {group.id for group in groups} + + if not user.role == 'admin' or not BYPASS_ADMIN_ACCESS_CONTROL: + if groups: + filter['group_ids'] = [group.id for group in groups] + + filter['user_id'] = user.id + + result = await Knowledges.search_knowledge_bases(user.id, filter=filter, skip=skip, limit=limit, db=db) + + # Batch-fetch writable knowledge IDs in a single query instead of N has_access calls + knowledge_base_ids = [knowledge_base.id for knowledge_base in result.items] + writable_knowledge_base_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type='knowledge', + resource_ids=knowledge_base_ids, + permission='write', + user_group_ids=user_group_ids, + db=db, + ) + + return KnowledgeAccessListResponse( + items=[ + KnowledgeAccessResponse( + **knowledge_base.model_dump(), + write_access=( + user.id == knowledge_base.user_id + or (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or knowledge_base.id in writable_knowledge_base_ids + ), + ) + for knowledge_base in result.items + ], + total=result.total, + ) + + +@router.get('/search/files', response_model=KnowledgeFileListResponse) +async def search_knowledge_files( + query: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + page = max(page, 1) + limit = PAGE_ITEM_COUNT + skip = (page - 1) * limit + + filter = {} + if query: + filter['query'] = query + + groups = await Groups.get_groups_by_member_id(user.id, db=db) + if groups: + filter['group_ids'] = [group.id for group in groups] + + filter['user_id'] = user.id + + return await Knowledges.search_knowledge_files(filter=filter, skip=skip, limit=limit, db=db) + + +############################ +# CreateNewKnowledge +############################ + + +@router.post('/create', response_model=Optional[KnowledgeResponse]) +async def create_new_knowledge( + request: Request, + 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). + if user.role != 'admin' and not await has_permission( + user.id, 'workspace.knowledge', request.app.state.config.USER_PERMISSIONS + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_knowledge', + ) + + knowledge = await Knowledges.insert_new_knowledge(user.id, form_data) + + if knowledge: + # Embed knowledge base for semantic search + await embed_knowledge_base_metadata( + request, + knowledge.id, + knowledge.name, + knowledge.description, + ) + return knowledge + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.FILE_EXISTS, + ) + + +############################ +# ReindexKnowledgeFiles +############################ + + +@router.post('/reindex', response_model=bool) +async def reindex_knowledge_files( + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin': + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + knowledge_bases = await Knowledges.get_knowledge_bases(db=db) + + log.info(f'Starting reindexing for {len(knowledge_bases)} knowledge bases') + + for knowledge_base in knowledge_bases: + try: + files = await Knowledges.get_files_by_id(knowledge_base.id, db=db) + try: + if await ASYNC_VECTOR_DB_CLIENT.has_collection(collection_name=knowledge_base.id): + await ASYNC_VECTOR_DB_CLIENT.delete_collection(collection_name=knowledge_base.id) + except Exception as e: + log.error(f'Error deleting collection {knowledge_base.id}: {str(e)}') + continue # Skip, don't raise + + failed_files = [] + for file in files: + try: + await process_file( + request, + ProcessFileForm(file_id=file.id, collection_name=knowledge_base.id), + user=user, + db=db, + ) + except Exception as e: + log.error(f'Error processing file {file.filename} (ID: {file.id}): {str(e)}') + failed_files.append({'file_id': file.id, 'error': str(e)}) + continue + + except Exception as e: + log.error(f'Error processing knowledge base {knowledge_base.id}: {str(e)}') + # Don't raise, just continue + continue + + if failed_files: + log.warning(f'Failed to process {len(failed_files)} files in knowledge base {knowledge_base.id}') + for failed in failed_files: + log.warning(f'File ID: {failed["file_id"]}, Error: {failed["error"]}') + + log.info(f'Reindexing completed.') + return True + + +############################ +# ReindexKnowledgeBases +############################ + + +@router.post('/metadata/reindex', response_model=dict) +async def reindex_knowledge_base_metadata_embeddings( + request: Request, + user=Depends(get_admin_user), +): + """Batch embed all existing knowledge bases. Admin only. + + NOTE: We intentionally do NOT use Depends(get_async_session) here. + This endpoint loops through ALL knowledge bases and calls embed_knowledge_base_metadata() + for each one, making N external embedding API calls. Holding a session during + this entire operation would exhaust the connection pool. + """ + knowledge_bases = await Knowledges.get_knowledge_bases() + log.info(f'Reindexing embeddings for {len(knowledge_bases)} knowledge bases') + + success_count = 0 + for kb in knowledge_bases: + if await embed_knowledge_base_metadata(request, kb.id, kb.name, kb.description): + success_count += 1 + + log.info(f'Embedding reindex complete: {success_count}/{len(knowledge_bases)}') + return {'total': len(knowledge_bases), 'success': success_count} + + +############################ +# GetKnowledgeById +############################ + + +class KnowledgeFilesResponse(KnowledgeResponse): + files: Optional[list[FileMetadataResponse]] = None + write_access: Optional[bool] = False + + +@router.get('/{id}', response_model=Optional[KnowledgeFilesResponse]) +async def get_knowledge_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) + + if knowledge: + if ( + user.role == 'admin' + or knowledge.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='read', + db=db, + ) + ): + return KnowledgeFilesResponse( + **knowledge.model_dump(), + write_access=( + user.id == knowledge.user_id + or (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + db=db, + ) + ), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# UpdateKnowledgeById +############################ + + +@router.post('/{id}/update', response_model=Optional[KnowledgeFilesResponse]) +async def update_knowledge_by_id( + request: Request, + id: str, + 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). + knowledge = await Knowledges.get_knowledge_by_id(id=id) + if not knowledge: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + # Is the user the original creator, in a group with write access, or an admin + if ( + knowledge.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_knowledge', + ) + + knowledge = await Knowledges.update_knowledge_by_id(id=id, form_data=form_data) + if knowledge: + # Re-embed knowledge base for semantic search + await embed_knowledge_base_metadata( + request, + knowledge.id, + knowledge.name, + knowledge.description, + ) + return KnowledgeFilesResponse( + **knowledge.model_dump(), + files=await Knowledges.get_file_metadatas_by_id(knowledge.id), + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ID_TAKEN, + ) + + +############################ +# UpdateKnowledgeAccessById +############################ + + +class KnowledgeAccessGrantsForm(BaseModel): + access_grants: list[dict] + + +@router.post('/{id}/access/update', response_model=Optional[KnowledgeFilesResponse]) +async def update_knowledge_access_by_id( + request: Request, + id: str, + form_data: KnowledgeAccessGrantsForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) + if not knowledge: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + knowledge.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_knowledge', + ) + + knowledge.access_grants = await AccessGrants.set_access_grants('knowledge', id, form_data.access_grants, db=db) + + return KnowledgeFilesResponse( + **knowledge.model_dump(), + files=await Knowledges.get_file_metadatas_by_id(id, db=db), + ) + + +############################ +# GetKnowledgeFilesById +############################ + + +@router.get('/{id}/files', response_model=KnowledgeFileListResponse) +async def get_knowledge_files_by_id( + id: str, + query: Optional[str] = None, + view_option: Optional[str] = None, + order_by: Optional[str] = None, + direction: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) + if not knowledge: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if not ( + user.role == 'admin' + or knowledge.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='read', + db=db, + ) + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + page = max(page, 1) + + limit = 30 + skip = (page - 1) * limit + + filter = {} + if query: + filter['query'] = query + if view_option: + filter['view_option'] = view_option + if order_by: + filter['order_by'] = order_by + if direction: + filter['direction'] = direction + + return await Knowledges.search_files_by_id(id, user.id, filter=filter, skip=skip, limit=limit, db=db) + + +############################ +# AddFileToKnowledge +############################ + + +class KnowledgeFileIdForm(BaseModel): + file_id: str + + +@router.post('/{id}/file/add', response_model=Optional[KnowledgeFilesResponse]) +async def add_file_to_knowledge_by_id( + request: Request, + id: str, + form_data: KnowledgeFileIdForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) + if not knowledge: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + knowledge.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + file = await Files.get_file_by_id(form_data.file_id, db=db) + if not file: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + if not file.data: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.FILE_NOT_PROCESSED, + ) + + # Add content to the vector database + try: + await process_file( + request, + ProcessFileForm(file_id=form_data.file_id, collection_name=id), + user=user, + db=db, + ) + + # Add file to knowledge base + await Knowledges.add_file_to_knowledge_by_id(knowledge_id=id, file_id=form_data.file_id, user_id=user.id, db=db) + except Exception as e: + log.debug(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + + if knowledge: + return KnowledgeFilesResponse( + **knowledge.model_dump(), + files=await Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +@router.post('/{id}/file/update', response_model=Optional[KnowledgeFilesResponse]) +async def update_file_from_knowledge_by_id( + request: Request, + id: str, + form_data: KnowledgeFileIdForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) + if not knowledge: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + knowledge.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + file = await Files.get_file_by_id(form_data.file_id, db=db) + if not file: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + # Validate the file actually belongs to this knowledge base + if not await Knowledges.has_file(knowledge_id=id, file_id=form_data.file_id, db=db): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + # Remove content from the vector database + await ASYNC_VECTOR_DB_CLIENT.delete(collection_name=knowledge.id, filter={'file_id': form_data.file_id}) + + # Add content to the vector database + try: + await process_file( + request, + ProcessFileForm(file_id=form_data.file_id, collection_name=id), + user=user, + db=db, + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + + if knowledge: + return KnowledgeFilesResponse( + **knowledge.model_dump(), + files=await Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# RemoveFileFromKnowledge +############################ + + +@router.post('/{id}/file/remove', response_model=Optional[KnowledgeFilesResponse]) +async def remove_file_from_knowledge_by_id( + id: str, + form_data: KnowledgeFileIdForm, + delete_file: bool = Query(True), + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) + if not knowledge: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + knowledge.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + file = await Files.get_file_by_id(form_data.file_id, db=db) + if not file: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + # Validate the file actually belongs to this knowledge base + if not await Knowledges.has_file(knowledge_id=id, file_id=form_data.file_id, db=db): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + await Knowledges.remove_file_from_knowledge_by_id(knowledge_id=id, file_id=form_data.file_id, db=db) + + # Remove content from the vector database + try: + await ASYNC_VECTOR_DB_CLIENT.delete( + collection_name=knowledge.id, filter={'file_id': form_data.file_id} + ) # Remove by file_id first + + await ASYNC_VECTOR_DB_CLIENT.delete( + collection_name=knowledge.id, filter={'hash': file.hash} + ) # Remove by hash as well in case of duplicates + except Exception as e: + log.debug('This was most likely caused by bypassing embedding processing') + log.debug(e) + pass + + # Only the file owner or an admin may permanently delete the underlying + # file. Collaborators with KB write access can unlink a file from the + # knowledge base but must not be able to destroy files they do not own, + # as the same file may be referenced by other KBs and chats. + if delete_file and (file.user_id == user.id or user.role == 'admin'): + try: + # Remove the file's collection from vector database + file_collection = f'file-{form_data.file_id}' + if await ASYNC_VECTOR_DB_CLIENT.has_collection(collection_name=file_collection): + await ASYNC_VECTOR_DB_CLIENT.delete_collection(collection_name=file_collection) + except Exception as e: + log.debug('This was most likely caused by bypassing embedding processing') + log.debug(e) + pass + + # Delete file from database + await Files.delete_file_by_id(form_data.file_id, db=db) + + if knowledge: + return KnowledgeFilesResponse( + **knowledge.model_dump(), + files=await Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# DeleteKnowledgeById +############################ + + +@router.delete('/{id}/delete', response_model=bool) +async def delete_knowledge_by_id( + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) + if not knowledge: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + knowledge.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + log.info(f'Deleting knowledge base: {id} (name: {knowledge.name})') + + # Get all models + models = await Models.get_all_models(db=db) + log.info(f'Found {len(models)} models to check for knowledge base {id}') + + # Update models that reference this knowledge base + for model in models: + if model.meta and hasattr(model.meta, 'knowledge'): + knowledge_list = model.meta.knowledge or [] + # Filter out the deleted knowledge base + updated_knowledge = [k for k in knowledge_list if k.get('id') != id] + + # If the knowledge list changed, update the model + if len(updated_knowledge) != len(knowledge_list): + log.info(f'Updating model {model.id} to remove knowledge base {id}') + model.meta.knowledge = updated_knowledge + model_form = ModelForm(**model.model_dump()) + await Models.update_model_by_id(model.id, model_form, db=db) + + # Clean up vector DB + try: + await ASYNC_VECTOR_DB_CLIENT.delete_collection(collection_name=id) + except Exception as e: + log.debug(e) + pass + + # Remove knowledge base embedding + await remove_knowledge_base_metadata_embedding(id) + + result = await Knowledges.delete_knowledge_by_id(id=id, db=db) + return result + + +############################ +# ResetKnowledgeById +############################ + + +@router.post('/{id}/reset', response_model=Optional[KnowledgeResponse]) +async def reset_knowledge_by_id( + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) + if not knowledge: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + knowledge.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + try: + await ASYNC_VECTOR_DB_CLIENT.delete_collection(collection_name=id) + except Exception as e: + log.debug(e) + pass + + knowledge = await Knowledges.reset_knowledge_by_id(id=id, db=db) + return knowledge + + +############################ +# AddFilesToKnowledge +############################ + + +@router.post('/{id}/files/batch/add', response_model=Optional[KnowledgeFilesResponse]) +async def add_files_to_knowledge_batch( + request: Request, + id: str, + form_data: list[KnowledgeFileIdForm], + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """ + Add multiple files to a knowledge base + """ + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) + if not knowledge: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + knowledge.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + # Batch-fetch all files to avoid N+1 queries + log.info(f'files/batch/add - {len(form_data)} files') + file_ids = [form.file_id for form in form_data] + files = await Files.get_files_by_ids(file_ids, db=db) + + # Verify all requested files were found + found_ids = {file.id for file in files} + missing_ids = [fid for fid in file_ids if fid not in found_ids] + if missing_ids: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f'File {missing_ids[0]} not found', + ) + + # Process files + try: + result = await process_files_batch( + request=request, + form_data=BatchProcessFilesForm(files=files, collection_name=id), + user=user, + db=db, + ) + except Exception as e: + log.error(f'add_files_to_knowledge_batch: Exception occurred: {e}', exc_info=True) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + + # Only add files that were successfully processed + successful_file_ids = [r.file_id for r in result.results if r.status == 'completed'] + for file_id in successful_file_ids: + await Knowledges.add_file_to_knowledge_by_id(knowledge_id=id, file_id=file_id, user_id=user.id, db=db) + + # If there were any errors, include them in the response + if result.errors: + error_details = [f'{err.file_id}: {err.error}' for err in result.errors] + return KnowledgeFilesResponse( + **knowledge.model_dump(), + files=await Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), + warnings={ + 'message': 'Some files failed to process', + 'errors': error_details, + }, + ) + + return KnowledgeFilesResponse( + **knowledge.model_dump(), + files=await Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), + ) + + +############################ +# ExportKnowledgeById +############################ + + +@router.get('/{id}/export') +async def export_knowledge_by_id(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + """ + Export a knowledge base as a zip file containing .txt files. + Admin only. + """ + + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) + if not knowledge: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + files = await Knowledges.get_files_by_id(id, db=db) + + # Create zip file in memory + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf: + for file in files: + content = file.data.get('content', '') if file.data else '' + if content: + # Use original filename with .txt extension + filename = file.filename + if not filename.endswith('.txt'): + filename = f'{filename}.txt' + zf.writestr(filename, content) + + zip_buffer.seek(0) + + # Sanitize knowledge name for filename + # ASCII-safe fallback for the basic filename parameter (latin-1 safe) + safe_name = ''.join(c if c.isascii() and (c.isalnum() or c in ' -_') else '_' for c in knowledge.name) + zip_filename = f'{safe_name}.zip' + + # Use RFC 5987 filename* for non-ASCII names so the browser gets the real name + quoted_name = quote(f'{knowledge.name}.zip') + content_disposition = f'attachment; filename="{zip_filename}"; filename*=UTF-8\'\'{quoted_name}' + + return StreamingResponse( + zip_buffer, + media_type='application/zip', + headers={'Content-Disposition': content_disposition}, + ) diff --git a/backend/open_webui/routers/memories.py b/backend/open_webui/routers/memories.py new file mode 100644 index 0000000000000000000000000000000000000000..6522118258a7b17c2ad6d0bee1608a97545b8f62 --- /dev/null +++ b/backend/open_webui/routers/memories.py @@ -0,0 +1,356 @@ +from fastapi import APIRouter, Depends, HTTPException, Request, status +from pydantic import BaseModel +import logging +import asyncio +from typing import Optional + +from open_webui.models.memories import Memories, MemoryModel +from open_webui.retrieval.vector.async_client import ASYNC_VECTOR_DB_CLIENT +from open_webui.utils.auth import get_verified_user +from open_webui.internal.db import get_async_session +from sqlalchemy.ext.asyncio import AsyncSession + +from open_webui.utils.access_control import has_permission +from open_webui.constants import ERROR_MESSAGES + +log = logging.getLogger(__name__) + +router = APIRouter() + + +############################ +# GetMemories +# Let what is remembered here spare someone the cost +# of learning it twice. +############################ + + +@router.get('/', response_model=list[MemoryModel]) +async def get_memories( + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if not request.app.state.config.ENABLE_MEMORIES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + return await Memories.get_memories_by_user_id(user.id, db=db) + + +############################ +# AddMemory +############################ + + +class AddMemoryForm(BaseModel): + content: str + + +class MemoryUpdateModel(BaseModel): + content: Optional[str] = None + + +@router.post('/add', response_model=Optional[MemoryModel]) +async def add_memory( + request: Request, + form_data: AddMemoryForm, + user=Depends(get_verified_user), +): + # NOTE: We intentionally do NOT use Depends(get_async_session) here. + # Database operations (insert_new_memory) manage their own short-lived sessions. + # This prevents holding a connection during EMBEDDING_FUNCTION() + # which makes external embedding API calls (1-5+ seconds). + if not request.app.state.config.ENABLE_MEMORIES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + memory = await Memories.insert_new_memory(user.id, form_data.content) + + vector = await request.app.state.EMBEDDING_FUNCTION(memory.content, user=user) + + await ASYNC_VECTOR_DB_CLIENT.upsert( + collection_name=f'user-memory-{user.id}', + items=[ + { + 'id': memory.id, + 'text': memory.content, + 'vector': vector, + 'metadata': {'created_at': memory.created_at}, + } + ], + ) + + return memory + + +############################ +# QueryMemory +############################ + + +class QueryMemoryForm(BaseModel): + content: str + k: Optional[int] = 1 + + +@router.post('/query') +async def query_memory( + request: Request, + 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). + if not request.app.state.config.ENABLE_MEMORIES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + memories = await Memories.get_memories_by_user_id(user.id) + if not memories: + raise HTTPException(status_code=404, detail='No memories found for user') + + vector = await request.app.state.EMBEDDING_FUNCTION(form_data.content, user=user) + + results = await ASYNC_VECTOR_DB_CLIENT.search( + collection_name=f'user-memory-{user.id}', + vectors=[vector], + 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). + relevance_threshold = getattr(request.app.state.config, '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 + + filtered_ids = [] + filtered_docs = [] + filtered_metas = [] + filtered_dists = [] + + for idx, score in enumerate(results.distances[0]): + if score >= relevance_threshold: + if results.ids and results.ids[0]: + filtered_ids.append(results.ids[0][idx]) + if results.documents and results.documents[0]: + filtered_docs.append(results.documents[0][idx]) + if results.metadatas and results.metadatas[0]: + filtered_metas.append(results.metadatas[0][idx]) + filtered_dists.append(score) + + results = SearchResult( + ids=[filtered_ids] if filtered_ids else [[]], + documents=[filtered_docs] if filtered_docs else [[]], + metadatas=[filtered_metas] if filtered_metas else [[]], + distances=[filtered_dists] if filtered_dists else [[]], + ) + + return results + + +############################ +# ResetMemoryFromVectorDB +############################ +@router.post('/reset', response_model=bool) +async def reset_memory_from_vector_db( + request: Request, + user=Depends(get_verified_user), +): + """Reset user's memory vector embeddings. + + CRITICAL: We intentionally do NOT use Depends(get_async_session) here. + This endpoint generates embeddings for ALL user memories in parallel using + asyncio.gather(). A user with 100 memories would trigger 100 embedding API + calls simultaneously. With a session held, this could block a connection + for MINUTES, completely exhausting the connection pool. + """ + if not request.app.state.config.ENABLE_MEMORIES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + await ASYNC_VECTOR_DB_CLIENT.delete_collection(f'user-memory-{user.id}') + + memories = await Memories.get_memories_by_user_id(user.id) + + # Generate vectors in parallel + vectors = await asyncio.gather( + *[request.app.state.EMBEDDING_FUNCTION(memory.content, user=user) for memory in memories] + ) + + await ASYNC_VECTOR_DB_CLIENT.upsert( + collection_name=f'user-memory-{user.id}', + items=[ + { + 'id': memory.id, + 'text': memory.content, + 'vector': vectors[idx], + 'metadata': { + 'created_at': memory.created_at, + 'updated_at': memory.updated_at, + }, + } + for idx, memory in enumerate(memories) + ], + ) + + return True + + +############################ +# DeleteMemoriesByUserId +############################ + + +@router.delete('/delete/user', response_model=bool) +async def delete_memory_by_user_id( + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if not request.app.state.config.ENABLE_MEMORIES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + result = await Memories.delete_memories_by_user_id(user.id, db=db) + + if result: + try: + await ASYNC_VECTOR_DB_CLIENT.delete_collection(f'user-memory-{user.id}') + except Exception as e: + log.error(e) + return True + + return False + + +############################ +# UpdateMemoryById +############################ + + +@router.post('/{memory_id}/update', response_model=Optional[MemoryModel]) +async def update_memory_by_id( + memory_id: str, + request: Request, + 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). + if not request.app.state.config.ENABLE_MEMORIES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + memory = await Memories.update_memory_by_id_and_user_id(memory_id, user.id, form_data.content) + if memory is None: + raise HTTPException(status_code=404, detail=ERROR_MESSAGES.NOT_FOUND) + + if form_data.content is not None: + vector = await request.app.state.EMBEDDING_FUNCTION(memory.content, user=user) + + await ASYNC_VECTOR_DB_CLIENT.upsert( + collection_name=f'user-memory-{user.id}', + items=[ + { + 'id': memory.id, + 'text': memory.content, + 'vector': vector, + 'metadata': { + 'created_at': memory.created_at, + 'updated_at': memory.updated_at, + }, + } + ], + ) + + return memory + + +############################ +# DeleteMemoryById +############################ + + +@router.delete('/{memory_id}', response_model=bool) +async def delete_memory_by_id( + memory_id: str, + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if not request.app.state.config.ENABLE_MEMORIES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + result = await Memories.delete_memory_by_id_and_user_id(memory_id, user.id, db=db) + + if result: + await ASYNC_VECTOR_DB_CLIENT.delete(collection_name=f'user-memory-{user.id}', ids=[memory_id]) + return True + + return False diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py new file mode 100644 index 0000000000000000000000000000000000000000..510a0d3d29c3825f14d2840a93e98e914fe5cf14 --- /dev/null +++ b/backend/open_webui/routers/models.py @@ -0,0 +1,711 @@ +from typing import Optional +import io +import base64 +import json +import asyncio +import logging +import posixpath +from urllib.parse import unquote + +from open_webui.models.groups import Groups +from open_webui.models.models import ( + ModelForm, + ModelMeta, + ModelModel, + ModelParams, + ModelResponse, + ModelListResponse, + ModelAccessListResponse, + ModelAccessResponse, + Models, +) +from open_webui.models.access_grants import AccessGrants + +from pydantic import BaseModel +from open_webui.constants import ERROR_MESSAGES +from fastapi import ( + APIRouter, + Depends, + HTTPException, + Request, + status, + Response, +) +from fastapi.responses import RedirectResponse, StreamingResponse + + +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.access_control import has_permission, filter_allowed_access_grants +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL +from open_webui.internal.db import get_async_session +from sqlalchemy.ext.asyncio import AsyncSession + +log = logging.getLogger(__name__) + +router = APIRouter() + + +def _safe_static_redirect_path(url: str) -> Optional[str]: + """ + If url is a same-origin static asset path, return a normalized path safe for + RedirectResponse Location. Otherwise None (caller should fall back to default). + Rejects traversal (..), encoded dots, query/fragment, and non-/static targets. + """ + if not url or not isinstance(url, str): + return None + path = url.split('?', 1)[0].split('#', 1)[0].strip() + for _ in range(2): + decoded = unquote(path) + if decoded == path: + break + path = decoded + if '\x00' in path or '\\' in path: + return None + if not path.startswith('/'): + return None + normalized = posixpath.normpath(path) + if normalized in ('.', '/'): + return None + if not (normalized == '/static' or normalized.startswith('/static/')): + return None + if normalized == '/static': + return '/static/' + return normalized + + +def is_valid_model_id(model_id: str) -> bool: + return model_id and len(model_id) <= 256 + + +########################### +# GetModels +# Let each model here be judged by what it does and not +# by what it claims. The house deserves honest servants. +########################### + + +PAGE_ITEM_COUNT = 30 + + +@router.get('/list', response_model=ModelAccessListResponse) # do NOT use "/" as path, conflicts with main.py +async def get_models( + query: Optional[str] = None, + view_option: Optional[str] = None, + tag: Optional[str] = None, + order_by: Optional[str] = None, + direction: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + limit = PAGE_ITEM_COUNT + + page = max(1, page) + skip = (page - 1) * limit + + filter = {} + if query: + filter['query'] = query + if view_option: + filter['view_option'] = view_option + if tag: + filter['tag'] = tag + if order_by: + filter['order_by'] = order_by + if direction: + filter['direction'] = direction + + # Pre-fetch user group IDs once - used for both filter and write_access check + groups = await Groups.get_groups_by_member_id(user.id, db=db) + user_group_ids = {group.id for group in groups} + + if not user.role == 'admin' or not BYPASS_ADMIN_ACCESS_CONTROL: + if groups: + filter['group_ids'] = [group.id for group in groups] + + filter['user_id'] = user.id + + result = await Models.search_models(user.id, filter=filter, skip=skip, limit=limit, db=db) + + # Batch-fetch writable model IDs in a single query instead of N has_access calls + model_ids = [model.id for model in result.items] + writable_model_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type='model', + resource_ids=model_ids, + permission='write', + user_group_ids=user_group_ids, + db=db, + ) + + # Strip profile_image_url from meta — images are served via /model/profile/image. + items = [] + for model in result.items: + data = model.model_dump() + if data.get('meta'): + data['meta'].pop('profile_image_url', None) + items.append( + ModelAccessResponse( + **data, + write_access=( + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == model.user_id + or model.id in writable_model_ids + ), + ) + ) + + return ModelAccessListResponse( + items=items, + total=result.total, + ) + + +########################### +# GetBaseModels +########################### + + +@router.get('/base', response_model=list[ModelResponse]) +async def get_base_models(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + return await Models.get_base_models(db=db) + + +########################### +# GetModelTags +########################### + + +@router.get('/tags', response_model=list[str]) +async def get_model_tags(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + tags = await Models.get_all_tags( + user_id=user.id, + is_admin=(user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL), + db=db, + ) + return sorted(tags) + + +############################ +# CreateNewModel +############################ + + +@router.post('/create', response_model=Optional[ModelModel]) +async def create_new_model( + request: Request, + form_data: ModelForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, 'workspace.models', request.app.state.config.USER_PERMISSIONS, db=db + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + model = await Models.get_model_by_id(form_data.id, db=db) + if model: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.MODEL_ID_TAKEN, + ) + + if not is_valid_model_id(form_data.id): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.MODEL_ID_TOO_LONG, + ) + + else: + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_models', + ) + + model = await Models.insert_new_model(form_data, user.id, db=db) + if model: + return model + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + +############################ +# ExportModels +############################ + + +@router.get('/export', response_model=list[ModelModel]) +async def export_models( + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, + 'workspace.models_export', + request.app.state.config.USER_PERMISSIONS, + db=db, + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: + return await Models.get_models(db=db) + else: + return await Models.get_models_by_user_id(user.id, db=db) + + +############################ +# ImportModels +############################ + + +class ModelsImportForm(BaseModel): + models: list[dict] + + +@router.post('/import', response_model=bool) +async def import_models( + request: Request, + user=Depends(get_verified_user), + form_data: ModelsImportForm = (...), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, + 'workspace.models_import', + request.app.state.config.USER_PERMISSIONS, + db=db, + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + try: + data = form_data.models + if isinstance(data, list): + # Batch-fetch all existing models in one query to avoid N+1 + model_ids = [ + model_data.get('id') + for model_data in data + if model_data.get('id') and is_valid_model_id(model_data.get('id')) + ] + existing_models = { + model.id: model for model in (await Models.get_models_by_ids(model_ids, db=db) if model_ids else []) + } + + # Batch-resolve write permissions in one query instead of + # per-model has_access calls (N+1 avoidance). + existing_model_ids = list(existing_models.keys()) + if user.role != 'admin' and existing_model_ids: + groups = await Groups.get_groups_by_member_id(user.id, db=db) + user_group_ids = {group.id for group in groups} + writable_model_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type='model', + resource_ids=existing_model_ids, + permission='write', + user_group_ids=user_group_ids, + db=db, + ) + else: + writable_model_ids = set(existing_model_ids) + + for model_data in data: + model_id = model_data.get('id') + + if model_id and is_valid_model_id(model_id): + existing_model = existing_models.get(model_id) + if existing_model: + # Enforce ownership/write-access before allowing overwrite + if ( + user.role != 'admin' + and existing_model.user_id != user.id + and model_id not in writable_model_ids + ): + log.warning( + 'import_models: user %s skipped model %s (no write access)', + user.id, + model_id, + ) + continue + + # Update existing model + model_data['meta'] = model_data.get('meta', {}) + model_data['params'] = model_data.get('params', {}) + + updated_model = ModelForm(**{**existing_model.model_dump(), **model_data}) + # Only filter access_grants when explicitly provided + # in the payload to avoid altering existing ACLs on + # metadata-only imports. + if 'access_grants' in model_data: + updated_model.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + updated_model.access_grants, + 'sharing.public_models', + ) + await Models.update_model_by_id(model_id, updated_model, db=db) + else: + # Insert new model + model_data['meta'] = model_data.get('meta', {}) + model_data['params'] = model_data.get('params', {}) + new_model = ModelForm(**model_data) + new_model.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + new_model.access_grants, + 'sharing.public_models', + ) + await Models.insert_new_model(user_id=user.id, form_data=new_model, db=db) + return True + else: + raise HTTPException(status_code=400, detail='Invalid JSON format') + except Exception as e: + log.exception(e) + raise HTTPException(status_code=500, detail=str(e)) + + +############################ +# SyncModels +############################ + + +class SyncModelsForm(BaseModel): + models: list[ModelModel] = [] + + +@router.post('/sync', response_model=list[ModelModel]) +async def sync_models( + request: Request, + form_data: SyncModelsForm, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + return await Models.sync_models(user.id, form_data.models, db=db) + + +########################### +# GetModelById +########################### + + +class ModelIdForm(BaseModel): + id: str + + +# Note: We're not using the typical url path param here, but instead using a query parameter to allow '/' in the id +@router.get('/model', response_model=Optional[ModelAccessResponse]) +async def get_model_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + model = await Models.get_model_by_id(id, db=db) + if model: + if ( + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or model.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='model', + resource_id=model.id, + permission='read', + db=db, + ) + ): + return ModelAccessResponse( + **model.model_dump(), + write_access=( + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == model.user_id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='model', + resource_id=model.id, + permission='write', + db=db, + ) + ), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +########################### +# GetModelById +########################### + + +@router.get('/model/profile/image') +async def get_model_profile_image( + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + model_meta = await Models.get_model_meta_by_id(id, db=db) + + if model_meta: + meta, updated_at = model_meta + profile_image_url = (meta or {}).get('profile_image_url') + + if profile_image_url: + if profile_image_url.startswith('http'): + return Response( + status_code=status.HTTP_302_FOUND, + headers={'Location': profile_image_url}, + ) + elif profile_image_url.startswith('data:image'): + try: + header, base64_data = profile_image_url.split(',', 1) + image_data = base64.b64decode(base64_data) + image_buffer = io.BytesIO(image_data) + media_type = header.split(';')[0].lstrip('data:') + + headers = {'Content-Disposition': 'inline'} + if updated_at: + headers['ETag'] = f'"{updated_at}"' + + return StreamingResponse( + image_buffer, + media_type=media_type, + headers=headers, + ) + except Exception: + pass + else: + safe_static = _safe_static_redirect_path(profile_image_url) + if safe_static: + return RedirectResponse( + url=safe_static, + status_code=status.HTTP_302_FOUND, + ) + + return RedirectResponse( + url='/static/favicon.png', + status_code=status.HTTP_302_FOUND, + ) + + +############################ +# ToggleModelById +############################ + + +@router.post('/model/toggle', response_model=Optional[ModelResponse]) +async def toggle_model_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + model = await Models.get_model_by_id(id, db=db) + if model: + if ( + user.role == 'admin' + or model.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='model', + resource_id=model.id, + permission='write', + db=db, + ) + ): + model = await Models.toggle_model_by_id(id, db=db) + + if model: + return model + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error updating function'), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# UpdateModelById +############################ + + +@router.post('/model/update', response_model=Optional[ModelModel]) +async def update_model_by_id( + request: Request, + form_data: ModelForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + model = await Models.get_model_by_id(form_data.id, db=db) + if not model: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + model.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='model', + resource_id=model.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_models', + ) + + model = await Models.update_model_by_id(form_data.id, ModelForm(**form_data.model_dump()), db=db) + return model + + +############################ +# UpdateModelAccessById +############################ + + +class ModelAccessGrantsForm(BaseModel): + id: str + name: Optional[str] = None + access_grants: list[dict] + + +@router.post('/model/access/update', response_model=Optional[ModelModel]) +async def update_model_access_by_id( + request: Request, + form_data: ModelAccessGrantsForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + model = await Models.get_model_by_id(form_data.id, db=db) + + # Non-preset models (e.g. direct Ollama/OpenAI models) may not have a DB + # entry yet. Create a minimal one so access grants can be stored. + if not model: + if user.role != 'admin': + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + model = await Models.insert_new_model( + ModelForm( + id=form_data.id, + name=form_data.name or form_data.id, + meta=ModelMeta(), + params=ModelParams(), + ), + user.id, + db=db, + ) + if not model: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DEFAULT('Error creating model entry'), + ) + + if ( + model.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='model', + resource_id=model.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_models', + ) + + await AccessGrants.set_access_grants('model', form_data.id, form_data.access_grants, db=db) + + await Models.update_model_updated_at_by_id(form_data.id, db=db) + + return await Models.get_model_by_id(form_data.id, db=db) + + +############################ +# DeleteModelById +############################ + + +@router.post('/model/delete', response_model=bool) +async def delete_model_by_id( + form_data: ModelIdForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + model = await Models.get_model_by_id(form_data.id, db=db) + if not model: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + user.role != 'admin' + and model.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='model', + resource_id=model.id, + permission='write', + db=db, + ) + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + result = await Models.delete_model_by_id(form_data.id, db=db) + return result + + +@router.delete('/delete/all', response_model=bool) +async def delete_all_models(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + result = await Models.delete_all_models(db=db) + return result diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py new file mode 100644 index 0000000000000000000000000000000000000000..4fbdd09993465b9eb264dda8567922236ef662df --- /dev/null +++ b/backend/open_webui/routers/notes.py @@ -0,0 +1,488 @@ +import json +import logging +from typing import Optional + + +from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks +from pydantic import BaseModel + +from open_webui.socket.main import sio + +from open_webui.models.groups import Groups +from open_webui.models.users import Users, UserResponse +from open_webui.models.notes import ( + NoteListResponse, + Notes, + NoteModel, + NoteForm, + NoteUserResponse, +) + +from open_webui.config import ( + BYPASS_ADMIN_ACCESS_CONTROL, + ENABLE_ADMIN_CHAT_ACCESS, + ENABLE_ADMIN_EXPORT, +) +from open_webui.constants import ERROR_MESSAGES + + +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.access_control import ( + has_permission, + has_public_read_access_grant, + has_public_write_access_grant, + filter_allowed_access_grants, +) +from open_webui.models.access_grants import AccessGrants +from open_webui.internal.db import get_async_session +from sqlalchemy.ext.asyncio import AsyncSession + +log = logging.getLogger(__name__) + +router = APIRouter() + + +def _truncate_note_data(data: Optional[dict], max_length: int = 1000) -> Optional[dict]: + if not data: + return data + md = (data.get('content') or {}).get('md') or '' + return {'content': {'md': md[:max_length]}} + + +############################ +# GetNotes +############################ + + +class NoteItemResponse(BaseModel): + id: str + title: str + data: Optional[dict] + is_pinned: Optional[bool] = False + updated_at: int + created_at: int + user: Optional[UserResponse] = None + + +@router.get('/', response_model=list[NoteItemResponse]) +async def get_notes( + request: Request, + page: Optional[int] = None, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + limit = None + skip = None + if page is not None: + limit = 60 + skip = (page - 1) * limit + + notes = await Notes.get_notes_by_user_id(user.id, 'read', skip=skip, limit=limit, db=db) + if not notes: + return [] + + user_ids = list(set(note.user_id for note in notes)) + users = {user.id: user for user in await Users.get_users_by_user_ids(user_ids, db=db)} + + return [ + NoteUserResponse( + **{ + **note.model_dump(), + 'data': _truncate_note_data(note.data), + 'user': UserResponse(**users[note.user_id].model_dump()), + } + ) + for note in notes + if note.user_id in users + ] + + +############################ +# GetPinnedNotes +############################ + + +@router.get('/pinned', response_model=list[NoteItemResponse]) +async def get_pinned_notes( + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + notes = await Notes.get_pinned_notes_by_user_id(user.id, 'read', db=db) + if not notes: + return [] + + user_ids = list(set(note.user_id for note in notes)) + users = {user.id: user for user in await Users.get_users_by_user_ids(user_ids, db=db)} + + return [ + NoteUserResponse( + **{ + **note.model_dump(), + 'data': _truncate_note_data(note.data), + 'user': UserResponse(**users[note.user_id].model_dump()), + } + ) + for note in notes + if note.user_id in users + ] + + +@router.get('/search', response_model=NoteListResponse) +async def search_notes( + request: Request, + query: Optional[str] = None, + view_option: Optional[str] = None, + permission: Optional[str] = None, + order_by: Optional[str] = None, + direction: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + limit = None + skip = None + if page is not None: + limit = 60 + skip = (page - 1) * limit + + filter = {} + if query: + filter['query'] = query + if view_option: + filter['view_option'] = view_option + if permission: + filter['permission'] = permission + if order_by: + filter['order_by'] = order_by + if direction: + filter['direction'] = direction + + if not user.role == 'admin' or not BYPASS_ADMIN_ACCESS_CONTROL: + groups = await Groups.get_groups_by_member_id(user.id, db=db) + if groups: + filter['group_ids'] = [group.id for group in groups] + + filter['user_id'] = user.id + + result = await Notes.search_notes(user.id, filter, skip=skip, limit=limit, db=db) + for note in result.items: + note.data = _truncate_note_data(note.data) + return result + + +############################ +# CreateNewNote +############################ + + +@router.post('/create', response_model=Optional[NoteModel]) +async def create_new_note( + request: Request, + form_data: NoteForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_notes', + db=db, + ) + + try: + note = await Notes.insert_new_note(user.id, form_data, db=db) + return note + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# GetNoteById +############################ + + +class NoteResponse(NoteModel): + write_access: bool = False + + +@router.get('/{id}', response_model=Optional[NoteResponse]) +async def get_note_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + note = await Notes.get_note_by_id(id, db=db) + if not note: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if user.role != 'admin' and ( + user.id != note.user_id + and ( + not await AccessGrants.has_access( + user_id=user.id, + resource_type='note', + resource_id=note.id, + permission='read', + db=db, + ) + ) + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + write_access = ( + user.role == 'admin' + or (user.id == note.user_id) + or await AccessGrants.has_access( + user_id=user.id, + resource_type='note', + resource_id=note.id, + permission='write', + db=db, + ) + or has_public_write_access_grant(note.access_grants) + ) + + return NoteResponse(**note.model_dump(), write_access=write_access) + + +############################ +# UpdateNoteById +############################ + + +@router.post('/{id}/update', response_model=Optional[NoteModel]) +async def update_note_by_id( + request: Request, + id: str, + form_data: NoteForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + note = await Notes.get_note_by_id(id, db=db) + if not note: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if user.role != 'admin' and ( + user.id != note.user_id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='note', + resource_id=note.id, + permission='write', + db=db, + ) + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_notes', + db=db, + ) + + try: + note = await Notes.update_note_by_id(id, form_data, db=db) + await sio.emit( + 'note-events', + note.model_dump(), + to=f'note:{note.id}', + ) + + return note + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# UpdateNoteAccessById +############################ + + +class NoteAccessGrantsForm(BaseModel): + access_grants: list[dict] + + +@router.post('/{id}/access/update', response_model=Optional[NoteModel]) +async def update_note_access_by_id( + request: Request, + id: str, + form_data: NoteAccessGrantsForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + note = await Notes.get_note_by_id(id, db=db) + if not note: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if user.role != 'admin' and ( + user.id != note.user_id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='note', + resource_id=note.id, + permission='write', + db=db, + ) + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_notes', + ) + + await AccessGrants.set_access_grants('note', id, form_data.access_grants, db=db) + + return await Notes.get_note_by_id(id, db=db) + + +############################ +# PinNoteById +############################ + + +@router.post('/{id}/pin', response_model=Optional[NoteModel]) +async def pin_note_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + note = await Notes.get_note_by_id(id, db=db) + if not note: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if user.role != 'admin' and ( + user.id != note.user_id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='note', + resource_id=note.id, + permission='read', + db=db, + ) + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + note = await Notes.toggle_note_pinned_by_id(id, db=db) + return note + + +############################ +# DeleteNoteById +############################ + + +@router.delete('/{id}/delete', response_model=bool) +async def delete_note_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + note = await Notes.get_note_by_id(id, db=db) + if not note: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if user.role != 'admin' and ( + user.id != note.user_id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='note', + resource_id=note.id, + permission='write', + db=db, + ) + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + try: + note = await Notes.delete_note_by_id(id, db=db) + return True + except Exception as e: + log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py new file mode 100644 index 0000000000000000000000000000000000000000..8311fee5d45f799e31d9ce1c4ba50549720fbdcc --- /dev/null +++ b/backend/open_webui/routers/ollama.py @@ -0,0 +1,1695 @@ +# TODO: Implement a more intelligent load balancing mechanism for distributing requests among multiple backend instances. +# Current implementation uses a simple round-robin approach (random.choice). Consider incorporating algorithms like weighted round-robin, +# least connections, or least response time for better resource utilization and performance optimization. + +import asyncio +import json +import logging +import os +import random +import re +import time +from datetime import datetime + +from typing import Optional, Union +from urllib.parse import urlparse +import aiohttp +from aiocache import cached + + +from open_webui.utils.headers import include_user_info_headers +from open_webui.models.chats import Chats +from open_webui.models.users import UserModel + +from open_webui.env import ( + ENABLE_FORWARD_USER_INFO_HEADERS, + FORWARD_SESSION_INFO_HEADER_CHAT_ID, +) + +from fastapi import ( + Depends, + FastAPI, + File, + HTTPException, + Request, + UploadFile, + APIRouter, +) +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse +from pydantic import BaseModel, ConfigDict, validator + +from sqlalchemy.ext.asyncio import AsyncSession + +from open_webui.internal.db import get_async_session + + +from open_webui.models.models import Models +from open_webui.models.access_grants import AccessGrants +from open_webui.models.groups import Groups +from open_webui.utils.access_control import check_model_access +from open_webui.utils.misc import ( + calculate_sha256, +) +from open_webui.utils.session_pool import ( + cleanup_response, + get_session, + stream_wrapper, +) +from open_webui.utils.payload import ( + apply_model_params_to_body_ollama, + apply_model_params_to_body_openai, + apply_system_prompt_to_body, +) +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.config import ( + UPLOAD_DIR, +) +from open_webui.env import ( + ENV, + MODELS_CACHE_TTL, + AIOHTTP_CLIENT_SESSION_SSL, + AIOHTTP_CLIENT_TIMEOUT, + AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST, + BYPASS_MODEL_ACCESS_CONTROL, +) +from open_webui.constants import ERROR_MESSAGES + +log = logging.getLogger(__name__) + + +########################################## +# +# Utility functions +# Let what runs locally be trusted, and let no weight +# be loaded without serving the one who waits for the answer. +# +########################################## + +# Headers that become stale after aiohttp auto-decompresses the upstream +# response body. Forwarding them verbatim causes desktop / programmatic +# clients to attempt decompression of an already-decoded payload, resulting +# in ZlibError. See https://github.com/aio-libs/aiohttp/issues/4462. +_STRIP_PROXY_HEADERS = frozenset({'Content-Encoding', 'Content-Length', 'Transfer-Encoding'}) + + +def _clean_proxy_headers(raw_headers) -> dict: + """Return a copy of *raw_headers* with stale encoding headers removed.""" + return {k: v for k, v in raw_headers.items() if k not in _STRIP_PROXY_HEADERS} + + +async def send_get_request(url, key=None, user: UserModel = None): + timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) + try: + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + headers = { + 'Content-Type': 'application/json', + **({'Authorization': f'Bearer {key}'} if key else {}), + } + + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) + + async with session.get( + url, + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + return await response.json() + except Exception as e: + # Handle connection error here + log.error(f'Connection error: {e}') + return None + + +async def send_request( + url: str, + method: str = 'POST', + *, + payload: Optional[Union[str, bytes]] = None, + key: Optional[str] = None, + user: UserModel = None, + stream: bool = False, + content_type: Optional[str] = None, + metadata: Optional[dict] = None, +): + r = None + streaming = False + try: + session = await get_session() + + headers = { + 'Content-Type': 'application/json', + **({'Authorization': f'Bearer {key}'} if key else {}), + } + + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) + if metadata and metadata.get('chat_id'): + headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = metadata.get('chat_id') + + r = await session.request( + method, + url, + data=payload, + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) + + if not r.ok: + try: + res = await r.json() + if 'error' in res: + raise HTTPException(status_code=r.status, detail=res['error']) + except HTTPException: + raise + except Exception as e: + log.error(f'Failed to parse error response: {e}') + raise HTTPException( + status_code=r.status, + detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR, + ) + + r.raise_for_status() + + if stream: + response_headers = _clean_proxy_headers(r.headers) + if content_type: + response_headers['Content-Type'] = content_type + + streaming = True + return StreamingResponse( + stream_wrapper(r), + status_code=r.status, + headers=response_headers, + ) + else: + try: + return await r.json() + except Exception: + return None + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=r.status if r else 500, + detail=f'Ollama: {e}' if str(e) else ERROR_MESSAGES.SERVER_CONNECTION_ERROR, + ) + finally: + if not streaming: + await cleanup_response(r) + + +def get_api_key(idx, url, configs): + parsed_url = urlparse(url) + base_url = f'{parsed_url.scheme}://{parsed_url.netloc}' + return configs.get(str(idx), configs.get(base_url, {})).get('key', None) # Legacy support + + +########################################## +# +# API routes +# +########################################## + +router = APIRouter() + + +@router.head('/') +@router.get('/') +async def get_status(): + return {'status': True} + + +class ConnectionVerificationForm(BaseModel): + url: str + key: Optional[str] = None + + +@router.post('/verify') +async def verify_connection(form_data: ConnectionVerificationForm, user=Depends(get_admin_user)): + url = form_data.url + key = form_data.key + + async with aiohttp.ClientSession( + trust_env=True, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST), + ) as session: + try: + headers = { + **({'Authorization': f'Bearer {key}'} if key else {}), + } + + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) + + async with session.get( + f'{url}/api/version', + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + if r.status != 200: + detail = f'HTTP Error: {r.status}' + res = await r.json() + + if 'error' in res: + detail = f'External Error: {res["error"]}' + raise Exception(detail) + + data = await r.json() + return data + except aiohttp.ClientError as e: + log.exception(f'Client error: {str(e)}') + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR) + except Exception as e: + log.exception(f'Unexpected error: {e}') + error_detail = f'Unexpected error: {str(e)}' + raise HTTPException(status_code=500, detail=error_detail) + + +@router.get('/config') +async def get_config(request: Request, user=Depends(get_admin_user)): + return { + 'ENABLE_OLLAMA_API': request.app.state.config.ENABLE_OLLAMA_API, + 'OLLAMA_BASE_URLS': request.app.state.config.OLLAMA_BASE_URLS, + 'OLLAMA_API_CONFIGS': request.app.state.config.OLLAMA_API_CONFIGS, + } + + +class OllamaConfigForm(BaseModel): + ENABLE_OLLAMA_API: Optional[bool] = None + OLLAMA_BASE_URLS: list[str] + OLLAMA_API_CONFIGS: dict + + +@router.post('/config/update') +async def update_config(request: Request, form_data: OllamaConfigForm, user=Depends(get_admin_user)): + request.app.state.config.ENABLE_OLLAMA_API = form_data.ENABLE_OLLAMA_API + + request.app.state.config.OLLAMA_BASE_URLS = form_data.OLLAMA_BASE_URLS + request.app.state.config.OLLAMA_API_CONFIGS = form_data.OLLAMA_API_CONFIGS + + # Remove the API configs that are not in the API URLS + keys = list(map(str, range(len(request.app.state.config.OLLAMA_BASE_URLS)))) + request.app.state.config.OLLAMA_API_CONFIGS = { + key: value for key, value in request.app.state.config.OLLAMA_API_CONFIGS.items() if key in keys + } + + return { + 'ENABLE_OLLAMA_API': request.app.state.config.ENABLE_OLLAMA_API, + 'OLLAMA_BASE_URLS': request.app.state.config.OLLAMA_BASE_URLS, + 'OLLAMA_API_CONFIGS': request.app.state.config.OLLAMA_API_CONFIGS, + } + + +def merge_ollama_models_lists(model_lists): + merged_models = {} + + for idx, model_list in enumerate(model_lists): + if model_list is not None: + for model in model_list: + id = model.get('model') + if id is not None: + if id not in merged_models: + model['urls'] = [idx] + merged_models[id] = model + else: + merged_models[id]['urls'].append(idx) + + return list(merged_models.values()) + + +@cached( + ttl=MODELS_CACHE_TTL, + key=lambda _, user: f'ollama_all_models_{user.id}' if user else 'ollama_all_models', +) +async def get_all_models(request: Request, user: UserModel = None): + log.info('get_all_models()') + if request.app.state.config.ENABLE_OLLAMA_API: + request_tasks = [] + for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS): + if (str(idx) not in request.app.state.config.OLLAMA_API_CONFIGS) and ( + url not in request.app.state.config.OLLAMA_API_CONFIGS # Legacy support + ): + request_tasks.append(send_get_request(f'{url}/api/tags', user=user)) + else: + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(idx), + request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support + ) + + enable = api_config.get('enable', True) + key = api_config.get('key', None) + + if enable: + request_tasks.append(send_get_request(f'{url}/api/tags', key, user=user)) + else: + request_tasks.append(asyncio.ensure_future(asyncio.sleep(0, None))) + + responses = await asyncio.gather(*request_tasks) + + for idx, response in enumerate(responses): + if response: + url = request.app.state.config.OLLAMA_BASE_URLS[idx] + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(idx), + request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support + ) + + connection_type = api_config.get('connection_type', 'local') + + prefix_id = api_config.get('prefix_id', None) + tags = api_config.get('tags', []) + model_ids = api_config.get('model_ids', []) + + if len(model_ids) != 0 and 'models' in response: + response['models'] = list( + filter( + lambda model: model['model'] in model_ids, + response['models'], + ) + ) + + for model in response.get('models', []): + if prefix_id: + model['model'] = f'{prefix_id}.{model["model"]}' + + if tags: + model['tags'] = tags + + if connection_type: + model['connection_type'] = connection_type + + models = { + 'models': merge_ollama_models_lists( + map( + lambda response: response.get('models', []) if response else None, + responses, + ) + ) + } + + try: + loaded_models = await get_ollama_loaded_models(request, user=user) + expires_map = {m['model']: m['expires_at'] for m in loaded_models['models'] if 'expires_at' in m} + + for m in models['models']: + if m['model'] in expires_map: + # Parse ISO8601 datetime with offset, get unix timestamp as int + dt = datetime.fromisoformat(expires_map[m['model']]) + m['expires_at'] = int(dt.timestamp()) + except Exception as e: + log.debug(f'Failed to get loaded models: {e}') + + else: + models = {'models': []} + + request.app.state.OLLAMA_MODELS = {model['model']: model for model in models['models']} + return models + + +async def get_filtered_models(models, user, db=None): + # Filter models based on user access control + model_ids = [model['model'] for model in models.get('models', [])] + model_infos = {model_info.id: model_info for model_info in await Models.get_models_by_ids(model_ids, db=db)} + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} + + # Batch-fetch accessible resource IDs in a single query instead of N has_access calls + accessible_model_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type='model', + resource_ids=list(model_infos.keys()), + permission='read', + user_group_ids=user_group_ids, + db=db, + ) + + filtered_models = [] + for model in models.get('models', []): + model_info = model_infos.get(model['model']) + if model_info: + if user.id == model_info.user_id or model_info.id in accessible_model_ids: + filtered_models.append(model) + return filtered_models + + +@router.get('/api/tags') +@router.get('/api/tags/{url_idx}') +async def get_ollama_tags(request: Request, url_idx: Optional[int] = None, user=Depends(get_verified_user)): + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + models = [] + + if url_idx is None: + models = await get_all_models(request, user=user) + else: + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) + models = await send_request(f'{url}/api/tags', 'GET', key=key, user=user) + + if user.role == 'user' and not BYPASS_MODEL_ACCESS_CONTROL: + models['models'] = await get_filtered_models(models, user) + + return models + + +@router.get('/api/ps') +async def get_ollama_loaded_models(request: Request, user=Depends(get_admin_user)): + """ + List models that are currently loaded into Ollama memory, and which node they are loaded on. + """ + if request.app.state.config.ENABLE_OLLAMA_API: + request_tasks = [] + for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS): + if (str(idx) not in request.app.state.config.OLLAMA_API_CONFIGS) and ( + url not in request.app.state.config.OLLAMA_API_CONFIGS # Legacy support + ): + request_tasks.append(send_get_request(f'{url}/api/ps', user=user)) + else: + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(idx), + request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support + ) + + enable = api_config.get('enable', True) + key = api_config.get('key', None) + + if enable: + request_tasks.append(send_get_request(f'{url}/api/ps', key, user=user)) + else: + request_tasks.append(asyncio.ensure_future(asyncio.sleep(0, None))) + + responses = await asyncio.gather(*request_tasks) + + for idx, response in enumerate(responses): + if response: + url = request.app.state.config.OLLAMA_BASE_URLS[idx] + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(idx), + request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support + ) + + prefix_id = api_config.get('prefix_id', None) + + for model in response.get('models', []): + if prefix_id: + model['model'] = f'{prefix_id}.{model["model"]}' + + models = { + 'models': merge_ollama_models_lists( + map( + lambda response: response.get('models', []) if response else None, + responses, + ) + ) + } + else: + models = {'models': []} + + return models + + +@router.get('/api/version') +@router.get('/api/version/{url_idx}') +async def get_ollama_versions(request: Request, url_idx: Optional[int] = None): + if request.app.state.config.ENABLE_OLLAMA_API: + if url_idx is None: + # returns lowest version + request_tasks = [] + + for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS): + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(idx), + request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support + ) + + enable = api_config.get('enable', True) + key = api_config.get('key', None) + + if enable: + request_tasks.append( + send_get_request( + f'{url}/api/version', + key, + ) + ) + + responses = await asyncio.gather(*request_tasks) + responses = list(filter(lambda x: x is not None, responses)) + + if len(responses) > 0: + lowest_version = min( + responses, + key=lambda x: tuple(map(int, re.sub(r'^v|-.*', '', x['version']).split('.'))), + ) + + return {'version': lowest_version['version']} + else: + raise HTTPException( + status_code=500, + detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND, + ) + else: + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + return await send_request(f'{url}/api/version', 'GET') + else: + return {'version': False} + + +class ModelNameForm(BaseModel): + model: Optional[str] = None + model_config = ConfigDict( + extra='allow', + ) + + +@router.post('/api/unload') +async def unload_model( + request: Request, + form_data: ModelNameForm, + user=Depends(get_admin_user), +): + form_data = form_data.model_dump(exclude_none=True) + model_name = form_data.get('model', form_data.get('name')) + + if not model_name: + raise HTTPException(status_code=400, detail='Missing name of the model to unload.') + + # Refresh/load models if needed, get mapping from name to URLs + await get_all_models(request, user=user) + models = request.app.state.OLLAMA_MODELS + + if model_name not in models: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model_name)) + url_indices = models[model_name]['urls'] + + # Send unload to ALL url_indices + results = [] + errors = [] + for idx in url_indices: + url = request.app.state.config.OLLAMA_BASE_URLS[idx] + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(idx), request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}) + ) + key = get_api_key(idx, url, request.app.state.config.OLLAMA_API_CONFIGS) + + prefix_id = api_config.get('prefix_id', None) + if prefix_id and model_name.startswith(f'{prefix_id}.'): + model_name = model_name[len(f'{prefix_id}.') :] + + payload = {'model': model_name, 'keep_alive': 0, 'prompt': ''} + + try: + res = await send_request( + f'{url}/api/generate', + payload=json.dumps(payload), + key=key, + user=user, + ) + results.append({'url_idx': idx, 'success': True, 'response': res}) + except Exception as e: + log.exception(f'Failed to unload model on node {idx}: {e}') + errors.append({'url_idx': idx, 'success': False, 'error': str(e)}) + + if len(errors) > 0: + raise HTTPException( + status_code=500, + detail=f'Failed to unload model on {len(errors)} nodes: {errors}', + ) + + return {'status': True} + + +@router.post('/api/pull') +@router.post('/api/pull/{url_idx}') +async def pull_model( + request: Request, + form_data: ModelNameForm, + url_idx: int = 0, + user=Depends(get_admin_user), +): + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + form_data = form_data.model_dump(exclude_none=True) + form_data['model'] = form_data.get('model', form_data.get('name')) + + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + log.info(f'url: {url}') + + # Admin should be able to pull models from any source + payload = {**form_data, 'insecure': True} + + return await send_request( + f'{url}/api/pull', + payload=json.dumps(payload), + key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), + user=user, + stream=True, + ) + + +class PushModelForm(BaseModel): + model: str + insecure: Optional[bool] = None + stream: Optional[bool] = None + + +@router.delete('/api/push') +@router.delete('/api/push/{url_idx}') +async def push_model( + request: Request, + form_data: PushModelForm, + url_idx: Optional[int] = None, + user=Depends(get_admin_user), +): + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + if url_idx is None: + await get_all_models(request, user=user) + models = request.app.state.OLLAMA_MODELS + + if form_data.model in models: + url_idx = models[form_data.model]['urls'][0] + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model), + ) + + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + log.debug(f'url: {url}') + + return await send_request( + f'{url}/api/push', + payload=form_data.model_dump_json(exclude_none=True).encode(), + key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), + user=user, + stream=True, + ) + + +class CreateModelForm(BaseModel): + model: Optional[str] = None + stream: Optional[bool] = None + path: Optional[str] = None + + model_config = ConfigDict(extra='allow') + + +@router.post('/api/create') +@router.post('/api/create/{url_idx}') +async def create_model( + request: Request, + form_data: CreateModelForm, + url_idx: int = 0, + user=Depends(get_admin_user), +): + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + log.debug(f'form_data: {form_data}') + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + + return await send_request( + f'{url}/api/create', + payload=form_data.model_dump_json(exclude_none=True).encode(), + key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), + user=user, + stream=True, + ) + + +class CopyModelForm(BaseModel): + source: str + destination: str + + +@router.post('/api/copy') +@router.post('/api/copy/{url_idx}') +async def copy_model( + request: Request, + form_data: CopyModelForm, + url_idx: Optional[int] = None, + user=Depends(get_admin_user), +): + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + if url_idx is None: + await get_all_models(request, user=user) + models = request.app.state.OLLAMA_MODELS + + if form_data.source in models: + url_idx = models[form_data.source]['urls'][0] + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.source), + ) + + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) + + await send_request( + f'{url}/api/copy', + payload=form_data.model_dump_json(exclude_none=True).encode(), + key=key, + user=user, + ) + return True + + +@router.delete('/api/delete') +@router.delete('/api/delete/{url_idx}') +async def delete_model( + request: Request, + form_data: ModelNameForm, + url_idx: Optional[int] = None, + user=Depends(get_admin_user), +): + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + form_data = form_data.model_dump(exclude_none=True) + form_data['model'] = form_data.get('model', form_data.get('name')) + + model = form_data.get('model') + + if url_idx is None: + await get_all_models(request, user=user) + models = request.app.state.OLLAMA_MODELS + + if model in models: + url_idx = models[model]['urls'][0] + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model), + ) + + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) + + await send_request( + f'{url}/api/delete', + 'DELETE', + payload=json.dumps(form_data), + key=key, + user=user, + ) + return True + + +@router.post('/api/show') +async def show_model_info(request: Request, form_data: ModelNameForm, user=Depends(get_verified_user)): + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + form_data = form_data.model_dump(exclude_none=True) + form_data['model'] = form_data.get('model', form_data.get('name')) + + model = form_data.get('model') + + # Enforce per-model access control + await check_model_access(user, await Models.get_model_by_id(model), BYPASS_MODEL_ACCESS_CONTROL) + + await get_all_models(request, user=user) + models = request.app.state.OLLAMA_MODELS + + if model not in models: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model), + ) + + url_idx = random.choice(models[model]['urls']) + + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) + + return await send_request( + f'{url}/api/show', + payload=json.dumps(form_data), + key=key, + user=user, + ) + + +class GenerateEmbedForm(BaseModel): + model: str + input: list[str] | str + truncate: Optional[bool] = None + options: Optional[dict] = None + keep_alive: Optional[Union[int, str]] = None + + model_config = ConfigDict( + extra='allow', + ) + + +@router.post('/api/embed') +@router.post('/api/embed/{url_idx}') +async def embed( + request: Request, + form_data: GenerateEmbedForm, + url_idx: Optional[int] = None, + user=Depends(get_verified_user), +): + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + log.info(f'generate_ollama_batch_embeddings {form_data}') + + # Enforce per-model access control + await check_model_access(user, await Models.get_model_by_id(form_data.model), BYPASS_MODEL_ACCESS_CONTROL) + + if url_idx is None: + model = form_data.model + + # Check if model is already in app state cache to avoid expensive get_all_models() call + models = request.app.state.OLLAMA_MODELS + if not models or model not in models: + await get_all_models(request, user=user) + models = request.app.state.OLLAMA_MODELS + + if model in models: + url_idx = random.choice(models[model]['urls']) + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model), + ) + + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(url_idx), + request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support + ) + key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) + + prefix_id = api_config.get('prefix_id', None) + if prefix_id: + form_data.model = form_data.model.replace(f'{prefix_id}.', '') + + return await send_request( + f'{url}/api/embed', + payload=form_data.model_dump_json(exclude_none=True).encode(), + key=key, + user=user, + ) + + +class GenerateEmbeddingsForm(BaseModel): + model: str + prompt: str + options: Optional[dict] = None + keep_alive: Optional[Union[int, str]] = None + + +@router.post('/api/embeddings') +@router.post('/api/embeddings/{url_idx}') +async def embeddings( + request: Request, + form_data: GenerateEmbeddingsForm, + url_idx: Optional[int] = None, + user=Depends(get_verified_user), +): + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + log.info(f'generate_ollama_embeddings {form_data}') + + # Enforce per-model access control + await check_model_access(user, await Models.get_model_by_id(form_data.model), BYPASS_MODEL_ACCESS_CONTROL) + + if url_idx is None: + model = form_data.model + + # Check if model is already in app state cache to avoid expensive get_all_models() call + models = request.app.state.OLLAMA_MODELS + if not models or model not in models: + await get_all_models(request, user=user) + models = request.app.state.OLLAMA_MODELS + + if model in models: + url_idx = random.choice(models[model]['urls']) + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model), + ) + + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(url_idx), + request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support + ) + key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) + + prefix_id = api_config.get('prefix_id', None) + if prefix_id: + form_data.model = form_data.model.replace(f'{prefix_id}.', '') + + return await send_request( + f'{url}/api/embeddings', + payload=form_data.model_dump_json(exclude_none=True).encode(), + key=key, + user=user, + ) + + +class GenerateCompletionForm(BaseModel): + model: str + prompt: Optional[str] = None + suffix: Optional[str] = None + images: Optional[list[str]] = None + format: Optional[Union[dict, str]] = None + options: Optional[dict] = None + system: Optional[str] = None + template: Optional[str] = None + context: Optional[list[int]] = None + stream: Optional[bool] = True + raw: Optional[bool] = None + keep_alive: Optional[Union[int, str]] = None + + +@router.post('/api/generate') +@router.post('/api/generate/{url_idx}') +async def generate_completion( + request: Request, + form_data: GenerateCompletionForm, + url_idx: Optional[int] = None, + user=Depends(get_verified_user), +): + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + # Enforce per-model access control + await check_model_access(user, await Models.get_model_by_id(form_data.model), BYPASS_MODEL_ACCESS_CONTROL) + + if url_idx is None: + await get_all_models(request, user=user) + models = request.app.state.OLLAMA_MODELS + + model = form_data.model + + if model in models: + url_idx = random.choice(models[model]['urls']) + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model), + ) + + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(url_idx), + request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support + ) + + prefix_id = api_config.get('prefix_id', None) + if prefix_id: + form_data.model = form_data.model.replace(f'{prefix_id}.', '') + + return await send_request( + f'{url}/api/generate', + payload=form_data.model_dump_json(exclude_none=True).encode(), + key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), + user=user, + stream=True, + ) + + +class ChatMessage(BaseModel): + role: str + content: Optional[str] = None + tool_calls: Optional[list[dict]] = None + images: Optional[list[str]] = None + + model_config = ConfigDict(extra='allow') + + @validator('content', pre=True) + @classmethod + def check_at_least_one_field(cls, field_value, values, **kwargs): + # Raise an error if both 'content' and 'tool_calls' are None + if field_value is None and ('tool_calls' not in values or values['tool_calls'] is None): + raise ValueError("At least one of 'content' or 'tool_calls' must be provided") + + return field_value + + +class GenerateChatCompletionForm(BaseModel): + model: str + messages: list[ChatMessage] + format: Optional[Union[dict, str]] = None + options: Optional[dict] = None + template: Optional[str] = None + stream: Optional[bool] = True + keep_alive: Optional[Union[int, str]] = None + tools: Optional[list[dict]] = None + model_config = ConfigDict( + extra='allow', + ) + + +async def get_ollama_url(request: Request, model: str, url_idx: Optional[int] = None): + if url_idx is None: + models = request.app.state.OLLAMA_MODELS + if model not in models: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model), + ) + url_idx = random.choice(models[model].get('urls', [])) + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + return url, url_idx + + +@router.post('/api/chat') +@router.post('/api/chat/{url_idx}') +async def generate_chat_completion( + request: Request, + form_data: dict, + url_idx: Optional[int] = None, + user=Depends(get_verified_user), + bypass_system_prompt: bool = False, +): + if not request.app.state.config.ENABLE_OLLAMA_API: + 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. + + # bypass_filter is read from request.state to prevent external clients from + # setting it via query parameter (CVE fix). Only internal server-side callers + # (e.g. utils/chat.py) should set request.state.bypass_filter = True. + bypass_filter = getattr(request.state, 'bypass_filter', False) + if BYPASS_MODEL_ACCESS_CONTROL: + bypass_filter = True + + metadata = form_data.pop('metadata', None) + try: + form_data = GenerateChatCompletionForm(**form_data) + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=400, + detail=str(e), + ) + + if isinstance(form_data, BaseModel): + payload = {**form_data.model_dump(exclude_none=True)} + + if 'metadata' in payload: + del payload['metadata'] + + model_id = payload['model'] + model_info = await Models.get_model_by_id(model_id) + + if model_info: + if model_info.base_model_id: + base_model_id = ( + request.base_model_id if hasattr(request, 'base_model_id') else model_info.base_model_id + ) # Use request's base_model_id if available + payload['model'] = base_model_id + + params = model_info.params.model_dump() + + if params: + system = params.pop('system', None) + + payload = apply_model_params_to_body_ollama(params, payload) + if not bypass_system_prompt: + payload = apply_system_prompt_to_body(system, payload, metadata, user) + + await check_model_access(user, model_info, bypass_filter) + else: + await check_model_access(user, None, bypass_filter) + + url, url_idx = await get_ollama_url(request, payload['model'], url_idx) + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(url_idx), + request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support + ) + + prefix_id = api_config.get('prefix_id', None) + if prefix_id: + payload['model'] = payload['model'].replace(f'{prefix_id}.', '') + + return await send_request( + f'{url}/api/chat', + payload=json.dumps(payload), + key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), + user=user, + stream=form_data.stream, + content_type='application/x-ndjson', + metadata=metadata, + ) + + +# TODO: we should update this part once Ollama supports other types +class OpenAIChatMessageContent(BaseModel): + type: str + model_config = ConfigDict(extra='allow') + + +class OpenAIChatMessage(BaseModel): + role: str + content: Union[Optional[str], list[OpenAIChatMessageContent]] + + model_config = ConfigDict(extra='allow') + + +class OpenAIChatCompletionForm(BaseModel): + model: str + messages: list[OpenAIChatMessage] + + model_config = ConfigDict(extra='allow') + + +class OpenAICompletionForm(BaseModel): + model: str + prompt: str + + model_config = ConfigDict(extra='allow') + + +@router.post('/v1/completions') +@router.post('/v1/completions/{url_idx}') +async def generate_openai_completion( + request: Request, + form_data: dict, + url_idx: Optional[int] = None, + user=Depends(get_verified_user), +): + # 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. + metadata = form_data.pop('metadata', None) + + try: + form_data = OpenAICompletionForm(**form_data) + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=400, + detail=str(e), + ) + + payload = {**form_data.model_dump(exclude_none=True, exclude=['metadata'])} + if 'metadata' in payload: + del payload['metadata'] + + model_id = form_data.model + model_info = await Models.get_model_by_id(model_id) + if model_info: + if model_info.base_model_id: + payload['model'] = model_info.base_model_id + params = model_info.params.model_dump() + + if params: + payload = apply_model_params_to_body_openai(params, payload) + + await check_model_access(user, model_info) + else: + await check_model_access(user, None) + + url, url_idx = await get_ollama_url(request, payload['model'], url_idx) + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(url_idx), + request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support + ) + + prefix_id = api_config.get('prefix_id', None) + + if prefix_id: + payload['model'] = payload['model'].replace(f'{prefix_id}.', '') + + return await send_request( + f'{url}/v1/completions', + payload=json.dumps(payload), + key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), + user=user, + stream=payload.get('stream', False), + metadata=metadata, + ) + + +@router.post('/v1/chat/completions') +@router.post('/v1/chat/completions/{url_idx}') +async def generate_openai_chat_completion( + request: Request, + form_data: dict, + url_idx: Optional[int] = None, + user=Depends(get_verified_user), +): + # 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. + metadata = form_data.pop('metadata', None) + + try: + completion_form = OpenAIChatCompletionForm(**form_data) + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=400, + detail=str(e), + ) + + payload = {**completion_form.model_dump(exclude_none=True, exclude=['metadata'])} + if 'metadata' in payload: + del payload['metadata'] + + model_id = completion_form.model + model_info = await Models.get_model_by_id(model_id) + if model_info: + if model_info.base_model_id: + payload['model'] = model_info.base_model_id + + params = model_info.params.model_dump() + + if params: + system = params.pop('system', None) + + payload = apply_model_params_to_body_openai(params, payload) + payload = apply_system_prompt_to_body(system, payload, metadata, user) + + await check_model_access(user, model_info) + else: + await check_model_access(user, None) + + url, url_idx = await get_ollama_url(request, payload['model'], url_idx) + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(url_idx), + request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support + ) + + prefix_id = api_config.get('prefix_id', None) + if prefix_id: + payload['model'] = payload['model'].replace(f'{prefix_id}.', '') + + return await send_request( + f'{url}/v1/chat/completions', + payload=json.dumps(payload), + key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), + user=user, + stream=payload.get('stream', False), + metadata=metadata, + ) + + +@router.post('/v1/messages') +@router.post('/v1/messages/{url_idx}') +async def generate_anthropic_messages( + request: Request, + form_data: dict, + url_idx: Optional[int] = None, + user=Depends(get_verified_user), +): + """ + Proxy for Ollama's Anthropic-compatible /v1/messages endpoint. + + Forwards the request as-is to the Ollama backend, applying the same + model resolution, access control, and prefix_id handling used by + the OpenAI-compatible /v1/chat/completions proxy. + + See https://docs.ollama.com/api/anthropic-compatibility + """ + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + payload = {**form_data} + model_id = payload.get('model', '') + + model_info = await Models.get_model_by_id(model_id) + if model_info: + if model_info.base_model_id: + payload['model'] = model_info.base_model_id + + await check_model_access(user, model_info) + else: + await check_model_access(user, None) + + url, url_idx = await get_ollama_url(request, payload['model'], url_idx) + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(url_idx), + request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support + ) + + prefix_id = api_config.get('prefix_id', None) + if prefix_id: + payload['model'] = payload['model'].replace(f'{prefix_id}.', '') + + return await send_request( + f'{url}/v1/messages', + payload=json.dumps(payload), + key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), + user=user, + stream=payload.get('stream', False), + content_type='text/event-stream' if payload.get('stream', False) else None, + ) + + +class ResponsesForm(BaseModel): + model: str + + model_config = ConfigDict(extra='allow') + + +@router.post('/v1/responses') +@router.post('/v1/responses/{url_idx}') +async def generate_responses( + request: Request, + form_data: ResponsesForm, + url_idx: Optional[int] = None, + user=Depends(get_verified_user), +): + """ + Proxy for Ollama's OpenAI-compatible /v1/responses endpoint. + + Forwards the request as-is to the Ollama backend, applying the same + model resolution, access control, and prefix_id handling used by + the OpenAI-compatible /v1/chat/completions proxy. + + See https://ollama.com/blog/responses-api + """ + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + payload = form_data.model_dump() + model_id = form_data.model + + model_info = await Models.get_model_by_id(model_id) + if model_info: + if model_info.base_model_id: + payload['model'] = model_info.base_model_id + + await check_model_access(user, model_info) + else: + await check_model_access(user, None) + + url, url_idx = await get_ollama_url(request, payload['model'], url_idx) + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(url_idx), + request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support + ) + + prefix_id = api_config.get('prefix_id', None) + if prefix_id: + payload['model'] = payload['model'].replace(f'{prefix_id}.', '') + + return await send_request( + f'{url}/v1/responses', + payload=json.dumps(payload), + key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), + user=user, + stream=payload.get('stream', False), + content_type='text/event-stream' if payload.get('stream', False) else None, + ) + + +@router.get('/v1/models') +@router.get('/v1/models/{url_idx}') +async def get_openai_models( + request: Request, + url_idx: Optional[int] = None, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + models = [] + if url_idx is None: + model_list = await get_all_models(request, user=user) + models = [ + { + 'id': model['model'], + 'object': 'model', + 'created': int(time.time()), + 'owned_by': 'openai', + } + for model in model_list['models'] + ] + + else: + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + model_list = await send_request(f'{url}/api/tags', 'GET') + + models = [ + { + 'id': model['model'], + 'object': 'model', + 'created': int(time.time()), + 'owned_by': 'openai', + } + for model in model_list.get('models', []) + ] + + if user.role == 'user' and not BYPASS_MODEL_ACCESS_CONTROL: + # Filter models based on user access control + model_ids = [model['id'] for model in models] + model_infos = {model_info.id: model_info for model_info in await Models.get_models_by_ids(model_ids, db=db)} + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} + + # Batch-fetch accessible resource IDs in a single query instead of N has_access calls + accessible_model_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type='model', + resource_ids=list(model_infos.keys()), + permission='read', + user_group_ids=user_group_ids, + db=db, + ) + + filtered_models = [] + for model in models: + model_info = model_infos.get(model['id']) + if model_info: + if user.id == model_info.user_id or model_info.id in accessible_model_ids: + filtered_models.append(model) + models = filtered_models + + return { + 'data': models, + 'object': 'list', + } + + +class UrlForm(BaseModel): + url: str + + +class UploadBlobForm(BaseModel): + filename: str + + +def parse_huggingface_url(hf_url): + try: + # Parse the URL + parsed_url = urlparse(hf_url) + + # Get the path and split it into components + path_components = parsed_url.path.split('/') + + # Extract the desired output + model_file = path_components[-1] + + return model_file + except ValueError: + return None + + +async def download_file_stream(ollama_url, file_url, file_path, file_name, chunk_size=1024 * 1024): + done = False + + if os.path.exists(file_path): + current_size = os.path.getsize(file_path) + else: + current_size = 0 + + headers = {'Range': f'bytes={current_size}-'} if current_size > 0 else {} + + timeout = aiohttp.ClientTimeout(total=600) # Set the timeout + + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.get(file_url, headers=headers, ssl=AIOHTTP_CLIENT_SESSION_SSL) as response: + total_size = int(response.headers.get('content-length', 0)) + current_size + + with open(file_path, 'ab+') as file: + async for data in response.content.iter_chunked(chunk_size): + current_size += len(data) + file.write(data) + + done = current_size == total_size + progress = round((current_size / total_size) * 100, 2) + + yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n' + + if done: + file.close() + hashed = calculate_sha256(file_path, chunk_size) + + with open(file_path, 'rb') as f: + blob_data = f.read() + + url = f'{ollama_url}/api/blobs/sha256:{hashed}' + blob_timeout = aiohttp.ClientTimeout(total=30) + async with aiohttp.ClientSession(timeout=blob_timeout, trust_env=True) as blob_session: + async with blob_session.post( + url, data=blob_data, ssl=AIOHTTP_CLIENT_SESSION_SSL + ) as blob_response: + if blob_response.ok: + res = { + 'done': done, + 'blob': f'sha256:{hashed}', + 'name': file_name, + } + os.remove(file_path) + + yield f'data: {json.dumps(res)}\n\n' + else: + raise RuntimeError('Ollama: Could not create blob, Please try again.') + + +# url = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf" +@router.post('/models/download') +@router.post('/models/download/{url_idx}') +async def download_model( + request: Request, + form_data: UrlForm, + url_idx: Optional[int] = None, + user=Depends(get_admin_user), +): + allowed_hosts = ['https://huggingface.co/', 'https://github.com/'] + + if not any(form_data.url.startswith(host) for host in allowed_hosts): + raise HTTPException( + status_code=400, + detail='Invalid file_url. Only URLs from allowed hosts are permitted.', + ) + + if url_idx is None: + url_idx = 0 + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + + file_name = parse_huggingface_url(form_data.url) + + if file_name: + file_path = os.path.join(UPLOAD_DIR, file_name) + + return StreamingResponse( + download_file_stream(url, form_data.url, file_path, file_name), + ) + else: + return None + + +# TODO: Progress bar does not reflect size & duration of upload. +@router.post('/models/upload') +@router.post('/models/upload/{url_idx}') +async def upload_model( + request: Request, + file: UploadFile = File(...), + url_idx: Optional[int] = None, + user=Depends(get_admin_user), +): + if url_idx is None: + url_idx = 0 + ollama_url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + + filename = os.path.basename(file.filename) + file_path = os.path.join(UPLOAD_DIR, filename) + os.makedirs(UPLOAD_DIR, exist_ok=True) + + # --- P1: save file locally --- + chunk_size = 1024 * 1024 * 2 # 2 MB chunks + with open(file_path, 'wb') as out_f: + while True: + chunk = file.file.read(chunk_size) + # log.info(f"Chunk: {str(chunk)}") # DEBUG + if not chunk: + break + out_f.write(chunk) + + async def file_process_stream(): + nonlocal ollama_url + total_size = os.path.getsize(file_path) + log.info(f'Total Model Size: {str(total_size)}') # DEBUG + + # --- P2: SSE progress + calculate sha256 hash --- + file_hash = calculate_sha256(file_path, chunk_size) + log.info(f'Model Hash: {str(file_hash)}') # DEBUG + try: + with open(file_path, 'rb') as f: + bytes_read = 0 + while chunk := f.read(chunk_size): + bytes_read += len(chunk) + progress = round(bytes_read / total_size * 100, 2) + data_msg = { + 'progress': progress, + 'total': total_size, + 'completed': bytes_read, + } + yield f'data: {json.dumps(data_msg)}\n\n' + + # --- P3: Upload to ollama /api/blobs --- + with open(file_path, 'rb') as f: + blob_data = f.read() + + url = f'{ollama_url}/api/blobs/sha256:{file_hash}' + upload_timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + async with aiohttp.ClientSession(timeout=upload_timeout, trust_env=True) as upload_session: + async with upload_session.post(url, data=blob_data, ssl=AIOHTTP_CLIENT_SESSION_SSL) as response: + if not response.ok: + raise Exception('Ollama: Could not create blob, Please try again.') + + log.info(f'Uploaded to /api/blobs') # DEBUG + # Remove local file + os.remove(file_path) + + # Create model in ollama + model_name, ext = os.path.splitext(filename) + log.info(f'Created Model: {model_name}') # DEBUG + + create_payload = { + 'model': model_name, + # Reference the file by its original name => the uploaded blob's digest + 'files': {filename: f'sha256:{file_hash}'}, + } + log.info(f'Model Payload: {create_payload}') # DEBUG + + # Call ollama /api/create + # https://github.com/ollama/ollama/blob/main/docs/api.md#create-a-model + async with aiohttp.ClientSession(timeout=upload_timeout, trust_env=True) as create_session: + async with create_session.post( + f'{ollama_url}/api/create', + headers={'Content-Type': 'application/json'}, + data=json.dumps(create_payload), + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as create_resp: + if create_resp.ok: + log.info(f'API SUCCESS!') # DEBUG + done_msg = { + 'done': True, + 'blob': f'sha256:{file_hash}', + 'name': filename, + 'model_created': model_name, + } + yield f'data: {json.dumps(done_msg)}\n\n' + else: + resp_text = await create_resp.text() + raise Exception(f'Failed to create model in Ollama. {resp_text}') + + except Exception as e: + res = {'error': str(e)} + yield f'data: {json.dumps(res)}\n\n' + + return StreamingResponse(file_process_stream(), media_type='text/event-stream') diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py new file mode 100644 index 0000000000000000000000000000000000000000..6f8c0f81bf954a469f618e4767c0870f08da6984 --- /dev/null +++ b/backend/open_webui/routers/openai.py @@ -0,0 +1,1582 @@ +import asyncio +import hashlib +import json +import logging +import re +from typing import Optional +from urllib.parse import quote, urlparse + +import aiohttp +from aiocache import cached + + +from azure.identity import DefaultAzureCredential, get_bearer_token_provider + +from fastapi import Depends, HTTPException, Request, APIRouter, status +from fastapi.responses import ( + FileResponse, + StreamingResponse, + JSONResponse, + PlainTextResponse, +) +from pydantic import BaseModel, ConfigDict + +from sqlalchemy.ext.asyncio import AsyncSession + +from open_webui.internal.db import get_async_session + +from open_webui.models.models import Models +from open_webui.models.access_grants import AccessGrants +from open_webui.models.groups import Groups +from open_webui.utils.access_control import has_connection_access, check_model_access +from open_webui.config import ( + CACHE_DIR, +) +from open_webui.env import ( + MODELS_CACHE_TTL, + AIOHTTP_CLIENT_SESSION_SSL, + AIOHTTP_CLIENT_TIMEOUT, + AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST, + ENABLE_FORWARD_USER_INFO_HEADERS, + FORWARD_SESSION_INFO_HEADER_CHAT_ID, + BYPASS_MODEL_ACCESS_CONTROL, + ENABLE_OPENAI_API_PASSTHROUGH, +) +from open_webui.models.users import UserModel + +from open_webui.constants import ERROR_MESSAGES + + +from open_webui.utils.payload import ( + apply_model_params_to_body_openai, + apply_system_prompt_to_body, +) +from open_webui.utils.misc import ( + convert_logit_bias_input_to_json, + stream_chunks_handler, +) +from open_webui.utils.session_pool import ( + cleanup_response, + get_session, + stream_wrapper, +) + +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.headers import include_user_info_headers +from open_webui.utils.anthropic import is_anthropic_url, get_anthropic_models + +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 +# clients to attempt decompression of an already-decoded payload, resulting +# in ZlibError. See https://github.com/aio-libs/aiohttp/issues/4462. +_STRIP_PROXY_HEADERS = frozenset({'Content-Encoding', 'Content-Length', 'Transfer-Encoding'}) + + +def _clean_proxy_headers(raw_headers) -> dict: + """Return a copy of *raw_headers* with stale encoding headers removed.""" + return {k: v for k, v in raw_headers.items() if k not in _STRIP_PROXY_HEADERS} + + +async def send_get_request( + request: Request = None, + url=None, + key=None, + user: UserModel = None, + config=None, +): + timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) + try: + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + if request and config: + headers, cookies = await get_headers_and_cookies(request, url, key, config, user=user) + else: + headers = { + **({'Authorization': f'Bearer {key}'} if key else {}), + } + cookies = None + + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) + + async with session.get( + url, + headers=headers, + cookies=cookies, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + return await response.json() + except Exception as e: + # Handle connection error here + log.error(f'Connection error: {e}') + return None + + +async def get_models_request( + request: Request = None, + url=None, + key=None, + user: UserModel = None, + config=None, +): + if is_anthropic_url(url): + return await get_anthropic_models(url, key, user=user) + return await send_get_request(request, f'{url}/models', key, user=user, config=config) + + +def openai_reasoning_model_handler(payload): + """ + Handle reasoning model specific parameters + """ + if 'max_tokens' in payload: + # Convert "max_tokens" to "max_completion_tokens" for all reasoning models + payload['max_completion_tokens'] = payload['max_tokens'] + del payload['max_tokens'] + + # Handle system role conversion based on model type + if payload['messages'][0]['role'] == 'system': + model_lower = payload['model'].lower() + # Legacy models use "user" role instead of "system" + if model_lower.startswith('o1-mini') or model_lower.startswith('o1-preview'): + payload['messages'][0]['role'] = 'user' + else: + payload['messages'][0]['role'] = 'developer' + + return payload + + +async def get_headers_and_cookies( + request: Request, + url, + key=None, + config=None, + metadata: Optional[dict] = None, + user: UserModel = None, +): + cookies = {} + headers = { + 'Content-Type': 'application/json', + **( + { + 'HTTP-Referer': 'https://openwebui.com/', + 'X-Title': 'Open WebUI', + } + if 'openrouter.ai' in url + else {} + ), + } + + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) + if metadata and metadata.get('chat_id'): + headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = metadata.get('chat_id') + + token = None + auth_type = config.get('auth_type') + + if auth_type == 'bearer' or auth_type is None: + # Default to bearer if not specified + token = f'{key}' + elif auth_type == 'none': + token = None + elif auth_type == 'session': + cookies = request.cookies + token = request.state.token.credentials + elif auth_type == 'system_oauth': + cookies = request.cookies + + oauth_token = None + try: + if request.cookies.get('oauth_session_id', None): + oauth_token = await request.app.state.oauth_manager.get_oauth_token( + user.id, + request.cookies.get('oauth_session_id', None), + ) + except Exception as e: + log.error(f'Error getting OAuth token: {e}') + + if oauth_token: + token = f'{oauth_token.get("access_token", "")}' + + elif auth_type in ('azure_ad', 'microsoft_entra_id'): + token = get_microsoft_entra_id_access_token() + + if token: + headers['Authorization'] = f'Bearer {token}' + + if config.get('headers') and isinstance(config.get('headers'), dict): + headers = {**headers, **config.get('headers')} + + return headers, cookies + + +def get_microsoft_entra_id_access_token(): + """ + Get Microsoft Entra ID access token using DefaultAzureCredential for Azure OpenAI. + Returns the token string or None if authentication fails. + """ + try: + token_provider = get_bearer_token_provider( + DefaultAzureCredential(), 'https://cognitiveservices.azure.com/.default' + ) + return token_provider() + except Exception as e: + log.error(f'Error getting Microsoft Entra ID access token: {e}') + return None + + +########################################## +# +# API routes +# +########################################## + +router = APIRouter() + + +@router.get('/config') +async def get_config(request: Request, user=Depends(get_admin_user)): + return { + 'ENABLE_OPENAI_API': request.app.state.config.ENABLE_OPENAI_API, + 'OPENAI_API_BASE_URLS': request.app.state.config.OPENAI_API_BASE_URLS, + 'OPENAI_API_KEYS': request.app.state.config.OPENAI_API_KEYS, + 'OPENAI_API_CONFIGS': request.app.state.config.OPENAI_API_CONFIGS, + } + + +class OpenAIConfigForm(BaseModel): + ENABLE_OPENAI_API: Optional[bool] = None + OPENAI_API_BASE_URLS: list[str] + OPENAI_API_KEYS: list[str] + OPENAI_API_CONFIGS: dict + + +@router.post('/config/update') +async def update_config(request: Request, form_data: OpenAIConfigForm, user=Depends(get_admin_user)): + request.app.state.config.ENABLE_OPENAI_API = form_data.ENABLE_OPENAI_API + request.app.state.config.OPENAI_API_BASE_URLS = form_data.OPENAI_API_BASE_URLS + request.app.state.config.OPENAI_API_KEYS = form_data.OPENAI_API_KEYS + + # Check if API KEYS length is same than API URLS length + if len(request.app.state.config.OPENAI_API_KEYS) != len(request.app.state.config.OPENAI_API_BASE_URLS): + if len(request.app.state.config.OPENAI_API_KEYS) > len(request.app.state.config.OPENAI_API_BASE_URLS): + request.app.state.config.OPENAI_API_KEYS = request.app.state.config.OPENAI_API_KEYS[ + : len(request.app.state.config.OPENAI_API_BASE_URLS) + ] + else: + request.app.state.config.OPENAI_API_KEYS += [''] * ( + len(request.app.state.config.OPENAI_API_BASE_URLS) - len(request.app.state.config.OPENAI_API_KEYS) + ) + + request.app.state.config.OPENAI_API_CONFIGS = form_data.OPENAI_API_CONFIGS + + # Remove the API configs that are not in the API URLS + keys = list(map(str, range(len(request.app.state.config.OPENAI_API_BASE_URLS)))) + request.app.state.config.OPENAI_API_CONFIGS = { + key: value for key, value in request.app.state.config.OPENAI_API_CONFIGS.items() if key in keys + } + + return { + 'ENABLE_OPENAI_API': request.app.state.config.ENABLE_OPENAI_API, + 'OPENAI_API_BASE_URLS': request.app.state.config.OPENAI_API_BASE_URLS, + 'OPENAI_API_KEYS': request.app.state.config.OPENAI_API_KEYS, + 'OPENAI_API_CONFIGS': request.app.state.config.OPENAI_API_CONFIGS, + } + + +@router.post('/audio/speech') +async def speech(request: Request, user=Depends(get_verified_user)): + idx = None + try: + idx = request.app.state.config.OPENAI_API_BASE_URLS.index('https://api.openai.com/v1') + + body = await request.body() + name = hashlib.sha256(body).hexdigest() + + SPEECH_CACHE_DIR = CACHE_DIR / 'audio' / 'speech' + SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True) + file_path = SPEECH_CACHE_DIR.joinpath(f'{name}.mp3') + file_body_path = SPEECH_CACHE_DIR.joinpath(f'{name}.json') + + # Check if the file already exists in the cache + if file_path.is_file(): + return FileResponse(file_path) + + url = request.app.state.config.OPENAI_API_BASE_URLS[idx] + key = request.app.state.config.OPENAI_API_KEYS[idx] + api_config = request.app.state.config.OPENAI_API_CONFIGS.get( + str(idx), + request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support + ) + + headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user) + + r = None + try: + session = await get_session() + r = await session.post( + url=f'{url}/audio/speech', + data=body, + headers=headers, + cookies=cookies, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) + + r.raise_for_status() + + # Save the streaming content to a file + with open(file_path, 'wb') as f: + async for chunk in r.content.iter_chunked(8192): + f.write(chunk) + + with open(file_body_path, 'w') as f: + json.dump(json.loads(body.decode('utf-8')), f) + + # Return the saved file + return FileResponse(file_path) + + except Exception as e: + log.exception(e) + + detail = None + if r is not None: + try: + res = await r.json() + if 'error' in res: + detail = f'External: {res["error"]}' + except Exception: + detail = f'External: {e}' + + raise HTTPException( + status_code=r.status if r else 500, + detail=detail if detail else 'Open WebUI: Server Connection Error', + ) + + except ValueError: + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND) + + +async def get_all_models_responses(request: Request, user: UserModel) -> list: + if not request.app.state.config.ENABLE_OPENAI_API: + return [] + + # Cache config values locally to avoid repeated Redis lookups. + # Each access to request.app.state.config. triggers a Redis GET; + # caching here avoids hundreds of redundant round-trips. + api_base_urls = request.app.state.config.OPENAI_API_BASE_URLS + api_keys = list(request.app.state.config.OPENAI_API_KEYS) + api_configs = request.app.state.config.OPENAI_API_CONFIGS + + # Check if API KEYS length is same than API URLS length + num_urls = len(api_base_urls) + num_keys = len(api_keys) + + if num_keys != num_urls: + # if there are more keys than urls, remove the extra keys + if num_keys > num_urls: + api_keys = api_keys[:num_urls] + request.app.state.config.OPENAI_API_KEYS = api_keys + # if there are more urls than keys, add empty keys + else: + api_keys += [''] * (num_urls - num_keys) + request.app.state.config.OPENAI_API_KEYS = api_keys + + request_tasks = [] + for idx, url in enumerate(api_base_urls): + if (str(idx) not in api_configs) and (url not in api_configs): # Legacy support + request_tasks.append(get_models_request(request, url, api_keys[idx], user=user)) + else: + api_config = api_configs.get( + str(idx), + api_configs.get(url, {}), # Legacy support + ) + + enable = api_config.get('enable', True) + model_ids = api_config.get('model_ids', []) + + if enable: + if len(model_ids) == 0: + request_tasks.append(get_models_request(request, url, api_keys[idx], user=user, config=api_config)) + else: + model_list = { + 'object': 'list', + 'data': [ + { + 'id': model_id, + 'name': model_id, + 'owned_by': 'openai', + 'openai': {'id': model_id}, + 'urlIdx': idx, + } + for model_id in model_ids + ], + } + + request_tasks.append(asyncio.ensure_future(asyncio.sleep(0, model_list))) + else: + request_tasks.append(asyncio.ensure_future(asyncio.sleep(0, None))) + + responses = await asyncio.gather(*request_tasks) + + for idx, response in enumerate(responses): + if response: + url = api_base_urls[idx] + api_config = api_configs.get( + str(idx), + api_configs.get(url, {}), # Legacy support + ) + + connection_type = api_config.get('connection_type', 'external') + prefix_id = api_config.get('prefix_id', None) + tags = api_config.get('tags', []) + + model_list = response if isinstance(response, list) else response.get('data', []) + if not isinstance(model_list, list): + # Catch non-list responses + model_list = [] + + for model in model_list: + # Remove name key if its value is None #16689 + if 'name' in model and model['name'] is None: + del model['name'] + + if prefix_id: + model['id'] = f'{prefix_id}.{model.get("id", model.get("name", ""))}' + + if tags: + model['tags'] = tags + + if connection_type: + model['connection_type'] = connection_type + + log.debug(f'get_all_models:responses() {responses}') + return responses + + +async def get_filtered_models(models, user, db=None): + # Filter models based on user access control + model_ids = [model['id'] for model in models.get('data', [])] + model_infos = {model_info.id: model_info for model_info in await Models.get_models_by_ids(model_ids, db=db)} + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} + + # Batch-fetch accessible resource IDs in a single query instead of N has_access calls + accessible_model_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type='model', + resource_ids=list(model_infos.keys()), + permission='read', + user_group_ids=user_group_ids, + db=db, + ) + + filtered_models = [] + for model in models.get('data', []): + model_info = model_infos.get(model['id']) + if model_info: + if user.id == model_info.user_id or model_info.id in accessible_model_ids: + filtered_models.append(model) + return filtered_models + + +@cached( + ttl=MODELS_CACHE_TTL, + key=lambda _, user: f'openai_all_models_{user.id}' if user else 'openai_all_models', +) +async def get_all_models(request: Request, user: UserModel) -> dict[str, list]: + log.info('get_all_models()') + + if not request.app.state.config.ENABLE_OPENAI_API: + return {'data': []} + + # Cache config value locally to avoid repeated Redis lookups inside + # the nested loop in get_merged_models (one GET per model otherwise). + api_base_urls = request.app.state.config.OPENAI_API_BASE_URLS + + responses = await get_all_models_responses(request, user=user) + + def extract_data(response): + if response and 'data' in response: + return response['data'] + if isinstance(response, list): + return response + return None + + def is_supported_openai_models(model_id): + if any( + name in model_id + for name in [ + 'babbage', + 'dall-e', + 'davinci', + 'embedding', + 'tts', + 'whisper', + ] + ): + return False + return True + + def get_merged_models(model_lists): + log.debug(f'merge_models_lists {model_lists}') + models = {} + + for idx, model_list in enumerate(model_lists): + if model_list is not None and 'error' not in model_list: + for model in model_list: + model_id = model.get('id') or model.get('name') + + base_url = api_base_urls[idx] + hostname = urlparse(base_url).hostname if base_url else None + if hostname == 'api.openai.com' and not is_supported_openai_models(model_id): + # Skip unwanted OpenAI models + continue + + if model_id and model_id not in models: + models[model_id] = { + **model, + 'name': model.get('name', model_id), + 'owned_by': 'openai', + 'openai': model, + 'connection_type': model.get('connection_type', 'external'), + 'urlIdx': idx, + } + + return models + + models = get_merged_models(map(extract_data, responses)) + log.debug(f'models: {models}') + + request.app.state.OPENAI_MODELS = models + return {'data': list(models.values())} + + +@router.get('/models') +@router.get('/models/{url_idx}') +async def get_models(request: Request, url_idx: Optional[int] = None, user=Depends(get_verified_user)): + if not request.app.state.config.ENABLE_OPENAI_API: + raise HTTPException(status_code=503, detail='OpenAI API is disabled') + + models = { + 'data': [], + } + + if url_idx is None: + models = await get_all_models(request, user=user) + else: + url = request.app.state.config.OPENAI_API_BASE_URLS[url_idx] + key = request.app.state.config.OPENAI_API_KEYS[url_idx] + + api_config = request.app.state.config.OPENAI_API_CONFIGS.get( + str(url_idx), + request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support + ) + + r = None + async with aiohttp.ClientSession( + trust_env=True, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST), + ) as session: + try: + headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user) + + if api_config.get('azure', False): + models = { + 'data': api_config.get('model_ids', []) or [], + 'object': 'list', + } + elif is_anthropic_url(url): + models = await get_anthropic_models(url, key, user=user) + if models is None: + raise Exception('Failed to connect to Anthropic API') + else: + async with session.get( + f'{url}/models', + headers=headers, + cookies=cookies, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + if r.status != 200: + error_detail = f'HTTP Error: {r.status}' + try: + res = await r.json() + if 'error' in res: + error_detail = f'External Error: {res["error"]}' + except Exception: + pass + raise Exception(error_detail) + + response_data = await r.json() + + if 'api.openai.com' in url: + response_data['data'] = [ + model + for model in response_data.get('data', []) + if not any( + name in model['id'] + for name in [ + 'babbage', + 'dall-e', + 'davinci', + 'embedding', + 'tts', + 'whisper', + ] + ) + ] + + models = response_data + except aiohttp.ClientError as e: + # ClientError covers all aiohttp requests issues + log.exception(f'Client error: {str(e)}') + raise HTTPException(status_code=500, detail='Open WebUI: Server Connection Error') + except Exception as e: + log.exception(f'Unexpected error: {e}') + error_detail = f'Unexpected error: {str(e)}' + raise HTTPException(status_code=500, detail=error_detail) + + if user.role == 'user' and not BYPASS_MODEL_ACCESS_CONTROL: + models['data'] = await get_filtered_models(models, user) + + return models + + +class ConnectionVerificationForm(BaseModel): + url: str + key: str + + config: Optional[dict] = None + + +@router.post('/verify') +async def verify_connection( + request: Request, + form_data: ConnectionVerificationForm, + user=Depends(get_admin_user), +): + url = form_data.url + key = form_data.key + + api_config = form_data.config or {} + + async with aiohttp.ClientSession( + trust_env=True, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST), + ) as session: + try: + headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user) + + if api_config.get('azure', False): + # Only set api-key header if not using Azure Entra ID authentication + auth_type = api_config.get('auth_type', 'bearer') + if auth_type not in ('azure_ad', 'microsoft_entra_id'): + headers['api-key'] = key + + api_version = api_config.get('api_version', '') or '2023-03-15-preview' + async with session.get( + url=f'{url}/openai/models?api-version={api_version}', + headers=headers, + cookies=cookies, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + try: + response_data = await r.json() + except Exception: + response_data = await r.text() + + if r.status != 200: + if isinstance(response_data, (dict, list)): + return JSONResponse(status_code=r.status, content=response_data) + else: + return PlainTextResponse(status_code=r.status, content=response_data) + + return response_data + elif is_anthropic_url(url): + result = await get_anthropic_models(url, key) + if result is None: + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR) + if 'error' in result: + raise HTTPException(status_code=500, detail=result['error']) + return result + else: + async with session.get( + f'{url}/models', + headers=headers, + cookies=cookies, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + try: + response_data = await r.json() + except Exception: + response_data = await r.text() + + if r.status != 200: + if isinstance(response_data, (dict, list)): + return JSONResponse(status_code=r.status, content=response_data) + else: + return PlainTextResponse(status_code=r.status, content=response_data) + + return response_data + + except aiohttp.ClientError as e: + # ClientError covers all aiohttp requests issues + log.exception(f'Client error: {str(e)}') + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR) + except Exception as e: + log.exception(f'Unexpected error: {e}') + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR) + + +def get_azure_allowed_params(api_version: str) -> set[str]: + allowed_params = { + 'messages', + 'temperature', + 'role', + 'content', + 'contentPart', + 'contentPartImage', + 'enhancements', + 'dataSources', + 'n', + 'stream', + 'stop', + 'max_tokens', + 'presence_penalty', + 'frequency_penalty', + 'logit_bias', + 'user', + 'function_call', + 'functions', + 'tools', + 'tool_choice', + 'top_p', + 'log_probs', + 'top_logprobs', + 'response_format', + 'seed', + 'max_completion_tokens', + 'reasoning_effort', + } + + try: + if api_version >= '2024-09-01-preview': + allowed_params.add('stream_options') + except ValueError: + log.debug(f'Invalid API version {api_version} for Azure OpenAI. Defaulting to allowed parameters.') + + return allowed_params + + +def is_openai_new_model(model: str) -> bool: + model_lower = model.lower() + # o-series models (o1, o3, o4, o5, ...) + if re.match(r'^o\d+', model_lower): + return True + # gpt-N where N >= 5 (gpt-5, gpt-5.2, gpt-6, ...) + m = re.match(r'^gpt-(\d+)', model_lower) + if m and int(m.group(1)) >= 5: + return True + return False + + +def _sanitize_model_for_url(model: str) -> str: + """Sanitize a model name before interpolating it into a URL path. + + Rejects path traversal attempts (../, /, \\) and percent-encodes + the name so it is safe to use as a single URL path segment + (e.g. Azure deployment name). + """ + if not model or '..' in model or '/' in model or '\\' in model: + raise HTTPException( + status_code=400, + detail='Invalid model name: must not be empty or contain path separators or traversal sequences', + ) + return quote(model, safe='') + + +def convert_to_azure_payload(url, payload: dict, api_version: str): + model = payload.get('model', '') + + # Filter allowed parameters based on Azure OpenAI API + allowed_params = get_azure_allowed_params(api_version) + + # Special handling for o-series models + if is_openai_new_model(model): + # Convert max_tokens to max_completion_tokens for o-series models + if 'max_tokens' in payload: + payload['max_completion_tokens'] = payload['max_tokens'] + del payload['max_tokens'] + + # Remove temperature if not 1 for o-series models + if 'temperature' in payload and payload['temperature'] != 1: + log.debug( + f'Removing temperature parameter for o-series model {model} as only default value (1) is supported' + ) + del payload['temperature'] + + # Filter out unsupported parameters + payload = {k: v for k, v in payload.items() if k in allowed_params} + + # Sanitize model name to prevent path traversal in the deployment URL + model = _sanitize_model_for_url(model) + + url = f'{url}/openai/deployments/{model}' + return url, payload + + +# Fields accepted by the Responses API for each input item type. +RESPONSES_ALLOWED_FIELDS: dict[str, set[str]] = { + 'message': {'type', 'role', 'content'}, + 'function_call': {'type', 'call_id', 'name', 'arguments', 'id'}, + 'function_call_output': {'type', 'call_id', 'output'}, +} + + +def _normalize_stored_item(item: dict) -> dict: + """Strip local-only fields from a stored output item before replaying it. + + Open WebUI stores extra bookkeeping fields (``id``, ``status``, + ``started_at``, ``ended_at``, ``duration``, ``_tag_type``, + ``attributes``, ``summary``, etc.) that the Responses API does + not accept. This helper returns a copy containing only the + fields the API understands. + """ + item_type = item.get('type', '') + allowed = RESPONSES_ALLOWED_FIELDS.get(item_type) + if allowed is None: + # Unknown type — pass through as-is (e.g. reasoning, extension items). + return item + return {k: v for k, v in item.items() if k in allowed} + + +def convert_to_responses_payload(payload: dict) -> dict: + """ + Convert Chat Completions payload to Responses API format. + + Chat Completions: { messages: [{role, content}], ... } + Responses API: { input: [{type: "message", role, content: [...]}], instructions: "system" } + """ + messages = payload.pop('messages', []) + + system_content = '' + input_items = [] + + for msg in messages: + role = msg.get('role', 'user') + content = msg.get('content', '') + + # Check for stored output items (from previous Responses API turn) + stored_output = msg.get('output') + if stored_output and isinstance(stored_output, list): + input_items.extend(_normalize_stored_item(item) for item in stored_output) + continue + + if role == 'system': + if isinstance(content, str): + system_content = content + elif isinstance(content, list): + system_content = '\n'.join(p.get('text', '') for p in content if p.get('type') == 'text') + continue + + # Handle assistant messages with tool_calls (from convert_output_to_messages) + if role == 'assistant' and msg.get('tool_calls'): + # Add text content as message if present + if content: + text = ( + content + if isinstance(content, str) + else '\n'.join(p.get('text', '') for p in content if p.get('type') == 'text') + ) + if text.strip(): + input_items.append( + { + 'type': 'message', + 'role': 'assistant', + 'content': [{'type': 'output_text', 'text': text}], + } + ) + # Convert each tool_call to a function_call input item + for tool_call in msg['tool_calls']: + func = tool_call.get('function', {}) + input_items.append( + { + 'type': 'function_call', + 'call_id': tool_call.get('id', ''), + 'name': func.get('name', ''), + 'arguments': func.get('arguments', '{}'), + } + ) + continue + + # Handle tool result messages + if role == 'tool': + input_items.append( + { + 'type': 'function_call_output', + 'call_id': msg.get('tool_call_id', ''), + 'output': msg.get('content', ''), + } + ) + continue + + # Convert content format + text_type = 'output_text' if role == 'assistant' else 'input_text' + + if isinstance(content, str): + content_parts = [{'type': text_type, 'text': content}] + elif isinstance(content, list): + content_parts = [] + for part in content: + if part.get('type') == 'text': + content_parts.append({'type': text_type, 'text': part.get('text', '')}) + elif part.get('type') == 'image_url': + url_data = part.get('image_url', {}) + url = url_data.get('url', '') if isinstance(url_data, dict) else url_data + content_parts.append({'type': 'input_image', 'image_url': url}) + else: + content_parts = [{'type': text_type, 'text': str(content)}] + + input_items.append({'type': 'message', 'role': role, 'content': content_parts}) + + responses_payload = {**payload, 'input': input_items} + + # Forward previous_response_id when the middleware has set it + # (only used when ENABLE_RESPONSES_API_STATEFUL is enabled). + previous_response_id = responses_payload.pop('previous_response_id', None) + if previous_response_id: + responses_payload['previous_response_id'] = previous_response_id + + if system_content: + responses_payload['instructions'] = system_content + + if 'max_tokens' in responses_payload: + responses_payload['max_output_tokens'] = responses_payload.pop('max_tokens') + + if 'max_completion_tokens' in responses_payload: + responses_payload['max_output_tokens'] = responses_payload.pop('max_completion_tokens') + + # Remove Chat Completions-only parameters not supported by the Responses API + for unsupported_key in ( + 'stream_options', + 'logit_bias', + 'frequency_penalty', + 'presence_penalty', + 'stop', + ): + responses_payload.pop(unsupported_key, None) + + # Convert Chat Completions tools format to Responses API format + # Chat Completions: {"type": "function", "function": {"name": ..., "description": ..., "parameters": ...}} + # Responses API: {"type": "function", "name": ..., "description": ..., "parameters": ...} + if 'tools' in responses_payload and isinstance(responses_payload['tools'], list): + converted_tools = [] + for tool in responses_payload['tools']: + if isinstance(tool, dict) and 'function' in tool: + func = tool['function'] + converted_tool = {'type': tool.get('type', 'function')} + if isinstance(func, dict): + converted_tool['name'] = func.get('name', '') + if 'description' in func: + converted_tool['description'] = func['description'] + if 'parameters' in func: + converted_tool['parameters'] = func['parameters'] + if 'strict' in func: + converted_tool['strict'] = func['strict'] + converted_tools.append(converted_tool) + else: + # Already in correct format or unknown format, pass through + converted_tools.append(tool) + responses_payload['tools'] = converted_tools + + return responses_payload + + +def convert_responses_result(response: dict) -> dict: + """ + Convert non-streaming Responses API result to Chat Completions format. + + Extracts text from message output items so all downstream consumers + (frontend tasks, get_content_from_response) work without modification. + """ + output_items = response.get('output', []) + + content = '' + for item in output_items: + if item.get('type') == 'message': + for part in item.get('content', []): + if part.get('type') == 'output_text': + content += part.get('text', '') + + return { + 'id': response.get('id', ''), + 'object': 'chat.completion', + 'model': response.get('model', ''), + 'choices': [ + { + 'index': 0, + 'message': { + 'role': 'assistant', + 'content': content, + }, + 'finish_reason': 'stop', + } + ], + 'usage': response.get('usage', {}), + } + + +@router.post('/chat/completions') +async def generate_chat_completion( + request: Request, + form_data: dict, + user=Depends(get_verified_user), + bypass_system_prompt: bool = False, +): + # 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. + + # bypass_filter is read from request.state to prevent external clients from + # setting it via query parameter (CVE fix). Only internal server-side callers + # (e.g. utils/chat.py) should set request.state.bypass_filter = True. + bypass_filter = getattr(request.state, 'bypass_filter', False) + if BYPASS_MODEL_ACCESS_CONTROL: + bypass_filter = True + + idx = 0 + + payload = {**form_data} + metadata = payload.pop('metadata', None) + + model_id = form_data.get('model') + model_info = await Models.get_model_by_id(model_id) + + # Check model info and override the payload + if model_info: + if model_info.base_model_id: + base_model_id = ( + request.base_model_id if hasattr(request, 'base_model_id') else model_info.base_model_id + ) # Use request's base_model_id if available + payload['model'] = base_model_id + model_id = base_model_id + + params = model_info.params.model_dump() + + if params: + system = params.pop('system', None) + + payload = apply_model_params_to_body_openai(params, payload) + if not bypass_system_prompt: + payload = apply_system_prompt_to_body(system, payload, metadata, user) + + await check_model_access(user, model_info, bypass_filter) + else: + await check_model_access(user, None, bypass_filter) + + # Check if model is already in app state cache to avoid expensive get_all_models() call + models = request.app.state.OPENAI_MODELS + if not models or model_id not in models: + await get_all_models(request, user=user) + models = request.app.state.OPENAI_MODELS + model = models.get(model_id) + + if model: + idx = model['urlIdx'] + else: + raise HTTPException( + status_code=404, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), + ) + + # Get the API config for the model + api_config = request.app.state.config.OPENAI_API_CONFIGS.get( + str(idx), + request.app.state.config.OPENAI_API_CONFIGS.get( + request.app.state.config.OPENAI_API_BASE_URLS[idx], {} + ), # Legacy support + ) + + prefix_id = api_config.get('prefix_id', None) + if prefix_id: + payload['model'] = payload['model'].replace(f'{prefix_id}.', '') + + # Add user info to the payload if the model is a pipeline + if 'pipeline' in model and model.get('pipeline'): + payload['user'] = { + 'name': user.name, + 'id': user.id, + 'email': user.email, + 'role': user.role, + } + + url = request.app.state.config.OPENAI_API_BASE_URLS[idx] + key = request.app.state.config.OPENAI_API_KEYS[idx] + + # Check if model is a reasoning model that needs special handling + if is_openai_new_model(payload['model']): + payload = openai_reasoning_model_handler(payload) + elif 'api.openai.com' not in url: + # Remove "max_completion_tokens" from the payload for backward compatibility + if 'max_completion_tokens' in payload: + payload['max_tokens'] = payload['max_completion_tokens'] + del payload['max_completion_tokens'] + + if 'max_tokens' in payload and 'max_completion_tokens' in payload: + del payload['max_tokens'] + + # Convert the modified body back to JSON + if 'logit_bias' in payload and payload['logit_bias']: + logit_bias = convert_logit_bias_input_to_json(payload['logit_bias']) + + if logit_bias: + payload['logit_bias'] = json.loads(logit_bias) + + headers, cookies = await get_headers_and_cookies(request, url, key, api_config, metadata, user=user) + + is_responses = api_config.get('api_type') == 'responses' + + if api_config.get('azure', False): + # Only set api-key header if not using Azure Entra ID authentication + auth_type = api_config.get('auth_type', 'bearer') + if auth_type not in ('azure_ad', 'microsoft_entra_id'): + headers['api-key'] = key + + # Azure v1 format: base URL already ends with /openai/v1, + # model stays in the payload, no deployment URL rewriting. + is_azure_v1 = bool(re.search(r'/openai/v1(?:/|$)', url)) + + if is_azure_v1: + if is_responses: + payload = convert_to_responses_payload(payload) + request_url = f'{url.rstrip("/")}/responses' + else: + request_url = f'{url.rstrip("/")}/chat/completions' + else: + api_version = api_config.get('api_version', '2023-03-15-preview') + request_url, payload = convert_to_azure_payload(url, payload, api_version) + headers['api-version'] = api_version + + if is_responses: + payload = convert_to_responses_payload(payload) + request_url = f'{request_url}/responses?api-version={api_version}' + else: + request_url = f'{request_url}/chat/completions?api-version={api_version}' + else: + if is_responses: + payload = convert_to_responses_payload(payload) + request_url = f'{url}/responses' + else: + request_url = f'{url}/chat/completions' + # For Chat Completions, strip image parts from multimodal tool messages + # (Chat Completions doesn't support images in tool content). + if not is_responses and 'messages' in payload: + for message in payload['messages']: + if message.get('role') == 'tool' and isinstance(message.get('content'), list): + message['content'] = ''.join( + part.get('text', '') for part in message['content'] if part.get('type') in ('input_text', 'text') + ) + + payload = json.dumps(payload) + + r = None + streaming = False + response = None + + try: + session = await get_session() + + r = await session.request( + method='POST', + url=request_url, + data=payload, + headers=headers, + cookies=cookies, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) + + # Check if response is SSE + if 'text/event-stream' in r.headers.get('Content-Type', ''): + # If the provider returned an error status with SSE content-type, + # read the body and return a proper error response instead of + # streaming the error back (which hides the error from logs). + if r.status >= 400: + error_body = await r.text() + log.error( + 'Provider returned HTTP %d with SSE content-type: %s', + r.status, + error_body[:1000], + ) + try: + error_json = json.loads(error_body) + return JSONResponse(status_code=r.status, content=error_json) + except json.JSONDecodeError: + return JSONResponse( + status_code=r.status, + content={'error': {'message': error_body, 'code': r.status}}, + ) + + streaming = True + return StreamingResponse( + stream_wrapper(r, content_handler=stream_chunks_handler), + status_code=r.status, + headers=_clean_proxy_headers(r.headers), + ) + else: + try: + response = await r.json() + except Exception as e: + log.error(e) + response = await r.text() + + if r.status >= 400: + if isinstance(response, (dict, list)): + return JSONResponse(status_code=r.status, content=response) + else: + return PlainTextResponse(status_code=r.status, content=response) + + # Convert Responses API result to simple format + if is_responses and isinstance(response, dict): + response = convert_responses_result(response) + + return response + except Exception as e: + log.exception(e) + + raise HTTPException( + status_code=r.status if r else 500, + detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR, + ) + finally: + if not streaming: + await cleanup_response(r) + + +async def embeddings(request: Request, form_data: dict, user): + """ + Calls the embeddings endpoint for OpenAI-compatible providers. + + Args: + request (Request): The FastAPI request context. + form_data (dict): OpenAI-compatible embeddings payload. + user (UserModel): The authenticated user. + + Returns: + dict: OpenAI-compatible embeddings response. + """ + idx = 0 + # Prepare payload/body + body = json.dumps(form_data) + # Find correct backend url/key based on model + model_id = form_data.get('model') + # Check if model is already in app state cache to avoid expensive get_all_models() call + models = request.app.state.OPENAI_MODELS + if not models or model_id not in models: + await get_all_models(request, user=user) + models = request.app.state.OPENAI_MODELS + if model_id in models: + idx = models[model_id]['urlIdx'] + + url = request.app.state.config.OPENAI_API_BASE_URLS[idx] + key = request.app.state.config.OPENAI_API_KEYS[idx] + api_config = request.app.state.config.OPENAI_API_CONFIGS.get( + str(idx), + request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support + ) + + r = None + streaming = False + + headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user) + try: + session = await get_session() + r = await session.request( + method='POST', + url=f'{url}/embeddings', + data=body, + headers=headers, + cookies=cookies, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) + + if 'text/event-stream' in r.headers.get('Content-Type', ''): + streaming = True + return StreamingResponse( + stream_wrapper(r), + status_code=r.status, + headers=_clean_proxy_headers(r.headers), + ) + else: + try: + response_data = await r.json() + except Exception: + response_data = await r.text() + + if r.status >= 400: + if isinstance(response_data, (dict, list)): + return JSONResponse(status_code=r.status, content=response_data) + else: + return PlainTextResponse(status_code=r.status, content=response_data) + + return response_data + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=r.status if r else 500, + detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR, + ) + finally: + if not streaming: + await cleanup_response(r) + + +class ResponsesForm(BaseModel): + model_config = ConfigDict(extra='allow') + + model: str + input: Optional[list | str] = None + instructions: Optional[str] = None + stream: Optional[bool] = None + temperature: Optional[float] = None + max_output_tokens: Optional[int] = None + top_p: Optional[float] = None + tools: Optional[list] = None + tool_choice: Optional[str | dict] = None + text: Optional[dict] = None + truncation: Optional[str] = None + metadata: Optional[dict] = None + store: Optional[bool] = None + reasoning: Optional[dict] = None + previous_response_id: Optional[str] = None + + +@router.post('/responses') +async def responses( + request: Request, + form_data: ResponsesForm, + user=Depends(get_verified_user), +): + """ + Forward requests to the OpenAI Responses API endpoint. + Routes to the correct upstream backend based on the model field. + """ + payload = form_data.model_dump(exclude_none=True) + + idx = 0 + model_id = form_data.model + + # Enforce per-model access control + await check_model_access(user, await Models.get_model_by_id(model_id), BYPASS_MODEL_ACCESS_CONTROL) + + body = json.dumps(payload) + + if model_id: + models = request.app.state.OPENAI_MODELS + if not models or model_id not in models: + await get_all_models(request, user=user) + models = request.app.state.OPENAI_MODELS + if model_id in models: + idx = models[model_id]['urlIdx'] + + url = request.app.state.config.OPENAI_API_BASE_URLS[idx] + key = request.app.state.config.OPENAI_API_KEYS[idx] + api_config = request.app.state.config.OPENAI_API_CONFIGS.get( + str(idx), + request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support + ) + + r = None + streaming = False + + try: + headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user) + + if api_config.get('azure', False): + auth_type = api_config.get('auth_type', 'bearer') + if auth_type not in ('azure_ad', 'microsoft_entra_id'): + headers['api-key'] = key + + is_azure_v1 = bool(re.search(r'/openai/v1(?:/|$)', url)) + + if is_azure_v1: + request_url = f'{url.rstrip("/")}/responses' + else: + api_version = api_config.get('api_version', '2023-03-15-preview') + headers['api-version'] = api_version + model = _sanitize_model_for_url(payload.get('model', '')) + request_url = f'{url}/openai/deployments/{model}/responses?api-version={api_version}' + else: + request_url = f'{url}/responses' + + session = await get_session() + r = await session.request( + method='POST', + url=request_url, + data=body, + headers=headers, + cookies=cookies, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) + + # Check if response is SSE + if 'text/event-stream' in r.headers.get('Content-Type', ''): + streaming = True + return StreamingResponse( + stream_wrapper(r), + status_code=r.status, + headers=_clean_proxy_headers(r.headers), + ) + else: + try: + response_data = await r.json() + except Exception: + response_data = await r.text() + + if r.status >= 400: + if isinstance(response_data, (dict, list)): + return JSONResponse(status_code=r.status, content=response_data) + else: + return PlainTextResponse(status_code=r.status, content=response_data) + + return response_data + + except HTTPException: + raise + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=r.status if r else 500, + detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR, + ) + finally: + if not streaming: + await cleanup_response(r) + + +@router.api_route('/{path:path}', methods=['GET', 'POST', 'PUT', 'DELETE']) +async def proxy(path: str, request: Request, user=Depends(get_verified_user)): + """ + Deprecated: proxy all requests to OpenAI API. + Disabled by default. Set ENABLE_OPENAI_API_PASSTHROUGH=True to enable. + """ + + if not ENABLE_OPENAI_API_PASSTHROUGH: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail='Direct API passthrough is disabled. Set ENABLE_OPENAI_API_PASSTHROUGH=True to enable.', + ) + + body = await request.body() + + # Parse JSON body to resolve model-based routing + payload = None + if body: + try: + payload = json.loads(body) + except (json.JSONDecodeError, ValueError): + payload = None + + idx = 0 + model_id = payload.get('model') if isinstance(payload, dict) else None + if model_id: + models = request.app.state.OPENAI_MODELS + if not models or model_id not in models: + await get_all_models(request, user=user) + models = request.app.state.OPENAI_MODELS + if model_id in models: + idx = models[model_id]['urlIdx'] + + url = request.app.state.config.OPENAI_API_BASE_URLS[idx] + key = request.app.state.config.OPENAI_API_KEYS[idx] + api_config = request.app.state.config.OPENAI_API_CONFIGS.get( + str(idx), + request.app.state.config.OPENAI_API_CONFIGS.get( + request.app.state.config.OPENAI_API_BASE_URLS[idx], {} + ), # Legacy support + ) + + r = None + streaming = False + + try: + headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user) + + if api_config.get('azure', False): + # Only set api-key header if not using Azure Entra ID authentication + auth_type = api_config.get('auth_type', 'bearer') + if auth_type not in ('azure_ad', 'microsoft_entra_id'): + headers['api-key'] = key + + is_azure_v1 = bool(re.search(r'/openai/v1(?:/|$)', url)) + + if is_azure_v1: + qs = request.url.query + request_url = f'{url.rstrip("/")}/{path}' + (f'?{qs}' if qs else '') + else: + api_version = api_config.get('api_version', '2023-03-15-preview') + headers['api-version'] = api_version + + payload = json.loads(body) + url, payload = convert_to_azure_payload(url, payload, api_version) + body = json.dumps(payload).encode() + + request_url = f'{url}/{path}?api-version={api_version}' + else: + request_url = f'{url}/{path}' + + session = await get_session() + r = await session.request( + method=request.method, + url=request_url, + data=body, + headers=headers, + cookies=cookies, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) + + # Check if response is SSE + if 'text/event-stream' in r.headers.get('Content-Type', ''): + streaming = True + return StreamingResponse( + stream_wrapper(r), + status_code=r.status, + headers=_clean_proxy_headers(r.headers), + ) + else: + try: + response_data = await r.json() + except Exception: + response_data = await r.text() + + if r.status >= 400: + if isinstance(response_data, (dict, list)): + return JSONResponse(status_code=r.status, content=response_data) + else: + return PlainTextResponse(status_code=r.status, content=response_data) + + return response_data + + except HTTPException: + raise + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=r.status if r else 500, + detail='Open WebUI: Server Connection Error', + ) + finally: + if not streaming: + await cleanup_response(r) diff --git a/backend/open_webui/routers/pipelines.py b/backend/open_webui/routers/pipelines.py new file mode 100644 index 0000000000000000000000000000000000000000..580fb42fb2dfd74aedb87f9c2b82a1a575ccb5c4 --- /dev/null +++ b/backend/open_webui/routers/pipelines.py @@ -0,0 +1,536 @@ +from fastapi import ( + Depends, + FastAPI, + File, + Form, + HTTPException, + Request, + UploadFile, + status, + APIRouter, +) +import aiohttp +import os +import logging +import shutil +from pydantic import BaseModel +from starlette.responses import FileResponse +from typing import Optional + +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL +from open_webui.config import CACHE_DIR +from open_webui.constants import ERROR_MESSAGES + + +from open_webui.routers.openai import get_all_models_responses + +from open_webui.utils.auth import get_admin_user + +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): + filters = [ + model + for model in models.values() + if 'pipeline' in model + and 'type' in model['pipeline'] + and model['pipeline']['type'] == 'filter' + and ( + model['pipeline']['pipelines'] == ['*'] + or any(model_id == target_model_id for target_model_id in model['pipeline']['pipelines']) + ) + ] + sorted_filters = sorted(filters, key=lambda x: x['pipeline']['priority']) + return sorted_filters + + +async def process_pipeline_inlet_filter(request, payload, user, models): + user = {'id': user.id, 'email': user.email, 'name': user.name, 'role': user.role} + model_id = payload['model'] + sorted_filters = get_sorted_filters(model_id, models) + model = models[model_id] + + if 'pipeline' in model: + sorted_filters.append(model) + + async with aiohttp.ClientSession(trust_env=True) as session: + for filter in sorted_filters: + urlIdx = filter.get('urlIdx') + + try: + urlIdx = int(urlIdx) + except Exception: + continue + + url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] + key = request.app.state.config.OPENAI_API_KEYS[urlIdx] + + if not key: + continue + + headers = {'Authorization': f'Bearer {key}'} + request_data = { + 'user': user, + 'body': payload, + } + + try: + async with session.post( + f'{url}/{filter["id"]}/filter/inlet', + headers=headers, + json=request_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + payload = await response.json() + except aiohttp.ClientResponseError as e: + try: + res = await response.json() if 'application/json' in response.content_type else {} + if 'detail' in res: + raise HTTPException( + status_code=response.status, + detail=res['detail'], + ) + except HTTPException: + raise + except Exception: + pass + + raise HTTPException( + status_code=response.status, + detail=e.message, + ) + except HTTPException: + raise + except Exception as e: + log.exception(f'Connection error: {e}') + + return payload + + +async def process_pipeline_outlet_filter(request, payload, user, models): + user = {'id': user.id, 'email': user.email, 'name': user.name, 'role': user.role} + model_id = payload['model'] + sorted_filters = get_sorted_filters(model_id, models) + model = models[model_id] + + if 'pipeline' in model: + sorted_filters = [model] + sorted_filters + + async with aiohttp.ClientSession(trust_env=True) as session: + for filter in sorted_filters: + urlIdx = filter.get('urlIdx') + + try: + urlIdx = int(urlIdx) + except Exception: + continue + + url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] + key = request.app.state.config.OPENAI_API_KEYS[urlIdx] + + if not key: + continue + + headers = {'Authorization': f'Bearer {key}'} + request_data = { + 'user': user, + 'body': payload, + } + + try: + async with session.post( + f'{url}/{filter["id"]}/filter/outlet', + headers=headers, + json=request_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + payload = await response.json() + except aiohttp.ClientResponseError as e: + try: + res = await response.json() if 'application/json' in response.content_type else {} + if 'detail' in res: + raise HTTPException( + status_code=response.status, + detail=res['detail'], + ) + except HTTPException: + raise + except Exception: + pass + + raise HTTPException( + status_code=response.status, + detail=e.message, + ) + except HTTPException: + raise + except Exception as e: + log.exception(f'Connection error: {e}') + + return payload + + +################################## +# +# Pipelines Endpoints +# +################################## + +router = APIRouter() + + +@router.get('/list') +async def get_pipelines_list(request: Request, user=Depends(get_admin_user)): + responses = await get_all_models_responses(request, user) + log.debug(f'get_pipelines_list: get_openai_models_responses returned {responses}') + + urlIdxs = [idx for idx, response in enumerate(responses) if response is not None and 'pipelines' in response] + + return { + 'data': [ + { + 'url': request.app.state.config.OPENAI_API_BASE_URLS[urlIdx], + 'idx': urlIdx, + } + for urlIdx in urlIdxs + ] + } + + +@router.post('/upload') +async def upload_pipeline( + request: Request, + urlIdx: int = Form(...), + file: UploadFile = File(...), + user=Depends(get_admin_user), +): + log.info(f'upload_pipeline: urlIdx={urlIdx}, filename={file.filename}') + filename = os.path.basename(file.filename) + + # Check if the uploaded file is a python file + if not (filename and filename.endswith('.py')): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Only Python (.py) files are allowed.', + ) + + upload_folder = f'{CACHE_DIR}/pipelines' + os.makedirs(upload_folder, exist_ok=True) + file_path = os.path.join(upload_folder, filename) + + response = None + try: + # Save the uploaded file + with open(file_path, 'wb') as buffer: + shutil.copyfileobj(file.file, buffer) + + url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] + key = request.app.state.config.OPENAI_API_KEYS[urlIdx] + + headers = {'Authorization': f'Bearer {key}'} + + async with aiohttp.ClientSession(trust_env=True) as session: + with open(file_path, 'rb') as f: + form_data = aiohttp.FormData() + form_data.add_field( + 'file', + f, + filename=filename, + content_type='application/octet-stream', + ) + + async with session.post( + f'{url}/pipelines/upload', + headers=headers, + data=form_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() + + return {**data} + except Exception as e: + # Handle connection error here + log.exception(f'Connection error: {e}') + + detail = None + status_code = status.HTTP_404_NOT_FOUND + if response is not None: + status_code = response.status + try: + res = await response.json() + if 'detail' in res: + detail = res['detail'] + except Exception: + pass + + raise HTTPException( + status_code=status_code, + detail=detail if detail else 'Pipeline not found', + ) + finally: + # Ensure the file is deleted after the upload is completed or on failure + if os.path.exists(file_path): + os.remove(file_path) + + +class AddPipelineForm(BaseModel): + url: str + urlIdx: int + + +@router.post('/add') +async def add_pipeline(request: Request, form_data: AddPipelineForm, user=Depends(get_admin_user)): + response = None + try: + urlIdx = form_data.urlIdx + + url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] + key = request.app.state.config.OPENAI_API_KEYS[urlIdx] + + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.post( + f'{url}/pipelines/add', + headers={'Authorization': f'Bearer {key}'}, + json={'url': form_data.url}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() + + return {**data} + except Exception as e: + # Handle connection error here + log.exception(f'Connection error: {e}') + + detail = None + if response is not None: + try: + res = await response.json() + if 'detail' in res: + detail = res['detail'] + except Exception: + pass + + raise HTTPException( + status_code=(response.status if response is not None else status.HTTP_404_NOT_FOUND), + detail=detail if detail else 'Pipeline not found', + ) + + +class DeletePipelineForm(BaseModel): + id: str + urlIdx: int + + +@router.delete('/delete') +async def delete_pipeline(request: Request, form_data: DeletePipelineForm, user=Depends(get_admin_user)): + response = None + try: + urlIdx = form_data.urlIdx + + url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] + key = request.app.state.config.OPENAI_API_KEYS[urlIdx] + + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.delete( + f'{url}/pipelines/delete', + headers={'Authorization': f'Bearer {key}'}, + json={'id': form_data.id}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() + + return {**data} + except Exception as e: + # Handle connection error here + log.exception(f'Connection error: {e}') + + detail = None + if response is not None: + try: + res = await response.json() + if 'detail' in res: + detail = res['detail'] + except Exception: + pass + + raise HTTPException( + status_code=(response.status if response is not None else status.HTTP_404_NOT_FOUND), + detail=detail if detail else 'Pipeline not found', + ) + + +@router.get('/') +async def get_pipelines(request: Request, urlIdx: Optional[int] = None, user=Depends(get_admin_user)): + response = None + try: + url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] + key = request.app.state.config.OPENAI_API_KEYS[urlIdx] + + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get( + f'{url}/pipelines', + headers={'Authorization': f'Bearer {key}'}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() + + return {**data} + except Exception as e: + # Handle connection error here + log.exception(f'Connection error: {e}') + + detail = None + if response is not None: + try: + res = await response.json() + if 'detail' in res: + detail = res['detail'] + except Exception: + pass + + raise HTTPException( + status_code=(response.status if response is not None else status.HTTP_404_NOT_FOUND), + detail=detail if detail else 'Pipeline not found', + ) + + +@router.get('/{pipeline_id}/valves') +async def get_pipeline_valves( + request: Request, + urlIdx: Optional[int], + pipeline_id: str, + user=Depends(get_admin_user), +): + response = None + try: + url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] + key = request.app.state.config.OPENAI_API_KEYS[urlIdx] + + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get( + f'{url}/{pipeline_id}/valves', + headers={'Authorization': f'Bearer {key}'}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() + + return {**data} + except Exception as e: + # Handle connection error here + log.exception(f'Connection error: {e}') + + detail = None + if response is not None: + try: + res = await response.json() + if 'detail' in res: + detail = res['detail'] + except Exception: + pass + + raise HTTPException( + status_code=(response.status if response is not None else status.HTTP_404_NOT_FOUND), + detail=detail if detail else 'Pipeline not found', + ) + + +@router.get('/{pipeline_id}/valves/spec') +async def get_pipeline_valves_spec( + request: Request, + urlIdx: Optional[int], + pipeline_id: str, + user=Depends(get_admin_user), +): + response = None + try: + url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] + key = request.app.state.config.OPENAI_API_KEYS[urlIdx] + + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get( + f'{url}/{pipeline_id}/valves/spec', + headers={'Authorization': f'Bearer {key}'}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() + + return {**data} + except Exception as e: + # Handle connection error here + log.exception(f'Connection error: {e}') + + detail = None + if response is not None: + try: + res = await response.json() + if 'detail' in res: + detail = res['detail'] + except Exception: + pass + + raise HTTPException( + status_code=(response.status if response is not None else status.HTTP_404_NOT_FOUND), + detail=detail if detail else 'Pipeline not found', + ) + + +@router.post('/{pipeline_id}/valves/update') +async def update_pipeline_valves( + request: Request, + urlIdx: Optional[int], + pipeline_id: str, + form_data: dict, + user=Depends(get_admin_user), +): + response = None + try: + url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] + key = request.app.state.config.OPENAI_API_KEYS[urlIdx] + + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.post( + f'{url}/{pipeline_id}/valves/update', + headers={'Authorization': f'Bearer {key}'}, + json={**form_data}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() + + return {**data} + except Exception as e: + # Handle connection error here + log.exception(f'Connection error: {e}') + + detail = None + + if response is not None: + try: + res = await response.json() + if 'detail' in res: + detail = res['detail'] + except Exception: + pass + + raise HTTPException( + status_code=(response.status if response is not None else status.HTTP_404_NOT_FOUND), + detail=detail if detail else 'Pipeline not found', + ) diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py new file mode 100644 index 0000000000000000000000000000000000000000..11901fc5a75822aba7c44b95821853470ac3bbca --- /dev/null +++ b/backend/open_webui/routers/prompts.py @@ -0,0 +1,757 @@ +from typing import Optional +from fastapi import APIRouter, Depends, HTTPException, status, Request + +from open_webui.models.prompts import ( + PromptForm, + PromptUserResponse, + PromptAccessResponse, + PromptAccessListResponse, + PromptModel, + Prompts, +) +from open_webui.models.access_grants import AccessGrants +from open_webui.models.groups import Groups +from open_webui.models.prompt_history import ( + PromptHistories, + PromptHistoryModel, + PromptHistoryResponse, +) +from open_webui.constants import ERROR_MESSAGES +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.access_control import has_permission, filter_allowed_access_grants +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL +from open_webui.internal.db import get_async_session +from sqlalchemy.ext.asyncio import AsyncSession +from pydantic import BaseModel + + +class PromptVersionUpdateForm(BaseModel): + version_id: str + + +class PromptMetadataForm(BaseModel): + name: str + command: str + tags: Optional[list[str]] = None + + +router = APIRouter() + +PAGE_ITEM_COUNT = 30 + + +############################ +# GetPrompts +# 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]) +async def get_prompts(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: + prompts = await Prompts.get_prompts(db=db) + else: + prompts = await Prompts.get_prompts_by_user_id(user.id, 'read', db=db) + + return prompts + + +@router.get('/tags', response_model=list[str]) +async def get_prompt_tags(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: + return await Prompts.get_tags(db=db) + else: + prompts = await Prompts.get_prompts_by_user_id(user.id, 'read', db=db) + tags = set() + for prompt in prompts: + if prompt.tags: + tags.update(prompt.tags) + return sorted(list(tags)) + + +@router.get('/list', response_model=PromptAccessListResponse) +async def get_prompt_list( + query: Optional[str] = None, + view_option: Optional[str] = None, + tag: Optional[str] = None, + order_by: Optional[str] = None, + direction: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + limit = PAGE_ITEM_COUNT + + page = max(1, page) + skip = (page - 1) * limit + + filter = {} + if query: + filter['query'] = query + if view_option: + filter['view_option'] = view_option + if tag: + filter['tag'] = tag + if order_by: + filter['order_by'] = order_by + if direction: + filter['direction'] = direction + + # Pre-fetch user group IDs once - used for both filter and write_access check + groups = await Groups.get_groups_by_member_id(user.id, db=db) + user_group_ids = {group.id for group in groups} + + if not (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL): + if groups: + filter['group_ids'] = [group.id for group in groups] + + filter['user_id'] = user.id + + result = await Prompts.search_prompts(user.id, filter=filter, skip=skip, limit=limit, db=db) + + # Batch-fetch writable prompt IDs in a single query instead of N has_access calls + prompt_ids = [prompt.id for prompt in result.items] + writable_prompt_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type='prompt', + resource_ids=prompt_ids, + permission='write', + user_group_ids=user_group_ids, + db=db, + ) + + return PromptAccessListResponse( + items=[ + PromptAccessResponse( + **prompt.model_dump(), + write_access=( + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == prompt.user_id + or prompt.id in writable_prompt_ids + ), + ) + for prompt in result.items + ], + total=result.total, + ) + + +############################ +# CreateNewPrompt +############################ + + +@router.post('/create', response_model=Optional[PromptModel]) +async def create_new_prompt( + request: Request, + form_data: PromptForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not ( + await has_permission( + user.id, + 'workspace.prompts', + request.app.state.config.USER_PERMISSIONS, + db=db, + ) + or await has_permission( + user.id, + 'workspace.prompts_import', + request.app.state.config.USER_PERMISSIONS, + db=db, + ) + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_prompts', + ) + + prompt = await Prompts.get_prompt_by_command(form_data.command, db=db) + if prompt is None: + prompt = await Prompts.insert_new_prompt(user.id, form_data, db=db) + + if prompt: + return prompt + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(), + ) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.COMMAND_TAKEN, + ) + + +############################ +# GetPromptByCommand +############################ + + +@router.get('/command/{command}', response_model=Optional[PromptAccessResponse]) +async def get_prompt_by_command( + command: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + prompt = await Prompts.get_prompt_by_command(command, db=db) + + if prompt: + if ( + user.role == 'admin' + or prompt.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='read', + db=db, + ) + ): + return PromptAccessResponse( + **prompt.model_dump(), + write_access=( + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == prompt.user_id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='write', + db=db, + ) + ), + ) + + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# GetPromptById +############################ + + +@router.get('/id/{prompt_id}', response_model=Optional[PromptAccessResponse]) +async def get_prompt_by_id( + prompt_id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + + if prompt: + if ( + user.role == 'admin' + or prompt.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='read', + db=db, + ) + ): + return PromptAccessResponse( + **prompt.model_dump(), + write_access=( + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == prompt.user_id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='write', + db=db, + ) + ), + ) + + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# UpdatePromptById +############################ + + +@router.post('/id/{prompt_id}/update', response_model=Optional[PromptModel]) +async def update_prompt_by_id( + request: Request, + prompt_id: str, + form_data: PromptForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + # Is the user the original creator, in a group with write access, or an admin + if ( + prompt.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + # Check for command collision if command is being changed + if form_data.command != prompt.command: + existing_prompt = await Prompts.get_prompt_by_command(form_data.command, db=db) + if existing_prompt and existing_prompt.id != prompt.id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.COMMAND_TAKEN, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_prompts', + ) + + # Use the ID from the found prompt + updated_prompt = await Prompts.update_prompt_by_id(prompt.id, form_data, user.id, db=db) + if updated_prompt: + return updated_prompt + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + +############################ +# UpdatePromptMetadata +############################ + + +@router.post('/id/{prompt_id}/update/meta', response_model=Optional[PromptModel]) +async def update_prompt_metadata( + prompt_id: str, + form_data: PromptMetadataForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """Update prompt name and command only (no history created).""" + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + prompt.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + # Check for command collision if command is being changed + if form_data.command != prompt.command: + existing_prompt = await Prompts.get_prompt_by_command(form_data.command, db=db) + if existing_prompt and existing_prompt.id != prompt.id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.COMMAND_TAKEN, + ) + + updated_prompt = await Prompts.update_prompt_metadata( + prompt.id, form_data.name, form_data.command, form_data.tags, db=db + ) + if updated_prompt: + return updated_prompt + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + +@router.post('/id/{prompt_id}/update/version', response_model=Optional[PromptModel]) +async def set_prompt_version( + prompt_id: str, + form_data: PromptVersionUpdateForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + prompt.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + updated_prompt = await Prompts.update_prompt_version(prompt.id, form_data.version_id, db=db) + if updated_prompt: + return updated_prompt + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + +############################ +# UpdatePromptAccessById +############################ + + +class PromptAccessGrantsForm(BaseModel): + access_grants: list[dict] + + +@router.post('/id/{prompt_id}/access/update', response_model=Optional[PromptModel]) +async def update_prompt_access_by_id( + request: Request, + prompt_id: str, + form_data: PromptAccessGrantsForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + prompt.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_prompts', + ) + + await AccessGrants.set_access_grants('prompt', prompt_id, form_data.access_grants, db=db) + + return await Prompts.get_prompt_by_id(prompt_id, db=db) + + +############################ +# TogglePromptActiveById +############################ + + +@router.post('/id/{prompt_id}/toggle', response_model=Optional[PromptModel]) +async def toggle_prompt_active( + prompt_id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + prompt.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + result = await Prompts.toggle_prompt_active(prompt.id, db=db) + if result: + return result + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + +############################ +# DeletePromptById +############################ + + +@router.delete('/id/{prompt_id}/delete', response_model=bool) +async def delete_prompt_by_id( + prompt_id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + prompt.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + result = await Prompts.delete_prompt_by_id(prompt.id, db=db) + return result + + +############################ +# Prompt History Endpoints +############################ + + +@router.get('/id/{prompt_id}/history', response_model=list[PromptHistoryResponse]) +async def get_prompt_history( + prompt_id: str, + page: int = 0, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """Get version history for a prompt.""" + PAGE_SIZE = 20 + + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + # Check read access + if not ( + user.role == 'admin' + or prompt.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='read', + db=db, + ) + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + history = await PromptHistories.get_history_by_prompt_id(prompt.id, limit=PAGE_SIZE, offset=page * PAGE_SIZE, db=db) + return history + + +@router.get('/id/{prompt_id}/history/{history_id}', response_model=PromptHistoryModel) +async def get_prompt_history_entry( + prompt_id: str, + history_id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """Get a specific version from history.""" + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + # Check read access + if not ( + user.role == 'admin' + or prompt.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='read', + db=db, + ) + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + history_entry = await PromptHistories.get_history_entry_by_id(history_id, db=db) + if not history_entry or history_entry.prompt_id != prompt.id: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + return history_entry + + +@router.delete('/id/{prompt_id}/history/{history_id}', response_model=bool) +async def delete_prompt_history_entry( + prompt_id: str, + history_id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """Delete a history entry. Cannot delete the active production version.""" + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + # Check write access + if not ( + user.role == 'admin' + or prompt.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='write', + db=db, + ) + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + # Cannot delete active production version + if prompt.version_id == history_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Cannot delete the active production version', + ) + + success = await PromptHistories.delete_history_entry(history_id, db=db) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + return success + + +@router.get('/id/{prompt_id}/history/diff') +async def get_prompt_diff( + prompt_id: str, + from_id: str, + to_id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """Get diff between two versions.""" + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + # Check read access + if not ( + user.role == 'admin' + or prompt.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='read', + db=db, + ) + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + diff = await PromptHistories.compute_diff(from_id, to_id, db=db) + if not diff: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + return diff diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py new file mode 100644 index 0000000000000000000000000000000000000000..01ef1d88860921ea2282f4930fe4d04b2d384ed1 --- /dev/null +++ b/backend/open_webui/routers/retrieval.py @@ -0,0 +1,2700 @@ +import json +import logging +import mimetypes +import os +import shutil +import asyncio + +import re +import uuid +from datetime import datetime +from pathlib import Path +from typing import Iterator, List, Optional, Sequence, Union + +from fastapi import ( + Depends, + FastAPI, + Query, + File, + Form, + HTTPException, + UploadFile, + Request, + status, + APIRouter, +) +from fastapi.middleware.cors import CORSMiddleware +from fastapi.concurrency import run_in_threadpool +from pydantic import BaseModel +import tiktoken + + +from langchain_text_splitters import ( + RecursiveCharacterTextSplitter, + TokenTextSplitter, + MarkdownHeaderTextSplitter, +) +from langchain_core.documents import Document + +from open_webui.models.files import FileModel, FileUpdateForm, Files +from open_webui.utils.access_control.files import has_access_to_file +from open_webui.models.knowledge import Knowledges +from open_webui.storage.provider import Storage +from open_webui.internal.db import get_async_db, get_async_session +from sqlalchemy.ext.asyncio import AsyncSession + + +from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT +from open_webui.retrieval.vector.async_client import ASYNC_VECTOR_DB_CLIENT + +# Document loaders + +from open_webui.retrieval.loaders.youtube import YoutubeLoader + +# Web search engines +from open_webui.retrieval.web.main import SearchResult +from open_webui.retrieval.web.utils import get_web_loader +from open_webui.retrieval.web.ollama import search_ollama_cloud +from open_webui.retrieval.web.perplexity_search import search_perplexity_search +from open_webui.retrieval.web.brave import search_brave +from open_webui.retrieval.web.kagi import search_kagi +from open_webui.retrieval.web.mojeek import search_mojeek +from open_webui.retrieval.web.bocha import search_bocha +from open_webui.retrieval.web.duckduckgo import search_duckduckgo +from open_webui.retrieval.web.google_pse import search_google_pse +from open_webui.retrieval.web.jina_search import search_jina +from open_webui.retrieval.web.searchapi import search_searchapi +from open_webui.retrieval.web.serpapi import search_serpapi +from open_webui.retrieval.web.searxng import search_searxng +from open_webui.retrieval.web.yacy import search_yacy +from open_webui.retrieval.web.serper import search_serper +from open_webui.retrieval.web.serply import search_serply +from open_webui.retrieval.web.serpstack import search_serpstack +from open_webui.retrieval.web.tavily import search_tavily +from open_webui.retrieval.web.bing import search_bing +from open_webui.retrieval.web.azure import search_azure +from open_webui.retrieval.web.exa import search_exa +from open_webui.retrieval.web.perplexity import search_perplexity +from open_webui.retrieval.web.sougou import search_sougou +from open_webui.retrieval.web.firecrawl import search_firecrawl +from open_webui.retrieval.web.external import search_external +from open_webui.retrieval.web.yandex import search_yandex +from open_webui.retrieval.web.ydc import search_youcom + +from open_webui.retrieval.utils import ( + build_loader_from_config, + filter_accessible_collections, + get_content_from_url, + get_embedding_function, + get_reranking_function, + get_model_path, + query_collection, + query_collection_with_hybrid_search, + query_doc, + query_doc_with_hybrid_search, +) +from open_webui.retrieval.vector.utils import filter_metadata +from open_webui.utils.misc import ( + calculate_sha256_string, + sanitize_text_for_db, +) +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.access_control import has_permission + +from open_webui.config import ( + ENV, + RAG_EMBEDDING_MODEL_AUTO_UPDATE, + RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE, + RAG_RERANKING_MODEL_AUTO_UPDATE, + RAG_RERANKING_MODEL_TRUST_REMOTE_CODE, + UPLOAD_DIR, + DEFAULT_LOCALE, + RAG_EMBEDDING_CONTENT_PREFIX, + RAG_EMBEDDING_QUERY_PREFIX, +) +from open_webui.env import ( + DEVICE_TYPE, + DOCKER, + RAG_EMBEDDING_TIMEOUT, + SENTENCE_TRANSFORMERS_BACKEND, + SENTENCE_TRANSFORMERS_MODEL_KWARGS, + SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND, + SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS, + SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION, +) + +from open_webui.constants import ERROR_MESSAGES + +log = logging.getLogger(__name__) + +########################################## +# +# Utility functions +# Give us this day our relevant chunks, and lead us +# not into hallucination, but deliver us from noise. +# +########################################## + + +def get_ef( + engine: str, + embedding_model: str, + auto_update: bool = RAG_EMBEDDING_MODEL_AUTO_UPDATE, +): + ef = None + if embedding_model and engine == '': + from sentence_transformers import SentenceTransformer + + try: + ef = SentenceTransformer( + get_model_path(embedding_model, auto_update), + device=DEVICE_TYPE, + trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE, + backend=SENTENCE_TRANSFORMERS_BACKEND, + model_kwargs=SENTENCE_TRANSFORMERS_MODEL_KWARGS, + ) + except Exception as e: + log.error(f'Error loading SentenceTransformer: {e}') + + return ef + + +def get_rf( + engine: str = '', + reranking_model: Optional[str] = None, + external_reranker_url: str = '', + external_reranker_api_key: str = '', + external_reranker_timeout: str = '', + auto_update: bool = RAG_RERANKING_MODEL_AUTO_UPDATE, +): + rf = None + # Convert timeout string to int or None (system default) + timeout_value = int(external_reranker_timeout) if external_reranker_timeout else None + if reranking_model: + if any(model in reranking_model for model in ['jinaai/jina-colbert-v2']): + try: + from open_webui.retrieval.models.colbert import ColBERT + + rf = ColBERT( + get_model_path(reranking_model, auto_update), + env='docker' if DOCKER else None, + ) + + except Exception as e: + log.error(f'ColBERT: {e}') + raise Exception(ERROR_MESSAGES.DEFAULT(e)) + else: + if engine == 'external': + try: + from open_webui.retrieval.models.external import ExternalReranker + + rf = ExternalReranker( + url=external_reranker_url, + api_key=external_reranker_api_key, + model=reranking_model, + timeout=timeout_value, + ) + except Exception as e: + log.error(f'ExternalReranking: {e}') + raise Exception(ERROR_MESSAGES.DEFAULT(e)) + else: + import sentence_transformers + import torch + + try: + rf = sentence_transformers.CrossEncoder( + get_model_path(reranking_model, auto_update), + device=DEVICE_TYPE, + trust_remote_code=RAG_RERANKING_MODEL_TRUST_REMOTE_CODE, + backend=SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND, + model_kwargs=SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS, + activation_fn=( + torch.nn.Sigmoid() + if SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION + else None + ), + ) + except Exception as e: + log.error(f'CrossEncoder: {e}') + raise Exception(ERROR_MESSAGES.DEFAULT('CrossEncoder error')) + + # Safely adjust pad_token_id if missing as some models do not have this in config + try: + model_cfg = getattr(rf, 'model', None) + if model_cfg and hasattr(model_cfg, 'config'): + cfg = model_cfg.config + if getattr(cfg, 'pad_token_id', None) is None: + # Fallback to eos_token_id when available + eos = getattr(cfg, 'eos_token_id', None) + if eos is not None: + cfg.pad_token_id = eos + log.debug(f'Missing pad_token_id detected; set to eos_token_id={eos}') + else: + log.warning('Neither pad_token_id nor eos_token_id present in model config') + except Exception as e2: + log.warning(f'Failed to adjust pad_token_id on CrossEncoder: {e2}') + + return rf + + +########################################## +# +# API routes +# +########################################## + + +router = APIRouter() + + +class CollectionNameForm(BaseModel): + collection_name: Optional[str] = None + + +class ProcessUrlForm(CollectionNameForm): + url: str + + +class SearchForm(BaseModel): + queries: List[str] + + +@router.get('/') +async def get_status(request: Request): + return { + 'status': True, + 'CHUNK_SIZE': request.app.state.config.CHUNK_SIZE, + 'CHUNK_OVERLAP': request.app.state.config.CHUNK_OVERLAP, + 'RAG_TEMPLATE': request.app.state.config.RAG_TEMPLATE, + 'RAG_EMBEDDING_ENGINE': request.app.state.config.RAG_EMBEDDING_ENGINE, + 'RAG_EMBEDDING_MODEL': request.app.state.config.RAG_EMBEDDING_MODEL, + 'RAG_RERANKING_MODEL': request.app.state.config.RAG_RERANKING_MODEL, + 'RAG_EMBEDDING_BATCH_SIZE': request.app.state.config.RAG_EMBEDDING_BATCH_SIZE, + 'ENABLE_ASYNC_EMBEDDING': request.app.state.config.ENABLE_ASYNC_EMBEDDING, + 'RAG_EMBEDDING_CONCURRENT_REQUESTS': request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, + } + + +@router.get('/embedding') +async def get_embedding_config(request: Request, user=Depends(get_admin_user)): + return { + 'status': True, + 'RAG_EMBEDDING_ENGINE': request.app.state.config.RAG_EMBEDDING_ENGINE, + 'RAG_EMBEDDING_MODEL': request.app.state.config.RAG_EMBEDDING_MODEL, + 'RAG_EMBEDDING_BATCH_SIZE': request.app.state.config.RAG_EMBEDDING_BATCH_SIZE, + 'ENABLE_ASYNC_EMBEDDING': request.app.state.config.ENABLE_ASYNC_EMBEDDING, + 'RAG_EMBEDDING_CONCURRENT_REQUESTS': request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, + 'openai_config': { + 'url': request.app.state.config.RAG_OPENAI_API_BASE_URL, + 'key': request.app.state.config.RAG_OPENAI_API_KEY, + }, + 'ollama_config': { + 'url': request.app.state.config.RAG_OLLAMA_BASE_URL, + 'key': request.app.state.config.RAG_OLLAMA_API_KEY, + }, + 'azure_openai_config': { + 'url': request.app.state.config.RAG_AZURE_OPENAI_BASE_URL, + 'key': request.app.state.config.RAG_AZURE_OPENAI_API_KEY, + 'version': request.app.state.config.RAG_AZURE_OPENAI_API_VERSION, + }, + } + + +class OpenAIConfigForm(BaseModel): + url: str + key: str + + +class OllamaConfigForm(BaseModel): + url: str + key: str + + +class AzureOpenAIConfigForm(BaseModel): + url: str + key: str + version: str + + +class EmbeddingModelUpdateForm(BaseModel): + openai_config: Optional[OpenAIConfigForm] = None + ollama_config: Optional[OllamaConfigForm] = None + azure_openai_config: Optional[AzureOpenAIConfigForm] = None + RAG_EMBEDDING_ENGINE: str + RAG_EMBEDDING_MODEL: str + RAG_EMBEDDING_BATCH_SIZE: Optional[int] = 1 + ENABLE_ASYNC_EMBEDDING: Optional[bool] = True + RAG_EMBEDDING_CONCURRENT_REQUESTS: Optional[int] = 0 + + +def unload_embedding_model(request: Request): + if request.app.state.config.RAG_EMBEDDING_ENGINE == '': + # unloads current internal embedding model and clears VRAM cache + request.app.state.ef = None + request.app.state.EMBEDDING_FUNCTION = None + import gc + + gc.collect() + if DEVICE_TYPE == 'cuda': + import torch + + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + +@router.post('/embedding/update') +async def update_embedding_config(request: Request, form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user)): + log.info( + f'Updating embedding model: {request.app.state.config.RAG_EMBEDDING_MODEL} to {form_data.RAG_EMBEDDING_MODEL}' + ) + unload_embedding_model(request) + try: + request.app.state.config.RAG_EMBEDDING_ENGINE = form_data.RAG_EMBEDDING_ENGINE + request.app.state.config.RAG_EMBEDDING_MODEL = form_data.RAG_EMBEDDING_MODEL + request.app.state.config.RAG_EMBEDDING_BATCH_SIZE = form_data.RAG_EMBEDDING_BATCH_SIZE + request.app.state.config.ENABLE_ASYNC_EMBEDDING = form_data.ENABLE_ASYNC_EMBEDDING + request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS = form_data.RAG_EMBEDDING_CONCURRENT_REQUESTS + + if request.app.state.config.RAG_EMBEDDING_ENGINE in [ + 'ollama', + 'openai', + 'azure_openai', + ]: + if form_data.openai_config is not None: + request.app.state.config.RAG_OPENAI_API_BASE_URL = form_data.openai_config.url + request.app.state.config.RAG_OPENAI_API_KEY = form_data.openai_config.key + + if form_data.ollama_config is not None: + request.app.state.config.RAG_OLLAMA_BASE_URL = form_data.ollama_config.url + request.app.state.config.RAG_OLLAMA_API_KEY = form_data.ollama_config.key + + if form_data.azure_openai_config is not None: + request.app.state.config.RAG_AZURE_OPENAI_BASE_URL = form_data.azure_openai_config.url + request.app.state.config.RAG_AZURE_OPENAI_API_KEY = form_data.azure_openai_config.key + request.app.state.config.RAG_AZURE_OPENAI_API_VERSION = form_data.azure_openai_config.version + + request.app.state.ef = get_ef( + request.app.state.config.RAG_EMBEDDING_ENGINE, + request.app.state.config.RAG_EMBEDDING_MODEL, + ) + + request.app.state.EMBEDDING_FUNCTION = get_embedding_function( + request.app.state.config.RAG_EMBEDDING_ENGINE, + request.app.state.config.RAG_EMBEDDING_MODEL, + request.app.state.ef, + ( + request.app.state.config.RAG_OPENAI_API_BASE_URL + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'openai' + else ( + request.app.state.config.RAG_OLLAMA_BASE_URL + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'ollama' + else request.app.state.config.RAG_AZURE_OPENAI_BASE_URL + ) + ), + ( + request.app.state.config.RAG_OPENAI_API_KEY + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'openai' + else ( + request.app.state.config.RAG_OLLAMA_API_KEY + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'ollama' + else request.app.state.config.RAG_AZURE_OPENAI_API_KEY + ) + ), + request.app.state.config.RAG_EMBEDDING_BATCH_SIZE, + azure_api_version=( + request.app.state.config.RAG_AZURE_OPENAI_API_VERSION + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'azure_openai' + else None + ), + enable_async=request.app.state.config.ENABLE_ASYNC_EMBEDDING, + concurrent_requests=request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, + ) + + return { + 'status': True, + 'RAG_EMBEDDING_ENGINE': request.app.state.config.RAG_EMBEDDING_ENGINE, + 'RAG_EMBEDDING_MODEL': request.app.state.config.RAG_EMBEDDING_MODEL, + 'RAG_EMBEDDING_BATCH_SIZE': request.app.state.config.RAG_EMBEDDING_BATCH_SIZE, + 'ENABLE_ASYNC_EMBEDDING': request.app.state.config.ENABLE_ASYNC_EMBEDDING, + 'RAG_EMBEDDING_CONCURRENT_REQUESTS': request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, + 'openai_config': { + 'url': request.app.state.config.RAG_OPENAI_API_BASE_URL, + 'key': request.app.state.config.RAG_OPENAI_API_KEY, + }, + 'ollama_config': { + 'url': request.app.state.config.RAG_OLLAMA_BASE_URL, + 'key': request.app.state.config.RAG_OLLAMA_API_KEY, + }, + 'azure_openai_config': { + 'url': request.app.state.config.RAG_AZURE_OPENAI_BASE_URL, + 'key': request.app.state.config.RAG_AZURE_OPENAI_API_KEY, + 'version': request.app.state.config.RAG_AZURE_OPENAI_API_VERSION, + }, + } + except Exception as e: + log.exception(f'Problem updating embedding model: {e}') + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +@router.get('/config') +async def get_rag_config(request: Request, user=Depends(get_admin_user)): + return { + 'status': True, + # RAG settings + 'RAG_TEMPLATE': request.app.state.config.RAG_TEMPLATE, + 'TOP_K': request.app.state.config.TOP_K, + 'BYPASS_EMBEDDING_AND_RETRIEVAL': request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL, + 'RAG_FULL_CONTEXT': request.app.state.config.RAG_FULL_CONTEXT, + # Hybrid search settings + 'ENABLE_RAG_HYBRID_SEARCH': request.app.state.config.ENABLE_RAG_HYBRID_SEARCH, + 'ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS': request.app.state.config.ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS, + 'TOP_K_RERANKER': request.app.state.config.TOP_K_RERANKER, + 'RELEVANCE_THRESHOLD': request.app.state.config.RELEVANCE_THRESHOLD, + 'HYBRID_BM25_WEIGHT': request.app.state.config.HYBRID_BM25_WEIGHT, + # Content extraction settings + 'CONTENT_EXTRACTION_ENGINE': request.app.state.config.CONTENT_EXTRACTION_ENGINE, + 'PDF_EXTRACT_IMAGES': request.app.state.config.PDF_EXTRACT_IMAGES, + 'PDF_LOADER_MODE': request.app.state.config.PDF_LOADER_MODE, + 'DATALAB_MARKER_API_KEY': request.app.state.config.DATALAB_MARKER_API_KEY, + 'DATALAB_MARKER_API_BASE_URL': request.app.state.config.DATALAB_MARKER_API_BASE_URL, + 'DATALAB_MARKER_ADDITIONAL_CONFIG': request.app.state.config.DATALAB_MARKER_ADDITIONAL_CONFIG, + 'DATALAB_MARKER_SKIP_CACHE': request.app.state.config.DATALAB_MARKER_SKIP_CACHE, + 'DATALAB_MARKER_FORCE_OCR': request.app.state.config.DATALAB_MARKER_FORCE_OCR, + 'DATALAB_MARKER_PAGINATE': request.app.state.config.DATALAB_MARKER_PAGINATE, + 'DATALAB_MARKER_STRIP_EXISTING_OCR': request.app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR, + 'DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION': request.app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, + 'DATALAB_MARKER_FORMAT_LINES': request.app.state.config.DATALAB_MARKER_FORMAT_LINES, + 'DATALAB_MARKER_USE_LLM': request.app.state.config.DATALAB_MARKER_USE_LLM, + 'DATALAB_MARKER_OUTPUT_FORMAT': request.app.state.config.DATALAB_MARKER_OUTPUT_FORMAT, + 'EXTERNAL_DOCUMENT_LOADER_URL': request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL, + 'EXTERNAL_DOCUMENT_LOADER_API_KEY': request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY, + 'TIKA_SERVER_URL': request.app.state.config.TIKA_SERVER_URL, + 'DOCLING_SERVER_URL': request.app.state.config.DOCLING_SERVER_URL, + 'DOCLING_API_KEY': request.app.state.config.DOCLING_API_KEY, + 'DOCLING_PARAMS': request.app.state.config.DOCLING_PARAMS, + 'DOCUMENT_INTELLIGENCE_ENDPOINT': request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, + 'DOCUMENT_INTELLIGENCE_KEY': request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, + 'DOCUMENT_INTELLIGENCE_MODEL': request.app.state.config.DOCUMENT_INTELLIGENCE_MODEL, + 'MISTRAL_OCR_API_BASE_URL': request.app.state.config.MISTRAL_OCR_API_BASE_URL, + 'MISTRAL_OCR_API_KEY': request.app.state.config.MISTRAL_OCR_API_KEY, + 'PADDLEOCR_VL_BASE_URL': request.app.state.config.PADDLEOCR_VL_BASE_URL, + 'PADDLEOCR_VL_TOKEN': request.app.state.config.PADDLEOCR_VL_TOKEN, + # MinerU settings + 'MINERU_API_MODE': request.app.state.config.MINERU_API_MODE, + 'MINERU_API_URL': request.app.state.config.MINERU_API_URL, + 'MINERU_API_KEY': request.app.state.config.MINERU_API_KEY, + 'MINERU_API_TIMEOUT': request.app.state.config.MINERU_API_TIMEOUT, + 'MINERU_PARAMS': request.app.state.config.MINERU_PARAMS, + # Reranking settings + 'RAG_RERANKING_MODEL': request.app.state.config.RAG_RERANKING_MODEL, + 'RAG_RERANKING_ENGINE': request.app.state.config.RAG_RERANKING_ENGINE, + 'RAG_RERANKING_BATCH_SIZE': request.app.state.config.RAG_RERANKING_BATCH_SIZE, + 'RAG_EXTERNAL_RERANKER_URL': request.app.state.config.RAG_EXTERNAL_RERANKER_URL, + 'RAG_EXTERNAL_RERANKER_API_KEY': request.app.state.config.RAG_EXTERNAL_RERANKER_API_KEY, + 'RAG_EXTERNAL_RERANKER_TIMEOUT': request.app.state.config.RAG_EXTERNAL_RERANKER_TIMEOUT, + # Chunking settings + 'TEXT_SPLITTER': request.app.state.config.TEXT_SPLITTER, + 'ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER': request.app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER, + 'CHUNK_SIZE': request.app.state.config.CHUNK_SIZE, + 'CHUNK_MIN_SIZE_TARGET': request.app.state.config.CHUNK_MIN_SIZE_TARGET, + 'CHUNK_OVERLAP': request.app.state.config.CHUNK_OVERLAP, + # File upload settings + 'FILE_MAX_SIZE': request.app.state.config.FILE_MAX_SIZE, + 'FILE_MAX_COUNT': request.app.state.config.FILE_MAX_COUNT, + 'FILE_IMAGE_COMPRESSION_WIDTH': request.app.state.config.FILE_IMAGE_COMPRESSION_WIDTH, + 'FILE_IMAGE_COMPRESSION_HEIGHT': request.app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT, + 'ALLOWED_FILE_EXTENSIONS': request.app.state.config.ALLOWED_FILE_EXTENSIONS, + # Integration settings + 'ENABLE_GOOGLE_DRIVE_INTEGRATION': request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, + 'ENABLE_ONEDRIVE_INTEGRATION': request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION, + # Web search settings + 'web': { + 'ENABLE_WEB_SEARCH': request.app.state.config.ENABLE_WEB_SEARCH, + 'WEB_SEARCH_ENGINE': request.app.state.config.WEB_SEARCH_ENGINE, + 'WEB_SEARCH_TRUST_ENV': request.app.state.config.WEB_SEARCH_TRUST_ENV, + 'WEB_SEARCH_RESULT_COUNT': request.app.state.config.WEB_SEARCH_RESULT_COUNT, + 'WEB_SEARCH_CONCURRENT_REQUESTS': request.app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS, + 'WEB_FETCH_MAX_CONTENT_LENGTH': request.app.state.config.WEB_FETCH_MAX_CONTENT_LENGTH, + 'WEB_LOADER_CONCURRENT_REQUESTS': request.app.state.config.WEB_LOADER_CONCURRENT_REQUESTS, + 'WEB_SEARCH_DOMAIN_FILTER_LIST': request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + 'BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL': request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL, + 'BYPASS_WEB_SEARCH_WEB_LOADER': request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER, + 'OLLAMA_CLOUD_WEB_SEARCH_API_KEY': request.app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY, + 'SEARXNG_QUERY_URL': request.app.state.config.SEARXNG_QUERY_URL, + 'SEARXNG_LANGUAGE': request.app.state.config.SEARXNG_LANGUAGE, + 'YACY_QUERY_URL': request.app.state.config.YACY_QUERY_URL, + 'YACY_USERNAME': request.app.state.config.YACY_USERNAME, + 'YACY_PASSWORD': request.app.state.config.YACY_PASSWORD, + 'GOOGLE_PSE_API_KEY': request.app.state.config.GOOGLE_PSE_API_KEY, + 'GOOGLE_PSE_ENGINE_ID': request.app.state.config.GOOGLE_PSE_ENGINE_ID, + 'BRAVE_SEARCH_API_KEY': request.app.state.config.BRAVE_SEARCH_API_KEY, + 'KAGI_SEARCH_API_KEY': request.app.state.config.KAGI_SEARCH_API_KEY, + 'MOJEEK_SEARCH_API_KEY': request.app.state.config.MOJEEK_SEARCH_API_KEY, + 'BOCHA_SEARCH_API_KEY': request.app.state.config.BOCHA_SEARCH_API_KEY, + 'SERPSTACK_API_KEY': request.app.state.config.SERPSTACK_API_KEY, + 'SERPSTACK_HTTPS': request.app.state.config.SERPSTACK_HTTPS, + 'SERPER_API_KEY': request.app.state.config.SERPER_API_KEY, + 'SERPLY_API_KEY': request.app.state.config.SERPLY_API_KEY, + 'DDGS_BACKEND': request.app.state.config.DDGS_BACKEND, + 'TAVILY_API_KEY': request.app.state.config.TAVILY_API_KEY, + 'SEARCHAPI_API_KEY': request.app.state.config.SEARCHAPI_API_KEY, + 'SEARCHAPI_ENGINE': request.app.state.config.SEARCHAPI_ENGINE, + 'SERPAPI_API_KEY': request.app.state.config.SERPAPI_API_KEY, + 'SERPAPI_ENGINE': request.app.state.config.SERPAPI_ENGINE, + 'JINA_API_KEY': request.app.state.config.JINA_API_KEY, + 'JINA_API_BASE_URL': request.app.state.config.JINA_API_BASE_URL, + 'BING_SEARCH_V7_ENDPOINT': request.app.state.config.BING_SEARCH_V7_ENDPOINT, + 'BING_SEARCH_V7_SUBSCRIPTION_KEY': request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY, + 'EXA_API_KEY': request.app.state.config.EXA_API_KEY, + 'PERPLEXITY_API_KEY': request.app.state.config.PERPLEXITY_API_KEY, + 'PERPLEXITY_MODEL': request.app.state.config.PERPLEXITY_MODEL, + 'PERPLEXITY_SEARCH_CONTEXT_USAGE': request.app.state.config.PERPLEXITY_SEARCH_CONTEXT_USAGE, + 'PERPLEXITY_SEARCH_API_URL': request.app.state.config.PERPLEXITY_SEARCH_API_URL, + 'SOUGOU_API_SID': request.app.state.config.SOUGOU_API_SID, + 'SOUGOU_API_SK': request.app.state.config.SOUGOU_API_SK, + 'WEB_LOADER_ENGINE': request.app.state.config.WEB_LOADER_ENGINE, + 'WEB_LOADER_TIMEOUT': request.app.state.config.WEB_LOADER_TIMEOUT, + 'ENABLE_WEB_LOADER_SSL_VERIFICATION': request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION, + 'PLAYWRIGHT_WS_URL': request.app.state.config.PLAYWRIGHT_WS_URL, + 'PLAYWRIGHT_TIMEOUT': request.app.state.config.PLAYWRIGHT_TIMEOUT, + 'FIRECRAWL_API_KEY': request.app.state.config.FIRECRAWL_API_KEY, + 'FIRECRAWL_API_BASE_URL': request.app.state.config.FIRECRAWL_API_BASE_URL, + 'FIRECRAWL_TIMEOUT': request.app.state.config.FIRECRAWL_TIMEOUT, + 'TAVILY_EXTRACT_DEPTH': request.app.state.config.TAVILY_EXTRACT_DEPTH, + 'EXTERNAL_WEB_SEARCH_URL': request.app.state.config.EXTERNAL_WEB_SEARCH_URL, + 'EXTERNAL_WEB_SEARCH_API_KEY': request.app.state.config.EXTERNAL_WEB_SEARCH_API_KEY, + 'EXTERNAL_WEB_LOADER_URL': request.app.state.config.EXTERNAL_WEB_LOADER_URL, + 'EXTERNAL_WEB_LOADER_API_KEY': request.app.state.config.EXTERNAL_WEB_LOADER_API_KEY, + 'YOUTUBE_LOADER_LANGUAGE': request.app.state.config.YOUTUBE_LOADER_LANGUAGE, + 'YOUTUBE_LOADER_PROXY_URL': request.app.state.config.YOUTUBE_LOADER_PROXY_URL, + 'YOUTUBE_LOADER_TRANSLATION': request.app.state.YOUTUBE_LOADER_TRANSLATION, + 'YANDEX_WEB_SEARCH_URL': request.app.state.config.YANDEX_WEB_SEARCH_URL, + 'YANDEX_WEB_SEARCH_API_KEY': request.app.state.config.YANDEX_WEB_SEARCH_API_KEY, + 'YANDEX_WEB_SEARCH_CONFIG': request.app.state.config.YANDEX_WEB_SEARCH_CONFIG, + 'YOUCOM_API_KEY': request.app.state.config.YOUCOM_API_KEY, + }, + } + + +class WebConfig(BaseModel): + ENABLE_WEB_SEARCH: Optional[bool] = None + WEB_SEARCH_ENGINE: Optional[str] = None + WEB_SEARCH_TRUST_ENV: Optional[bool] = None + WEB_SEARCH_RESULT_COUNT: Optional[int] = None + WEB_SEARCH_CONCURRENT_REQUESTS: Optional[int] = None + WEB_SEARCH_DOMAIN_FILTER_LIST: Optional[List[str]] = [] + WEB_FETCH_MAX_CONTENT_LENGTH: Optional[int] = None + WEB_LOADER_CONCURRENT_REQUESTS: Optional[int] = None + BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: Optional[bool] = None + BYPASS_WEB_SEARCH_WEB_LOADER: Optional[bool] = None + OLLAMA_CLOUD_WEB_SEARCH_API_KEY: Optional[str] = None + SEARXNG_QUERY_URL: Optional[str] = None + SEARXNG_LANGUAGE: Optional[str] = None + YACY_QUERY_URL: Optional[str] = None + YACY_USERNAME: Optional[str] = None + YACY_PASSWORD: Optional[str] = None + GOOGLE_PSE_API_KEY: Optional[str] = None + GOOGLE_PSE_ENGINE_ID: Optional[str] = None + BRAVE_SEARCH_API_KEY: Optional[str] = None + KAGI_SEARCH_API_KEY: Optional[str] = None + MOJEEK_SEARCH_API_KEY: Optional[str] = None + BOCHA_SEARCH_API_KEY: Optional[str] = None + SERPSTACK_API_KEY: Optional[str] = None + SERPSTACK_HTTPS: Optional[bool] = None + SERPER_API_KEY: Optional[str] = None + SERPLY_API_KEY: Optional[str] = None + DDGS_BACKEND: Optional[str] = None + TAVILY_API_KEY: Optional[str] = None + SEARCHAPI_API_KEY: Optional[str] = None + SEARCHAPI_ENGINE: Optional[str] = None + SERPAPI_API_KEY: Optional[str] = None + SERPAPI_ENGINE: Optional[str] = None + JINA_API_KEY: Optional[str] = None + JINA_API_BASE_URL: Optional[str] = None + BING_SEARCH_V7_ENDPOINT: Optional[str] = None + BING_SEARCH_V7_SUBSCRIPTION_KEY: Optional[str] = None + EXA_API_KEY: Optional[str] = None + PERPLEXITY_API_KEY: Optional[str] = None + PERPLEXITY_MODEL: Optional[str] = None + PERPLEXITY_SEARCH_CONTEXT_USAGE: Optional[str] = None + PERPLEXITY_SEARCH_API_URL: Optional[str] = None + SOUGOU_API_SID: Optional[str] = None + SOUGOU_API_SK: Optional[str] = None + WEB_LOADER_ENGINE: Optional[str] = None + WEB_LOADER_TIMEOUT: Optional[str] = None + ENABLE_WEB_LOADER_SSL_VERIFICATION: Optional[bool] = None + PLAYWRIGHT_WS_URL: Optional[str] = None + PLAYWRIGHT_TIMEOUT: Optional[int] = None + FIRECRAWL_API_KEY: Optional[str] = None + FIRECRAWL_API_BASE_URL: Optional[str] = None + FIRECRAWL_TIMEOUT: Optional[str] = None + TAVILY_EXTRACT_DEPTH: Optional[str] = None + EXTERNAL_WEB_SEARCH_URL: Optional[str] = None + EXTERNAL_WEB_SEARCH_API_KEY: Optional[str] = None + EXTERNAL_WEB_LOADER_URL: Optional[str] = None + EXTERNAL_WEB_LOADER_API_KEY: Optional[str] = None + YOUTUBE_LOADER_LANGUAGE: Optional[List[str]] = None + YOUTUBE_LOADER_PROXY_URL: Optional[str] = None + YOUTUBE_LOADER_TRANSLATION: Optional[str] = None + YANDEX_WEB_SEARCH_URL: Optional[str] = None + YANDEX_WEB_SEARCH_API_KEY: Optional[str] = None + YANDEX_WEB_SEARCH_CONFIG: Optional[str] = None + YOUCOM_API_KEY: Optional[str] = None + + +class ConfigForm(BaseModel): + # RAG settings + RAG_TEMPLATE: Optional[str] = None + TOP_K: Optional[int] = None + BYPASS_EMBEDDING_AND_RETRIEVAL: Optional[bool] = None + RAG_FULL_CONTEXT: Optional[bool] = None + + # Hybrid search settings + ENABLE_RAG_HYBRID_SEARCH: Optional[bool] = None + ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS: Optional[bool] = None + TOP_K_RERANKER: Optional[int] = None + RELEVANCE_THRESHOLD: Optional[float] = None + HYBRID_BM25_WEIGHT: Optional[float] = None + + # Content extraction settings + CONTENT_EXTRACTION_ENGINE: Optional[str] = None + PDF_EXTRACT_IMAGES: Optional[bool] = None + PDF_LOADER_MODE: Optional[str] = None + + DATALAB_MARKER_API_KEY: Optional[str] = None + DATALAB_MARKER_API_BASE_URL: Optional[str] = None + DATALAB_MARKER_ADDITIONAL_CONFIG: Optional[str] = None + DATALAB_MARKER_SKIP_CACHE: Optional[bool] = None + DATALAB_MARKER_FORCE_OCR: Optional[bool] = None + DATALAB_MARKER_PAGINATE: Optional[bool] = None + DATALAB_MARKER_STRIP_EXISTING_OCR: Optional[bool] = None + DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION: Optional[bool] = None + DATALAB_MARKER_FORMAT_LINES: Optional[bool] = None + DATALAB_MARKER_USE_LLM: Optional[bool] = None + DATALAB_MARKER_OUTPUT_FORMAT: Optional[str] = None + + EXTERNAL_DOCUMENT_LOADER_URL: Optional[str] = None + EXTERNAL_DOCUMENT_LOADER_API_KEY: Optional[str] = None + + TIKA_SERVER_URL: Optional[str] = None + DOCLING_SERVER_URL: Optional[str] = None + DOCLING_API_KEY: Optional[str] = None + DOCLING_PARAMS: Optional[dict] = None + DOCUMENT_INTELLIGENCE_ENDPOINT: Optional[str] = None + DOCUMENT_INTELLIGENCE_KEY: Optional[str] = None + DOCUMENT_INTELLIGENCE_MODEL: Optional[str] = None + MISTRAL_OCR_API_BASE_URL: Optional[str] = None + MISTRAL_OCR_API_KEY: Optional[str] = None + PADDLEOCR_VL_BASE_URL: Optional[str] = None + PADDLEOCR_VL_TOKEN: Optional[str] = None + + # MinerU settings + MINERU_API_MODE: Optional[str] = None + MINERU_API_URL: Optional[str] = None + MINERU_API_KEY: Optional[str] = None + MINERU_API_TIMEOUT: Optional[str] = None + MINERU_PARAMS: Optional[dict] = None + + # Reranking settings + RAG_RERANKING_MODEL: Optional[str] = None + RAG_RERANKING_ENGINE: Optional[str] = None + RAG_RERANKING_BATCH_SIZE: Optional[int] = None + RAG_EXTERNAL_RERANKER_URL: Optional[str] = None + RAG_EXTERNAL_RERANKER_API_KEY: Optional[str] = None + RAG_EXTERNAL_RERANKER_TIMEOUT: Optional[str] = None + + # Chunking settings + TEXT_SPLITTER: Optional[str] = None + ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER: Optional[bool] = None + CHUNK_SIZE: Optional[int] = None + CHUNK_MIN_SIZE_TARGET: Optional[int] = None + CHUNK_OVERLAP: Optional[int] = None + + # File upload settings + FILE_MAX_SIZE: Optional[Union[int, str]] = None + FILE_MAX_COUNT: Optional[Union[int, str]] = None + FILE_IMAGE_COMPRESSION_WIDTH: Optional[Union[int, str]] = None + FILE_IMAGE_COMPRESSION_HEIGHT: Optional[Union[int, str]] = None + ALLOWED_FILE_EXTENSIONS: Optional[List[str]] = None + + # Integration settings + ENABLE_GOOGLE_DRIVE_INTEGRATION: Optional[bool] = None + ENABLE_ONEDRIVE_INTEGRATION: Optional[bool] = None + + # Web search settings + web: Optional[WebConfig] = None + + +@router.post('/config/update') +async def update_rag_config(request: Request, form_data: ConfigForm, user=Depends(get_admin_user)): + # RAG settings + request.app.state.config.RAG_TEMPLATE = ( + form_data.RAG_TEMPLATE if form_data.RAG_TEMPLATE is not None else request.app.state.config.RAG_TEMPLATE + ) + request.app.state.config.TOP_K = form_data.TOP_K if form_data.TOP_K is not None else request.app.state.config.TOP_K + request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL = ( + form_data.BYPASS_EMBEDDING_AND_RETRIEVAL + if form_data.BYPASS_EMBEDDING_AND_RETRIEVAL is not None + else request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL + ) + request.app.state.config.RAG_FULL_CONTEXT = ( + form_data.RAG_FULL_CONTEXT + if form_data.RAG_FULL_CONTEXT is not None + else request.app.state.config.RAG_FULL_CONTEXT + ) + + # Hybrid search settings + request.app.state.config.ENABLE_RAG_HYBRID_SEARCH = ( + form_data.ENABLE_RAG_HYBRID_SEARCH + if form_data.ENABLE_RAG_HYBRID_SEARCH is not None + else request.app.state.config.ENABLE_RAG_HYBRID_SEARCH + ) + request.app.state.config.ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS = ( + form_data.ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS + if form_data.ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS is not None + else request.app.state.config.ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS + ) + + request.app.state.config.TOP_K_RERANKER = ( + form_data.TOP_K_RERANKER if form_data.TOP_K_RERANKER is not None else request.app.state.config.TOP_K_RERANKER + ) + request.app.state.config.RELEVANCE_THRESHOLD = ( + form_data.RELEVANCE_THRESHOLD + if form_data.RELEVANCE_THRESHOLD is not None + else request.app.state.config.RELEVANCE_THRESHOLD + ) + request.app.state.config.HYBRID_BM25_WEIGHT = ( + form_data.HYBRID_BM25_WEIGHT + if form_data.HYBRID_BM25_WEIGHT is not None + else request.app.state.config.HYBRID_BM25_WEIGHT + ) + + # Content extraction settings + request.app.state.config.CONTENT_EXTRACTION_ENGINE = ( + form_data.CONTENT_EXTRACTION_ENGINE + if form_data.CONTENT_EXTRACTION_ENGINE is not None + else request.app.state.config.CONTENT_EXTRACTION_ENGINE + ) + request.app.state.config.PDF_EXTRACT_IMAGES = ( + form_data.PDF_EXTRACT_IMAGES + if form_data.PDF_EXTRACT_IMAGES is not None + else request.app.state.config.PDF_EXTRACT_IMAGES + ) + request.app.state.config.PDF_LOADER_MODE = ( + form_data.PDF_LOADER_MODE if form_data.PDF_LOADER_MODE is not None else request.app.state.config.PDF_LOADER_MODE + ) + request.app.state.config.DATALAB_MARKER_API_KEY = ( + form_data.DATALAB_MARKER_API_KEY + if form_data.DATALAB_MARKER_API_KEY is not None + else request.app.state.config.DATALAB_MARKER_API_KEY + ) + request.app.state.config.DATALAB_MARKER_API_BASE_URL = ( + form_data.DATALAB_MARKER_API_BASE_URL + if form_data.DATALAB_MARKER_API_BASE_URL is not None + else request.app.state.config.DATALAB_MARKER_API_BASE_URL + ) + request.app.state.config.DATALAB_MARKER_ADDITIONAL_CONFIG = ( + form_data.DATALAB_MARKER_ADDITIONAL_CONFIG + if form_data.DATALAB_MARKER_ADDITIONAL_CONFIG is not None + else request.app.state.config.DATALAB_MARKER_ADDITIONAL_CONFIG + ) + request.app.state.config.DATALAB_MARKER_SKIP_CACHE = ( + form_data.DATALAB_MARKER_SKIP_CACHE + if form_data.DATALAB_MARKER_SKIP_CACHE is not None + else request.app.state.config.DATALAB_MARKER_SKIP_CACHE + ) + request.app.state.config.DATALAB_MARKER_FORCE_OCR = ( + form_data.DATALAB_MARKER_FORCE_OCR + if form_data.DATALAB_MARKER_FORCE_OCR is not None + else request.app.state.config.DATALAB_MARKER_FORCE_OCR + ) + request.app.state.config.DATALAB_MARKER_PAGINATE = ( + form_data.DATALAB_MARKER_PAGINATE + if form_data.DATALAB_MARKER_PAGINATE is not None + else request.app.state.config.DATALAB_MARKER_PAGINATE + ) + request.app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR = ( + form_data.DATALAB_MARKER_STRIP_EXISTING_OCR + if form_data.DATALAB_MARKER_STRIP_EXISTING_OCR is not None + else request.app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR + ) + request.app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION = ( + form_data.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION + if form_data.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION is not None + else request.app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION + ) + request.app.state.config.DATALAB_MARKER_FORMAT_LINES = ( + form_data.DATALAB_MARKER_FORMAT_LINES + if form_data.DATALAB_MARKER_FORMAT_LINES is not None + else request.app.state.config.DATALAB_MARKER_FORMAT_LINES + ) + request.app.state.config.DATALAB_MARKER_OUTPUT_FORMAT = ( + form_data.DATALAB_MARKER_OUTPUT_FORMAT + if form_data.DATALAB_MARKER_OUTPUT_FORMAT is not None + else request.app.state.config.DATALAB_MARKER_OUTPUT_FORMAT + ) + request.app.state.config.DATALAB_MARKER_USE_LLM = ( + form_data.DATALAB_MARKER_USE_LLM + if form_data.DATALAB_MARKER_USE_LLM is not None + else request.app.state.config.DATALAB_MARKER_USE_LLM + ) + request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL = ( + form_data.EXTERNAL_DOCUMENT_LOADER_URL + if form_data.EXTERNAL_DOCUMENT_LOADER_URL is not None + else request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL + ) + request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY = ( + form_data.EXTERNAL_DOCUMENT_LOADER_API_KEY + if form_data.EXTERNAL_DOCUMENT_LOADER_API_KEY is not None + else request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY + ) + request.app.state.config.TIKA_SERVER_URL = ( + form_data.TIKA_SERVER_URL if form_data.TIKA_SERVER_URL is not None else request.app.state.config.TIKA_SERVER_URL + ) + request.app.state.config.DOCLING_SERVER_URL = ( + form_data.DOCLING_SERVER_URL + if form_data.DOCLING_SERVER_URL is not None + else request.app.state.config.DOCLING_SERVER_URL + ) + request.app.state.config.DOCLING_API_KEY = ( + form_data.DOCLING_API_KEY if form_data.DOCLING_API_KEY is not None else request.app.state.config.DOCLING_API_KEY + ) + request.app.state.config.DOCLING_PARAMS = ( + form_data.DOCLING_PARAMS if form_data.DOCLING_PARAMS is not None else request.app.state.config.DOCLING_PARAMS + ) + request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = ( + form_data.DOCUMENT_INTELLIGENCE_ENDPOINT + if form_data.DOCUMENT_INTELLIGENCE_ENDPOINT is not None + else request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT + ) + request.app.state.config.DOCUMENT_INTELLIGENCE_KEY = ( + form_data.DOCUMENT_INTELLIGENCE_KEY + if form_data.DOCUMENT_INTELLIGENCE_KEY is not None + else request.app.state.config.DOCUMENT_INTELLIGENCE_KEY + ) + request.app.state.config.DOCUMENT_INTELLIGENCE_MODEL = ( + form_data.DOCUMENT_INTELLIGENCE_MODEL + if form_data.DOCUMENT_INTELLIGENCE_MODEL is not None + else request.app.state.config.DOCUMENT_INTELLIGENCE_MODEL + ) + + request.app.state.config.MISTRAL_OCR_API_BASE_URL = ( + form_data.MISTRAL_OCR_API_BASE_URL + if form_data.MISTRAL_OCR_API_BASE_URL is not None + else request.app.state.config.MISTRAL_OCR_API_BASE_URL + ) + request.app.state.config.MISTRAL_OCR_API_KEY = ( + form_data.MISTRAL_OCR_API_KEY + if form_data.MISTRAL_OCR_API_KEY is not None + else request.app.state.config.MISTRAL_OCR_API_KEY + ) + request.app.state.config.PADDLEOCR_VL_BASE_URL = ( + form_data.PADDLEOCR_VL_BASE_URL + if form_data.PADDLEOCR_VL_BASE_URL is not None + else request.app.state.config.PADDLEOCR_VL_BASE_URL + ) + request.app.state.config.PADDLEOCR_VL_TOKEN = ( + form_data.PADDLEOCR_VL_TOKEN + if form_data.PADDLEOCR_VL_TOKEN is not None + else request.app.state.config.PADDLEOCR_VL_TOKEN + ) + + # MinerU settings + request.app.state.config.MINERU_API_MODE = ( + form_data.MINERU_API_MODE if form_data.MINERU_API_MODE is not None else request.app.state.config.MINERU_API_MODE + ) + request.app.state.config.MINERU_API_URL = ( + form_data.MINERU_API_URL if form_data.MINERU_API_URL is not None else request.app.state.config.MINERU_API_URL + ) + request.app.state.config.MINERU_API_KEY = ( + form_data.MINERU_API_KEY if form_data.MINERU_API_KEY is not None else request.app.state.config.MINERU_API_KEY + ) + request.app.state.config.MINERU_API_TIMEOUT = ( + form_data.MINERU_API_TIMEOUT + if form_data.MINERU_API_TIMEOUT is not None + else request.app.state.config.MINERU_API_TIMEOUT + ) + request.app.state.config.MINERU_PARAMS = ( + form_data.MINERU_PARAMS if form_data.MINERU_PARAMS is not None else request.app.state.config.MINERU_PARAMS + ) + + # Reranking settings + if request.app.state.config.RAG_RERANKING_ENGINE == '': + # Unloading the internal reranker and clear VRAM memory + request.app.state.rf = None + request.app.state.RERANKING_FUNCTION = None + import gc + + gc.collect() + if DEVICE_TYPE == 'cuda': + import torch + + if torch.cuda.is_available(): + torch.cuda.empty_cache() + request.app.state.config.RAG_RERANKING_ENGINE = ( + form_data.RAG_RERANKING_ENGINE + if form_data.RAG_RERANKING_ENGINE is not None + else request.app.state.config.RAG_RERANKING_ENGINE + ) + + request.app.state.config.RAG_EXTERNAL_RERANKER_URL = ( + form_data.RAG_EXTERNAL_RERANKER_URL + if form_data.RAG_EXTERNAL_RERANKER_URL is not None + else request.app.state.config.RAG_EXTERNAL_RERANKER_URL + ) + + request.app.state.config.RAG_EXTERNAL_RERANKER_API_KEY = ( + form_data.RAG_EXTERNAL_RERANKER_API_KEY + if form_data.RAG_EXTERNAL_RERANKER_API_KEY is not None + else request.app.state.config.RAG_EXTERNAL_RERANKER_API_KEY + ) + + request.app.state.config.RAG_EXTERNAL_RERANKER_TIMEOUT = ( + form_data.RAG_EXTERNAL_RERANKER_TIMEOUT + if form_data.RAG_EXTERNAL_RERANKER_TIMEOUT is not None + else request.app.state.config.RAG_EXTERNAL_RERANKER_TIMEOUT + ) + + request.app.state.config.RAG_RERANKING_BATCH_SIZE = ( + form_data.RAG_RERANKING_BATCH_SIZE + if form_data.RAG_RERANKING_BATCH_SIZE is not None + else request.app.state.config.RAG_RERANKING_BATCH_SIZE + ) + + log.info( + f'Updating reranking model: {request.app.state.config.RAG_RERANKING_MODEL} to {form_data.RAG_RERANKING_MODEL}' + ) + try: + request.app.state.config.RAG_RERANKING_MODEL = ( + form_data.RAG_RERANKING_MODEL + if form_data.RAG_RERANKING_MODEL is not None + else request.app.state.config.RAG_RERANKING_MODEL + ) + + try: + if ( + request.app.state.config.ENABLE_RAG_HYBRID_SEARCH + and not request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL + ): + request.app.state.rf = get_rf( + request.app.state.config.RAG_RERANKING_ENGINE, + request.app.state.config.RAG_RERANKING_MODEL, + request.app.state.config.RAG_EXTERNAL_RERANKER_URL, + request.app.state.config.RAG_EXTERNAL_RERANKER_API_KEY, + request.app.state.config.RAG_EXTERNAL_RERANKER_TIMEOUT, + ) + + request.app.state.RERANKING_FUNCTION = get_reranking_function( + request.app.state.config.RAG_RERANKING_ENGINE, + request.app.state.config.RAG_RERANKING_MODEL, + request.app.state.rf, + reranking_batch_size=request.app.state.config.RAG_RERANKING_BATCH_SIZE, + ) + except Exception as e: + log.error(f'Error loading reranking model: {e}') + request.app.state.config.ENABLE_RAG_HYBRID_SEARCH = False + except Exception as e: + log.exception(f'Problem updating reranking model: {e}') + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + # Chunking settings + request.app.state.config.TEXT_SPLITTER = ( + form_data.TEXT_SPLITTER if form_data.TEXT_SPLITTER is not None else request.app.state.config.TEXT_SPLITTER + ) + request.app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER = ( + form_data.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER + if form_data.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER is not None + else request.app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER + ) + request.app.state.config.CHUNK_SIZE = ( + form_data.CHUNK_SIZE if form_data.CHUNK_SIZE is not None else request.app.state.config.CHUNK_SIZE + ) + request.app.state.config.CHUNK_MIN_SIZE_TARGET = ( + form_data.CHUNK_MIN_SIZE_TARGET + if form_data.CHUNK_MIN_SIZE_TARGET is not None + else request.app.state.config.CHUNK_MIN_SIZE_TARGET + ) + request.app.state.config.CHUNK_OVERLAP = ( + form_data.CHUNK_OVERLAP if form_data.CHUNK_OVERLAP is not None else request.app.state.config.CHUNK_OVERLAP + ) + + # File upload settings + # Empty string means "clear to None" (unlimited/no compression), + # None means "don't change", int means "set to this value" + if form_data.FILE_MAX_SIZE is not None: + request.app.state.config.FILE_MAX_SIZE = None if form_data.FILE_MAX_SIZE == '' else form_data.FILE_MAX_SIZE + if form_data.FILE_MAX_COUNT is not None: + request.app.state.config.FILE_MAX_COUNT = None if form_data.FILE_MAX_COUNT == '' else form_data.FILE_MAX_COUNT + if form_data.FILE_IMAGE_COMPRESSION_WIDTH is not None: + request.app.state.config.FILE_IMAGE_COMPRESSION_WIDTH = ( + None if form_data.FILE_IMAGE_COMPRESSION_WIDTH == '' else form_data.FILE_IMAGE_COMPRESSION_WIDTH + ) + if form_data.FILE_IMAGE_COMPRESSION_HEIGHT is not None: + request.app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT = ( + None if form_data.FILE_IMAGE_COMPRESSION_HEIGHT == '' else form_data.FILE_IMAGE_COMPRESSION_HEIGHT + ) + + request.app.state.config.ALLOWED_FILE_EXTENSIONS = ( + form_data.ALLOWED_FILE_EXTENSIONS + if form_data.ALLOWED_FILE_EXTENSIONS is not None + else request.app.state.config.ALLOWED_FILE_EXTENSIONS + ) + + # Integration settings + request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ( + form_data.ENABLE_GOOGLE_DRIVE_INTEGRATION + if form_data.ENABLE_GOOGLE_DRIVE_INTEGRATION is not None + else request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION + ) + request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION = ( + form_data.ENABLE_ONEDRIVE_INTEGRATION + if form_data.ENABLE_ONEDRIVE_INTEGRATION is not None + else request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION + ) + + if form_data.web is not None: + # Web search settings + request.app.state.config.ENABLE_WEB_SEARCH = form_data.web.ENABLE_WEB_SEARCH + request.app.state.config.WEB_SEARCH_ENGINE = form_data.web.WEB_SEARCH_ENGINE + request.app.state.config.WEB_SEARCH_TRUST_ENV = form_data.web.WEB_SEARCH_TRUST_ENV + request.app.state.config.WEB_SEARCH_RESULT_COUNT = form_data.web.WEB_SEARCH_RESULT_COUNT + request.app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS = form_data.web.WEB_SEARCH_CONCURRENT_REQUESTS + request.app.state.config.WEB_FETCH_MAX_CONTENT_LENGTH = form_data.web.WEB_FETCH_MAX_CONTENT_LENGTH + request.app.state.config.WEB_LOADER_CONCURRENT_REQUESTS = form_data.web.WEB_LOADER_CONCURRENT_REQUESTS + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST = form_data.web.WEB_SEARCH_DOMAIN_FILTER_LIST + request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = ( + form_data.web.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL + ) + request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER = form_data.web.BYPASS_WEB_SEARCH_WEB_LOADER + request.app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY = form_data.web.OLLAMA_CLOUD_WEB_SEARCH_API_KEY + request.app.state.config.SEARXNG_QUERY_URL = form_data.web.SEARXNG_QUERY_URL + request.app.state.config.SEARXNG_LANGUAGE = form_data.web.SEARXNG_LANGUAGE + request.app.state.config.YACY_QUERY_URL = form_data.web.YACY_QUERY_URL + request.app.state.config.YACY_USERNAME = form_data.web.YACY_USERNAME + request.app.state.config.YACY_PASSWORD = form_data.web.YACY_PASSWORD + request.app.state.config.GOOGLE_PSE_API_KEY = form_data.web.GOOGLE_PSE_API_KEY + request.app.state.config.GOOGLE_PSE_ENGINE_ID = form_data.web.GOOGLE_PSE_ENGINE_ID + request.app.state.config.BRAVE_SEARCH_API_KEY = form_data.web.BRAVE_SEARCH_API_KEY + request.app.state.config.KAGI_SEARCH_API_KEY = form_data.web.KAGI_SEARCH_API_KEY + request.app.state.config.MOJEEK_SEARCH_API_KEY = form_data.web.MOJEEK_SEARCH_API_KEY + request.app.state.config.BOCHA_SEARCH_API_KEY = form_data.web.BOCHA_SEARCH_API_KEY + request.app.state.config.SERPSTACK_API_KEY = form_data.web.SERPSTACK_API_KEY + request.app.state.config.SERPSTACK_HTTPS = form_data.web.SERPSTACK_HTTPS + request.app.state.config.SERPER_API_KEY = form_data.web.SERPER_API_KEY + request.app.state.config.SERPLY_API_KEY = form_data.web.SERPLY_API_KEY + request.app.state.config.DDGS_BACKEND = form_data.web.DDGS_BACKEND + request.app.state.config.TAVILY_API_KEY = form_data.web.TAVILY_API_KEY + request.app.state.config.SEARCHAPI_API_KEY = form_data.web.SEARCHAPI_API_KEY + request.app.state.config.SEARCHAPI_ENGINE = form_data.web.SEARCHAPI_ENGINE + request.app.state.config.SERPAPI_API_KEY = form_data.web.SERPAPI_API_KEY + request.app.state.config.SERPAPI_ENGINE = form_data.web.SERPAPI_ENGINE + request.app.state.config.JINA_API_KEY = form_data.web.JINA_API_KEY + request.app.state.config.JINA_API_BASE_URL = form_data.web.JINA_API_BASE_URL + request.app.state.config.BING_SEARCH_V7_ENDPOINT = form_data.web.BING_SEARCH_V7_ENDPOINT + request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = form_data.web.BING_SEARCH_V7_SUBSCRIPTION_KEY + request.app.state.config.EXA_API_KEY = form_data.web.EXA_API_KEY + request.app.state.config.PERPLEXITY_API_KEY = form_data.web.PERPLEXITY_API_KEY + request.app.state.config.PERPLEXITY_MODEL = form_data.web.PERPLEXITY_MODEL + request.app.state.config.PERPLEXITY_SEARCH_CONTEXT_USAGE = form_data.web.PERPLEXITY_SEARCH_CONTEXT_USAGE + request.app.state.config.PERPLEXITY_SEARCH_API_URL = form_data.web.PERPLEXITY_SEARCH_API_URL + request.app.state.config.SOUGOU_API_SID = form_data.web.SOUGOU_API_SID + request.app.state.config.SOUGOU_API_SK = form_data.web.SOUGOU_API_SK + + # Web loader settings + request.app.state.config.WEB_LOADER_ENGINE = form_data.web.WEB_LOADER_ENGINE + request.app.state.config.WEB_LOADER_TIMEOUT = form_data.web.WEB_LOADER_TIMEOUT + + request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION = form_data.web.ENABLE_WEB_LOADER_SSL_VERIFICATION + request.app.state.config.PLAYWRIGHT_WS_URL = form_data.web.PLAYWRIGHT_WS_URL + request.app.state.config.PLAYWRIGHT_TIMEOUT = form_data.web.PLAYWRIGHT_TIMEOUT + request.app.state.config.FIRECRAWL_API_KEY = form_data.web.FIRECRAWL_API_KEY + request.app.state.config.FIRECRAWL_API_BASE_URL = form_data.web.FIRECRAWL_API_BASE_URL + request.app.state.config.FIRECRAWL_TIMEOUT = form_data.web.FIRECRAWL_TIMEOUT + request.app.state.config.EXTERNAL_WEB_SEARCH_URL = form_data.web.EXTERNAL_WEB_SEARCH_URL + request.app.state.config.EXTERNAL_WEB_SEARCH_API_KEY = form_data.web.EXTERNAL_WEB_SEARCH_API_KEY + request.app.state.config.EXTERNAL_WEB_LOADER_URL = form_data.web.EXTERNAL_WEB_LOADER_URL + request.app.state.config.EXTERNAL_WEB_LOADER_API_KEY = form_data.web.EXTERNAL_WEB_LOADER_API_KEY + request.app.state.config.TAVILY_EXTRACT_DEPTH = form_data.web.TAVILY_EXTRACT_DEPTH + request.app.state.config.YOUTUBE_LOADER_LANGUAGE = form_data.web.YOUTUBE_LOADER_LANGUAGE + request.app.state.config.YOUTUBE_LOADER_PROXY_URL = form_data.web.YOUTUBE_LOADER_PROXY_URL + request.app.state.YOUTUBE_LOADER_TRANSLATION = form_data.web.YOUTUBE_LOADER_TRANSLATION + request.app.state.config.YANDEX_WEB_SEARCH_URL = form_data.web.YANDEX_WEB_SEARCH_URL + request.app.state.config.YANDEX_WEB_SEARCH_API_KEY = form_data.web.YANDEX_WEB_SEARCH_API_KEY + request.app.state.config.YANDEX_WEB_SEARCH_CONFIG = form_data.web.YANDEX_WEB_SEARCH_CONFIG + request.app.state.config.YOUCOM_API_KEY = form_data.web.YOUCOM_API_KEY + + return { + 'status': True, + # RAG settings + 'RAG_TEMPLATE': request.app.state.config.RAG_TEMPLATE, + 'TOP_K': request.app.state.config.TOP_K, + 'BYPASS_EMBEDDING_AND_RETRIEVAL': request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL, + 'RAG_FULL_CONTEXT': request.app.state.config.RAG_FULL_CONTEXT, + # Hybrid search settings + 'ENABLE_RAG_HYBRID_SEARCH': request.app.state.config.ENABLE_RAG_HYBRID_SEARCH, + 'TOP_K_RERANKER': request.app.state.config.TOP_K_RERANKER, + 'RELEVANCE_THRESHOLD': request.app.state.config.RELEVANCE_THRESHOLD, + 'HYBRID_BM25_WEIGHT': request.app.state.config.HYBRID_BM25_WEIGHT, + # Content extraction settings + 'CONTENT_EXTRACTION_ENGINE': request.app.state.config.CONTENT_EXTRACTION_ENGINE, + 'PDF_EXTRACT_IMAGES': request.app.state.config.PDF_EXTRACT_IMAGES, + 'PDF_LOADER_MODE': request.app.state.config.PDF_LOADER_MODE, + 'DATALAB_MARKER_API_KEY': request.app.state.config.DATALAB_MARKER_API_KEY, + 'DATALAB_MARKER_API_BASE_URL': request.app.state.config.DATALAB_MARKER_API_BASE_URL, + 'DATALAB_MARKER_ADDITIONAL_CONFIG': request.app.state.config.DATALAB_MARKER_ADDITIONAL_CONFIG, + 'DATALAB_MARKER_SKIP_CACHE': request.app.state.config.DATALAB_MARKER_SKIP_CACHE, + 'DATALAB_MARKER_FORCE_OCR': request.app.state.config.DATALAB_MARKER_FORCE_OCR, + 'DATALAB_MARKER_PAGINATE': request.app.state.config.DATALAB_MARKER_PAGINATE, + 'DATALAB_MARKER_STRIP_EXISTING_OCR': request.app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR, + 'DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION': request.app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, + 'DATALAB_MARKER_USE_LLM': request.app.state.config.DATALAB_MARKER_USE_LLM, + 'DATALAB_MARKER_OUTPUT_FORMAT': request.app.state.config.DATALAB_MARKER_OUTPUT_FORMAT, + 'EXTERNAL_DOCUMENT_LOADER_URL': request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL, + 'EXTERNAL_DOCUMENT_LOADER_API_KEY': request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY, + 'TIKA_SERVER_URL': request.app.state.config.TIKA_SERVER_URL, + 'DOCLING_SERVER_URL': request.app.state.config.DOCLING_SERVER_URL, + 'DOCLING_API_KEY': request.app.state.config.DOCLING_API_KEY, + 'DOCLING_PARAMS': request.app.state.config.DOCLING_PARAMS, + 'DOCUMENT_INTELLIGENCE_ENDPOINT': request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, + 'DOCUMENT_INTELLIGENCE_KEY': request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, + 'DOCUMENT_INTELLIGENCE_MODEL': request.app.state.config.DOCUMENT_INTELLIGENCE_MODEL, + 'MISTRAL_OCR_API_BASE_URL': request.app.state.config.MISTRAL_OCR_API_BASE_URL, + 'MISTRAL_OCR_API_KEY': request.app.state.config.MISTRAL_OCR_API_KEY, + 'PADDLEOCR_VL_BASE_URL': request.app.state.config.PADDLEOCR_VL_BASE_URL, + 'PADDLEOCR_VL_TOKEN': request.app.state.config.PADDLEOCR_VL_TOKEN, + # MinerU settings + 'MINERU_API_MODE': request.app.state.config.MINERU_API_MODE, + 'MINERU_API_URL': request.app.state.config.MINERU_API_URL, + 'MINERU_API_KEY': request.app.state.config.MINERU_API_KEY, + 'MINERU_API_TIMEOUT': request.app.state.config.MINERU_API_TIMEOUT, + 'MINERU_PARAMS': request.app.state.config.MINERU_PARAMS, + # Reranking settings + 'RAG_RERANKING_MODEL': request.app.state.config.RAG_RERANKING_MODEL, + 'RAG_RERANKING_ENGINE': request.app.state.config.RAG_RERANKING_ENGINE, + 'RAG_EXTERNAL_RERANKER_URL': request.app.state.config.RAG_EXTERNAL_RERANKER_URL, + 'RAG_EXTERNAL_RERANKER_API_KEY': request.app.state.config.RAG_EXTERNAL_RERANKER_API_KEY, + 'RAG_EXTERNAL_RERANKER_TIMEOUT': request.app.state.config.RAG_EXTERNAL_RERANKER_TIMEOUT, + # Chunking settings + 'TEXT_SPLITTER': request.app.state.config.TEXT_SPLITTER, + 'CHUNK_SIZE': request.app.state.config.CHUNK_SIZE, + 'CHUNK_MIN_SIZE_TARGET': request.app.state.config.CHUNK_MIN_SIZE_TARGET, + 'ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER': request.app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER, + 'CHUNK_OVERLAP': request.app.state.config.CHUNK_OVERLAP, + # File upload settings + 'FILE_MAX_SIZE': request.app.state.config.FILE_MAX_SIZE, + 'FILE_MAX_COUNT': request.app.state.config.FILE_MAX_COUNT, + 'FILE_IMAGE_COMPRESSION_WIDTH': request.app.state.config.FILE_IMAGE_COMPRESSION_WIDTH, + 'FILE_IMAGE_COMPRESSION_HEIGHT': request.app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT, + 'ALLOWED_FILE_EXTENSIONS': request.app.state.config.ALLOWED_FILE_EXTENSIONS, + # Integration settings + 'ENABLE_GOOGLE_DRIVE_INTEGRATION': request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, + 'ENABLE_ONEDRIVE_INTEGRATION': request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION, + # Web search settings + 'web': { + 'ENABLE_WEB_SEARCH': request.app.state.config.ENABLE_WEB_SEARCH, + 'WEB_SEARCH_ENGINE': request.app.state.config.WEB_SEARCH_ENGINE, + 'WEB_SEARCH_TRUST_ENV': request.app.state.config.WEB_SEARCH_TRUST_ENV, + 'WEB_SEARCH_RESULT_COUNT': request.app.state.config.WEB_SEARCH_RESULT_COUNT, + 'WEB_SEARCH_CONCURRENT_REQUESTS': request.app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS, + 'WEB_FETCH_MAX_CONTENT_LENGTH': request.app.state.config.WEB_FETCH_MAX_CONTENT_LENGTH, + 'WEB_LOADER_CONCURRENT_REQUESTS': request.app.state.config.WEB_LOADER_CONCURRENT_REQUESTS, + 'WEB_SEARCH_DOMAIN_FILTER_LIST': request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + 'BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL': request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL, + 'BYPASS_WEB_SEARCH_WEB_LOADER': request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER, + 'OLLAMA_CLOUD_WEB_SEARCH_API_KEY': request.app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY, + 'SEARXNG_QUERY_URL': request.app.state.config.SEARXNG_QUERY_URL, + 'SEARXNG_LANGUAGE': request.app.state.config.SEARXNG_LANGUAGE, + 'YACY_QUERY_URL': request.app.state.config.YACY_QUERY_URL, + 'YACY_USERNAME': request.app.state.config.YACY_USERNAME, + 'YACY_PASSWORD': request.app.state.config.YACY_PASSWORD, + 'GOOGLE_PSE_API_KEY': request.app.state.config.GOOGLE_PSE_API_KEY, + 'GOOGLE_PSE_ENGINE_ID': request.app.state.config.GOOGLE_PSE_ENGINE_ID, + 'BRAVE_SEARCH_API_KEY': request.app.state.config.BRAVE_SEARCH_API_KEY, + 'KAGI_SEARCH_API_KEY': request.app.state.config.KAGI_SEARCH_API_KEY, + 'MOJEEK_SEARCH_API_KEY': request.app.state.config.MOJEEK_SEARCH_API_KEY, + 'BOCHA_SEARCH_API_KEY': request.app.state.config.BOCHA_SEARCH_API_KEY, + 'SERPSTACK_API_KEY': request.app.state.config.SERPSTACK_API_KEY, + 'SERPSTACK_HTTPS': request.app.state.config.SERPSTACK_HTTPS, + 'SERPER_API_KEY': request.app.state.config.SERPER_API_KEY, + 'SERPLY_API_KEY': request.app.state.config.SERPLY_API_KEY, + 'TAVILY_API_KEY': request.app.state.config.TAVILY_API_KEY, + 'SEARCHAPI_API_KEY': request.app.state.config.SEARCHAPI_API_KEY, + 'SEARCHAPI_ENGINE': request.app.state.config.SEARCHAPI_ENGINE, + 'SERPAPI_API_KEY': request.app.state.config.SERPAPI_API_KEY, + 'SERPAPI_ENGINE': request.app.state.config.SERPAPI_ENGINE, + 'JINA_API_KEY': request.app.state.config.JINA_API_KEY, + 'JINA_API_BASE_URL': request.app.state.config.JINA_API_BASE_URL, + 'BING_SEARCH_V7_ENDPOINT': request.app.state.config.BING_SEARCH_V7_ENDPOINT, + 'BING_SEARCH_V7_SUBSCRIPTION_KEY': request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY, + 'EXA_API_KEY': request.app.state.config.EXA_API_KEY, + 'PERPLEXITY_API_KEY': request.app.state.config.PERPLEXITY_API_KEY, + 'PERPLEXITY_MODEL': request.app.state.config.PERPLEXITY_MODEL, + 'PERPLEXITY_SEARCH_CONTEXT_USAGE': request.app.state.config.PERPLEXITY_SEARCH_CONTEXT_USAGE, + 'PERPLEXITY_SEARCH_API_URL': request.app.state.config.PERPLEXITY_SEARCH_API_URL, + 'SOUGOU_API_SID': request.app.state.config.SOUGOU_API_SID, + 'SOUGOU_API_SK': request.app.state.config.SOUGOU_API_SK, + 'WEB_LOADER_ENGINE': request.app.state.config.WEB_LOADER_ENGINE, + 'WEB_LOADER_TIMEOUT': request.app.state.config.WEB_LOADER_TIMEOUT, + 'ENABLE_WEB_LOADER_SSL_VERIFICATION': request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION, + 'PLAYWRIGHT_WS_URL': request.app.state.config.PLAYWRIGHT_WS_URL, + 'PLAYWRIGHT_TIMEOUT': request.app.state.config.PLAYWRIGHT_TIMEOUT, + 'FIRECRAWL_API_KEY': request.app.state.config.FIRECRAWL_API_KEY, + 'FIRECRAWL_API_BASE_URL': request.app.state.config.FIRECRAWL_API_BASE_URL, + 'FIRECRAWL_TIMEOUT': request.app.state.config.FIRECRAWL_TIMEOUT, + 'TAVILY_EXTRACT_DEPTH': request.app.state.config.TAVILY_EXTRACT_DEPTH, + 'EXTERNAL_WEB_SEARCH_URL': request.app.state.config.EXTERNAL_WEB_SEARCH_URL, + 'EXTERNAL_WEB_SEARCH_API_KEY': request.app.state.config.EXTERNAL_WEB_SEARCH_API_KEY, + 'EXTERNAL_WEB_LOADER_URL': request.app.state.config.EXTERNAL_WEB_LOADER_URL, + 'EXTERNAL_WEB_LOADER_API_KEY': request.app.state.config.EXTERNAL_WEB_LOADER_API_KEY, + 'YOUTUBE_LOADER_LANGUAGE': request.app.state.config.YOUTUBE_LOADER_LANGUAGE, + 'YOUTUBE_LOADER_PROXY_URL': request.app.state.config.YOUTUBE_LOADER_PROXY_URL, + 'YOUTUBE_LOADER_TRANSLATION': request.app.state.YOUTUBE_LOADER_TRANSLATION, + 'YANDEX_WEB_SEARCH_URL': request.app.state.config.YANDEX_WEB_SEARCH_URL, + 'YANDEX_WEB_SEARCH_API_KEY': request.app.state.config.YANDEX_WEB_SEARCH_API_KEY, + 'YANDEX_WEB_SEARCH_CONFIG': request.app.state.config.YANDEX_WEB_SEARCH_CONFIG, + 'YOUCOM_API_KEY': request.app.state.config.YOUCOM_API_KEY, + }, + } + + +#################################### +# +# Document process and retrieval +# +#################################### + + +def can_merge_chunks(a: Document, b: Document) -> bool: + if a.metadata.get('source') != b.metadata.get('source'): + return False + + a_file_id = a.metadata.get('file_id') + b_file_id = b.metadata.get('file_id') + + if a_file_id is not None and b_file_id is not None: + return a_file_id == b_file_id + + return True + + +def merge_docs_to_target_size( + request: Request, + chunks: list[Document], +) -> list[Document]: + """ + Best-effort normalization of chunk sizes. + + Attempts to grow small chunks up to a desired minimum size, + without exceeding the maximum size or crossing source/file + boundaries. + """ + min_chunk_size_target = request.app.state.config.CHUNK_MIN_SIZE_TARGET + max_chunk_size = request.app.state.config.CHUNK_SIZE + + if min_chunk_size_target <= 0: + return chunks + + measure_chunk_size = len + if request.app.state.config.TEXT_SPLITTER == 'token': + encoding = tiktoken.get_encoding(str(request.app.state.config.TIKTOKEN_ENCODING_NAME)) + measure_chunk_size = lambda text: len(encoding.encode(text)) + + processed_chunks: list[Document] = [] + + current_chunk: Document | None = None + current_content: str = '' + + for next_chunk in chunks: + if current_chunk is None: + current_chunk = next_chunk + current_content = next_chunk.page_content + continue # First chunk initialization + + proposed_content = f'{current_content}\n\n{next_chunk.page_content}' + + can_merge = ( + can_merge_chunks(current_chunk, next_chunk) + and measure_chunk_size(current_content) < min_chunk_size_target + and measure_chunk_size(proposed_content) <= max_chunk_size + ) + + if can_merge: + current_content = proposed_content + else: + processed_chunks.append( + Document( + page_content=current_content, + metadata={**current_chunk.metadata}, + ) + ) + current_chunk = next_chunk + current_content = next_chunk.page_content + + if current_chunk is not None: + processed_chunks.append( + Document( + page_content=current_content, + metadata={**current_chunk.metadata}, + ) + ) + + return processed_chunks + + +def save_docs_to_vector_db( + request: Request, + docs, + collection_name, + metadata: Optional[dict] = None, + overwrite: bool = False, + split: bool = True, + add: bool = False, + user=None, +) -> bool: + def _get_docs_info(docs: list[Document]) -> str: + docs_info = set() + + # Trying to select relevant metadata identifying the document. + for doc in docs: + metadata = getattr(doc, 'metadata', {}) + doc_name = metadata.get('name', '') + if not doc_name: + doc_name = metadata.get('title', '') + if not doc_name: + doc_name = metadata.get('source', '') + if doc_name: + docs_info.add(doc_name) + + return ', '.join(docs_info) + + log.debug(f'save_docs_to_vector_db: document {_get_docs_info(docs)} {collection_name}') + + # Check if entries with the same hash (metadata.hash) already exist + if metadata and 'hash' in metadata: + result = VECTOR_DB_CLIENT.query( + collection_name=collection_name, + filter={'hash': metadata['hash']}, + ) + + if result is not None and result.ids and len(result.ids) > 0: + existing_doc_ids = result.ids[0] + if existing_doc_ids: + # Check if the existing document belongs to the same file + # If same file_id, this is a re-add/reindex - allow it + # If different file_id, this is a duplicate - block it + existing_file_id = None + if result.metadatas and result.metadatas[0]: + existing_file_id = result.metadatas[0][0].get('file_id') + + if existing_file_id != metadata.get('file_id'): + log.info(f'Document with hash {metadata["hash"]} already exists') + raise ValueError(ERROR_MESSAGES.DUPLICATE_CONTENT) + + if split: + if request.app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER: + log.info('Using markdown header text splitter') + # Define headers to split on - covering most common markdown header levels + markdown_splitter = MarkdownHeaderTextSplitter( + headers_to_split_on=[ + ('#', 'Header 1'), + ('##', 'Header 2'), + ('###', 'Header 3'), + ('####', 'Header 4'), + ('#####', 'Header 5'), + ('######', 'Header 6'), + ], + strip_headers=False, # Keep headers in content for context + ) + + split_docs = [] + for doc in docs: + split_docs.extend( + [ + Document( + page_content=split_chunk.page_content, + metadata={**doc.metadata}, + ) + for split_chunk in markdown_splitter.split_text(doc.page_content) + ] + ) + + docs = split_docs + if request.app.state.config.CHUNK_MIN_SIZE_TARGET > 0: + docs = merge_docs_to_target_size(request, docs) + + if request.app.state.config.TEXT_SPLITTER in ['', 'character']: + text_splitter = RecursiveCharacterTextSplitter( + chunk_size=request.app.state.config.CHUNK_SIZE, + chunk_overlap=request.app.state.config.CHUNK_OVERLAP, + add_start_index=True, + ) + docs = text_splitter.split_documents(docs) + elif request.app.state.config.TEXT_SPLITTER == 'token': + log.info(f'Using token text splitter: {request.app.state.config.TIKTOKEN_ENCODING_NAME}') + + tiktoken.get_encoding(str(request.app.state.config.TIKTOKEN_ENCODING_NAME)) + text_splitter = TokenTextSplitter( + encoding_name=str(request.app.state.config.TIKTOKEN_ENCODING_NAME), + chunk_size=request.app.state.config.CHUNK_SIZE, + chunk_overlap=request.app.state.config.CHUNK_OVERLAP, + add_start_index=True, + ) + docs = text_splitter.split_documents(docs) + else: + raise ValueError(ERROR_MESSAGES.DEFAULT('Invalid text splitter')) + + if len(docs) == 0: + raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT) + + texts = [sanitize_text_for_db(doc.page_content) for doc in docs] + metadatas = [ + { + **doc.metadata, + **(metadata if metadata else {}), + 'embedding_config': { + 'engine': request.app.state.config.RAG_EMBEDDING_ENGINE, + 'model': request.app.state.config.RAG_EMBEDDING_MODEL, + }, + } + for doc in docs + ] + + try: + if VECTOR_DB_CLIENT.has_collection(collection_name=collection_name): + log.info(f'collection {collection_name} already exists') + + if overwrite: + VECTOR_DB_CLIENT.delete_collection(collection_name=collection_name) + log.info(f'deleting existing collection {collection_name}') + elif add is False: + log.info(f'collection {collection_name} already exists, overwrite is False and add is False') + return True + + log.info(f'generating embeddings for {collection_name}') + embedding_function = get_embedding_function( + request.app.state.config.RAG_EMBEDDING_ENGINE, + request.app.state.config.RAG_EMBEDDING_MODEL, + request.app.state.ef, + ( + request.app.state.config.RAG_OPENAI_API_BASE_URL + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'openai' + else ( + request.app.state.config.RAG_OLLAMA_BASE_URL + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'ollama' + else request.app.state.config.RAG_AZURE_OPENAI_BASE_URL + ) + ), + ( + request.app.state.config.RAG_OPENAI_API_KEY + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'openai' + else ( + request.app.state.config.RAG_OLLAMA_API_KEY + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'ollama' + else request.app.state.config.RAG_AZURE_OPENAI_API_KEY + ) + ), + request.app.state.config.RAG_EMBEDDING_BATCH_SIZE, + azure_api_version=( + request.app.state.config.RAG_AZURE_OPENAI_API_VERSION + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'azure_openai' + else None + ), + enable_async=request.app.state.config.ENABLE_ASYNC_EMBEDDING, + concurrent_requests=request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, + ) + + # Run async embedding in sync context using the main event loop + # This allows the main loop to stay responsive to health checks during long operations + embedding_timeout = RAG_EMBEDDING_TIMEOUT + + future = asyncio.run_coroutine_threadsafe( + embedding_function( + list(map(lambda x: x.replace('\n', ' '), texts)), + prefix=RAG_EMBEDDING_CONTENT_PREFIX, + user=user, + ), + request.app.state.main_loop, + ) + embeddings = future.result(timeout=embedding_timeout) + log.info(f'embeddings generated {len(embeddings)} for {len(texts)} items') + + items = [ + { + 'id': str(uuid.uuid4()), + 'text': text, + 'vector': embeddings[idx], + 'metadata': metadatas[idx], + } + for idx, text in enumerate(texts) + ] + + log.info(f'adding to collection {collection_name}') + VECTOR_DB_CLIENT.insert( + collection_name=collection_name, + items=items, + ) + + log.info(f'added {len(items)} items to collection {collection_name}') + return True + except Exception as e: + log.exception(e) + raise e + + +class ProcessFileForm(BaseModel): + file_id: str + content: Optional[str] = None + collection_name: Optional[str] = None + + +@router.post('/process/file') +async def process_file( + request: Request, + form_data: ProcessFileForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """ + Process a file and save its content to the vector database. + Process a file and save its content to the vector database. + Note: granular session management is used to prevent connection pool exhaustion. + The session is committed before external API calls, and updates use a fresh session. + """ + if user.role == 'admin': + file = await Files.get_file_by_id(form_data.file_id, db=db) + else: + file = await Files.get_file_by_id_and_user_id(form_data.file_id, user.id, db=db) + + if file: + try: + collection_name = form_data.collection_name + + if collection_name is None: + collection_name = f'file-{file.id}' + + if form_data.content: + # Update the content in the file + # Usage: /files/{file_id}/data/content/update, /files/ (audio file upload pipeline) + + try: + # /files/{file_id}/data/content/update + await ASYNC_VECTOR_DB_CLIENT.delete_collection(collection_name=f'file-{file.id}') + except Exception: + # Audio file upload pipeline + pass + + docs = [ + Document( + page_content=form_data.content.replace('
', '\n'), + metadata={ + **file.meta, + 'name': file.filename, + 'created_by': file.user_id, + 'file_id': file.id, + 'source': file.filename, + }, + ) + ] + + text_content = form_data.content + elif form_data.collection_name: + # Check if the file has already been processed and save the content + # Usage: /knowledge/{id}/file/add, /knowledge/{id}/file/update + + result = await ASYNC_VECTOR_DB_CLIENT.query( + collection_name=f'file-{file.id}', filter={'file_id': file.id} + ) + + if result is not None and len(result.ids[0]) > 0: + docs = [ + Document( + page_content=result.documents[0][idx], + metadata=result.metadatas[0][idx], + ) + for idx, id in enumerate(result.ids[0]) + ] + else: + docs = [ + Document( + page_content=file.data.get('content', ''), + metadata={ + **file.meta, + 'name': file.filename, + 'created_by': file.user_id, + 'file_id': file.id, + 'source': file.filename, + }, + ) + ] + + text_content = file.data.get('content', '') + else: + # Process the file and save the content + # Usage: /files/ + file_path = file.path + if file_path: + file_path = await asyncio.to_thread(Storage.get_file, file_path) + loader = build_loader_from_config(request) + loader.user = user + docs = await loader.aload(file.filename, file.meta.get('content_type'), file_path) + + docs = [ + Document( + page_content=doc.page_content, + metadata={ + **filter_metadata(doc.metadata), + 'name': file.filename, + 'created_by': file.user_id, + 'file_id': file.id, + 'source': file.filename, + }, + ) + for doc in docs + ] + else: + docs = [ + Document( + page_content=file.data.get('content', ''), + metadata={ + **file.meta, + 'name': file.filename, + 'created_by': file.user_id, + 'file_id': file.id, + 'source': file.filename, + }, + ) + ] + text_content = ' '.join([doc.page_content for doc in docs]) + + log.debug(f'text_content: {text_content}') + await Files.update_file_data_by_id( + file.id, + {'content': text_content}, + db=db, + ) + hash = calculate_sha256_string(text_content) + + if request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL: + await Files.update_file_data_by_id(file.id, {'status': 'completed'}, db=db) + await Files.update_file_hash_by_id(file.id, hash, db=db) + return { + 'status': True, + 'collection_name': None, + 'filename': file.filename, + 'content': text_content, + } + else: + try: + # Commit any pending changes before the slow embedding step. + # Note: file is already a Pydantic model (not ORM), so no expunge needed. + await db.commit() + + # External embedding API takes time (5-60s+). + # Subsequent updates use fresh async sessions. + # NOTE: save_docs_to_vector_db is a sync function that + # calls asyncio.run_coroutine_threadsafe(..., main_loop).result() + # which blocks the calling thread. We MUST run it in a + # worker thread to avoid deadlocking the event loop. + result = await run_in_threadpool( + save_docs_to_vector_db, + request, + docs=docs, + collection_name=collection_name, + metadata={ + 'file_id': file.id, + 'name': file.filename, + 'hash': hash, + }, + add=(True if form_data.collection_name else False), + user=user, + ) + log.info(f'added {len(docs)} items to collection {collection_name}') + + if result: + # Fresh session for the final update. + async with get_async_db() as session: + await Files.update_file_metadata_by_id( + file.id, + { + 'collection_name': collection_name, + }, + db=session, + ) + + await Files.update_file_data_by_id( + file.id, + {'status': 'completed'}, + db=session, + ) + await Files.update_file_hash_by_id(file.id, hash, db=session) + + return { + 'status': True, + 'collection_name': collection_name, + 'filename': file.filename, + 'content': text_content, + } + else: + raise Exception('Error saving document to vector database') + except Exception as e: + raise e + + except Exception as e: + log.exception(e) + # Fresh session for error status update. + async with get_async_db() as session: + await Files.update_file_data_by_id( + file.id, + {'status': 'failed'}, + db=session, + ) + # Clear the hash so the file can be re-uploaded after fixing the issue + await Files.update_file_hash_by_id(file.id, None, db=session) + + if 'No pandoc was found' in str(e): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.PANDOC_NOT_INSTALLED, + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + + else: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + +class ProcessTextForm(BaseModel): + name: str + content: str + collection_name: Optional[str] = None + + +@router.post('/process/text') +async def process_text( + request: Request, + form_data: ProcessTextForm, + user=Depends(get_verified_user), +): + collection_name = form_data.collection_name + if collection_name is None: + collection_name = calculate_sha256_string(form_data.content) + else: + await _validate_collection_access([collection_name], user, access_type='write') + + docs = [ + Document( + page_content=form_data.content, + metadata={'name': form_data.name, 'created_by': user.id}, + ) + ] + text_content = form_data.content + log.debug(f'text_content: {text_content}') + + result = await run_in_threadpool(save_docs_to_vector_db, request, docs, collection_name, user=user) + if result: + return { + 'status': True, + 'collection_name': collection_name, + 'content': text_content, + } + else: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + +@router.post('/process/youtube') +@router.post('/process/web') +async def process_web( + request: Request, + form_data: ProcessUrlForm, + process: bool = Query(True, description='Whether to process and save the content'), + overwrite: bool = Query(True, description='Whether to overwrite existing collection'), + user=Depends(get_verified_user), +): + try: + content, docs = await run_in_threadpool(get_content_from_url, request, form_data.url) + log.debug(f'text_content: {content}') + + if process: + collection_name = form_data.collection_name + if not collection_name: + collection_name = calculate_sha256_string(form_data.url)[:63] + else: + await _validate_collection_access([collection_name], user, access_type='write') + + if not request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: + await run_in_threadpool( + save_docs_to_vector_db, + request, + docs, + collection_name, + overwrite=overwrite, + add=(not overwrite), + user=user, + ) + else: + collection_name = None + + return { + 'status': True, + 'collection_name': collection_name, + 'filename': form_data.url, + 'file': { + 'data': { + 'content': content, + }, + 'meta': { + 'name': form_data.url, + 'source': form_data.url, + }, + }, + } + else: + return { + 'status': True, + 'content': content, + } + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +def search_web(request: Request, engine: str, query: str, user=None) -> list[SearchResult]: + """Search the web using a search engine and return the results as a list of SearchResult objects. + Will look for a search engine API key in environment variables in the following order: + - SEARXNG_QUERY_URL + - YACY_QUERY_URL + YACY_USERNAME + YACY_PASSWORD + - GOOGLE_PSE_API_KEY + GOOGLE_PSE_ENGINE_ID + - BRAVE_SEARCH_API_KEY + - KAGI_SEARCH_API_KEY + - MOJEEK_SEARCH_API_KEY + - BOCHA_SEARCH_API_KEY + - SERPSTACK_API_KEY + - SERPER_API_KEY + - SERPLY_API_KEY + - TAVILY_API_KEY + - EXA_API_KEY + - PERPLEXITY_API_KEY + - SOUGOU_API_SID + SOUGOU_API_SK + - SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`) + - SERPAPI_API_KEY + SERPAPI_ENGINE (by default `google`) + Args: + query (str): The query to search for + """ + + # TODO: add playwright to search the web + if engine == 'ollama_cloud': + return search_ollama_cloud( + 'https://ollama.com', + request.app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + elif engine == 'perplexity_search': + if request.app.state.config.PERPLEXITY_API_KEY: + return search_perplexity_search( + request.app.state.config.PERPLEXITY_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + request.app.state.config.PERPLEXITY_SEARCH_API_URL, + user, + ) + else: + raise Exception('No PERPLEXITY_API_KEY found in environment variables') + elif engine == 'searxng': + if request.app.state.config.SEARXNG_QUERY_URL: + searxng_kwargs = {'language': request.app.state.config.SEARXNG_LANGUAGE} + return search_searxng( + request.app.state.config.SEARXNG_QUERY_URL, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + **searxng_kwargs, + ) + else: + raise Exception('No SEARXNG_QUERY_URL found in environment variables') + elif engine == 'yacy': + if request.app.state.config.YACY_QUERY_URL: + return search_yacy( + request.app.state.config.YACY_QUERY_URL, + request.app.state.config.YACY_USERNAME, + request.app.state.config.YACY_PASSWORD, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception('No YACY_QUERY_URL found in environment variables') + elif engine == 'google_pse': + if request.app.state.config.GOOGLE_PSE_API_KEY and request.app.state.config.GOOGLE_PSE_ENGINE_ID: + return search_google_pse( + request.app.state.config.GOOGLE_PSE_API_KEY, + request.app.state.config.GOOGLE_PSE_ENGINE_ID, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + referer=request.app.state.config.WEBUI_URL, + ) + else: + raise Exception('No GOOGLE_PSE_API_KEY or GOOGLE_PSE_ENGINE_ID found in environment variables') + elif engine == 'brave': + if request.app.state.config.BRAVE_SEARCH_API_KEY: + return search_brave( + request.app.state.config.BRAVE_SEARCH_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception('No BRAVE_SEARCH_API_KEY found in environment variables') + elif engine == 'kagi': + if request.app.state.config.KAGI_SEARCH_API_KEY: + return search_kagi( + request.app.state.config.KAGI_SEARCH_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception('No KAGI_SEARCH_API_KEY found in environment variables') + elif engine == 'mojeek': + if request.app.state.config.MOJEEK_SEARCH_API_KEY: + return search_mojeek( + request.app.state.config.MOJEEK_SEARCH_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception('No MOJEEK_SEARCH_API_KEY found in environment variables') + elif engine == 'bocha': + if request.app.state.config.BOCHA_SEARCH_API_KEY: + return search_bocha( + request.app.state.config.BOCHA_SEARCH_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception('No BOCHA_SEARCH_API_KEY found in environment variables') + elif engine == 'serpstack': + if request.app.state.config.SERPSTACK_API_KEY: + return search_serpstack( + request.app.state.config.SERPSTACK_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + https_enabled=request.app.state.config.SERPSTACK_HTTPS, + ) + else: + raise Exception('No SERPSTACK_API_KEY found in environment variables') + elif engine == 'serper': + if request.app.state.config.SERPER_API_KEY: + return search_serper( + request.app.state.config.SERPER_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception('No SERPER_API_KEY found in environment variables') + elif engine == 'serply': + if request.app.state.config.SERPLY_API_KEY: + return search_serply( + request.app.state.config.SERPLY_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + filter_list=request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception('No SERPLY_API_KEY found in environment variables') + elif engine == 'duckduckgo': + return search_duckduckgo( + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + concurrent_requests=request.app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS, + backend=request.app.state.config.DDGS_BACKEND, + ) + elif engine == 'tavily': + if request.app.state.config.TAVILY_API_KEY: + return search_tavily( + request.app.state.config.TAVILY_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception('No TAVILY_API_KEY found in environment variables') + elif engine == 'exa': + if request.app.state.config.EXA_API_KEY: + return search_exa( + request.app.state.config.EXA_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception('No EXA_API_KEY found in environment variables') + elif engine == 'searchapi': + if request.app.state.config.SEARCHAPI_API_KEY: + return search_searchapi( + request.app.state.config.SEARCHAPI_API_KEY, + request.app.state.config.SEARCHAPI_ENGINE, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception('No SEARCHAPI_API_KEY found in environment variables') + elif engine == 'serpapi': + if request.app.state.config.SERPAPI_API_KEY: + return search_serpapi( + request.app.state.config.SERPAPI_API_KEY, + request.app.state.config.SERPAPI_ENGINE, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception('No SERPAPI_API_KEY found in environment variables') + elif engine == 'jina': + return search_jina( + request.app.state.config.JINA_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.JINA_API_BASE_URL, + ) + elif engine == 'bing': + return search_bing( + request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY, + request.app.state.config.BING_SEARCH_V7_ENDPOINT, + str(DEFAULT_LOCALE), + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + elif engine == 'azure': + if ( + request.app.state.config.AZURE_AI_SEARCH_API_KEY + and request.app.state.config.AZURE_AI_SEARCH_ENDPOINT + and request.app.state.config.AZURE_AI_SEARCH_INDEX_NAME + ): + return search_azure( + request.app.state.config.AZURE_AI_SEARCH_API_KEY, + request.app.state.config.AZURE_AI_SEARCH_ENDPOINT, + request.app.state.config.AZURE_AI_SEARCH_INDEX_NAME, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception( + 'AZURE_AI_SEARCH_API_KEY, AZURE_AI_SEARCH_ENDPOINT, and AZURE_AI_SEARCH_INDEX_NAME are required for Azure AI Search' + ) + elif engine == 'exa': + return search_exa( + request.app.state.config.EXA_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + elif engine == 'perplexity': + return search_perplexity( + request.app.state.config.PERPLEXITY_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + model=request.app.state.config.PERPLEXITY_MODEL, + search_context_usage=request.app.state.config.PERPLEXITY_SEARCH_CONTEXT_USAGE, + ) + elif engine == 'sougou': + if request.app.state.config.SOUGOU_API_SID and request.app.state.config.SOUGOU_API_SK: + return search_sougou( + request.app.state.config.SOUGOU_API_SID, + request.app.state.config.SOUGOU_API_SK, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception('No SOUGOU_API_SID or SOUGOU_API_SK found in environment variables') + elif engine == 'firecrawl': + return search_firecrawl( + request.app.state.config.FIRECRAWL_API_BASE_URL, + request.app.state.config.FIRECRAWL_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + elif engine == 'external': + return search_external( + request, + request.app.state.config.EXTERNAL_WEB_SEARCH_URL, + request.app.state.config.EXTERNAL_WEB_SEARCH_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + user=user, + ) + elif engine == 'yandex': + return search_yandex( + request, + request.app.state.config.YANDEX_WEB_SEARCH_URL, + request.app.state.config.YANDEX_WEB_SEARCH_API_KEY, + request.app.state.config.YANDEX_WEB_SEARCH_CONFIG, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + user=user, + ) + elif engine == 'youcom': + return search_youcom( + request.app.state.config.YOUCOM_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception('No search engine API key found in environment variables') + + +@router.post('/process/web/search') +async def process_web_search(request: Request, form_data: SearchForm, user=Depends(get_verified_user)): + if not request.app.state.config.ENABLE_WEB_SEARCH: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + if user.role != 'admin' and not await has_permission( + user.id, 'features.web_search', request.app.state.config.USER_PERMISSIONS + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + urls = [] + result_items = [] + + try: + logging.debug(f'trying to web search with {request.app.state.config.WEB_SEARCH_ENGINE, form_data.queries}') + + # Use semaphore to limit concurrent requests based on WEB_SEARCH_CONCURRENT_REQUESTS + # 0 or None = unlimited (previous behavior), positive number = limited concurrency + # Set to 1 for sequential execution (rate-limited APIs like Brave free tier) + concurrent_limit = request.app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS + + if concurrent_limit: + # Limited concurrency with semaphore + semaphore = asyncio.Semaphore(concurrent_limit) + + async def search_query_with_semaphore(query): + async with semaphore: + return await run_in_threadpool( + search_web, + request, + request.app.state.config.WEB_SEARCH_ENGINE, + query, + user, + ) + + search_tasks = [search_query_with_semaphore(query) for query in form_data.queries] + else: + # Unlimited parallel execution (previous behavior) + search_tasks = [ + run_in_threadpool( + search_web, + request, + request.app.state.config.WEB_SEARCH_ENGINE, + query, + user, + ) + for query in form_data.queries + ] + + search_results = await asyncio.gather(*search_tasks) + + for result in search_results: + if result: + for item in result: + if item and item.link: + result_items.append(item) + urls.append(item.link) + + urls = list(dict.fromkeys(urls)) + log.debug(f'urls: {urls}') + + except Exception as e: + log.exception(e) + + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.WEB_SEARCH_ERROR(e), + ) + + if len(urls) == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.DEFAULT('No results found from web search'), + ) + + try: + if request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER: + search_results = [item for result in search_results for item in result if result] + + docs = [ + Document( + page_content=result.snippet, + metadata={ + 'source': result.link, + 'title': result.title, + 'snippet': result.snippet, + 'link': result.link, + }, + ) + for result in search_results + if hasattr(result, 'snippet') and result.snippet is not None + ] + else: + loader = get_web_loader( + urls, + verify_ssl=request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION, + requests_per_second=request.app.state.config.WEB_LOADER_CONCURRENT_REQUESTS, + trust_env=request.app.state.config.WEB_SEARCH_TRUST_ENV, + ) + docs = await loader.aload() + + urls = [ + doc.metadata.get('source') for doc in docs if doc.metadata.get('source') + ] # only keep the urls returned by the loader + result_items = [ + dict(item) for item in result_items if item.link in urls + ] # only keep the search results that have been loaded + + if request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: + return { + 'status': True, + 'collection_name': None, + 'filenames': urls, + 'items': result_items, + 'docs': [ + { + 'content': doc.page_content, + 'metadata': doc.metadata, + } + for doc in docs + ], + 'loaded_count': len(docs), + } + else: + # Create a single collection for all documents + collection_name = f'web-search-{calculate_sha256_string("-".join(form_data.queries))}'[:63] + + try: + await run_in_threadpool( + save_docs_to_vector_db, + request, + docs, + collection_name, + overwrite=True, + user=user, + ) + except Exception as e: + log.debug(f'error saving docs: {e}') + + return { + 'status': True, + 'collection_names': [collection_name], + 'items': result_items, + 'filenames': urls, + 'loaded_count': len(docs), + } + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +async def _validate_collection_access(collection_names: list[str], user, access_type: str = 'read') -> None: + """ + Raise 403 if the user lacks access to any of the requested collections. + Delegates to the shared filter_accessible_collections utility so the + access rules stay in one place. + """ + requested = set(collection_names) + allowed = await filter_accessible_collections(requested, user, access_type=access_type) + denied = requested - allowed + if denied: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + +class QueryDocForm(BaseModel): + collection_name: str + query: str + k: Optional[int] = None + k_reranker: Optional[int] = None + r: Optional[float] = None + hybrid: Optional[bool] = None + + +@router.post('/query/doc') +async def query_doc_handler( + request: Request, + form_data: QueryDocForm, + user=Depends(get_verified_user), +): + await _validate_collection_access([form_data.collection_name], user) + + try: + if request.app.state.config.ENABLE_RAG_HYBRID_SEARCH and (form_data.hybrid is None or form_data.hybrid): + collection_results = {} + collection_results[form_data.collection_name] = await ASYNC_VECTOR_DB_CLIENT.get( + collection_name=form_data.collection_name + ) + return await query_doc_with_hybrid_search( + collection_name=form_data.collection_name, + collection_result=collection_results[form_data.collection_name], + query=form_data.query, + embedding_function=lambda query, prefix: request.app.state.EMBEDDING_FUNCTION( + query, prefix=prefix, user=user + ), + k=form_data.k if form_data.k else request.app.state.config.TOP_K, + reranking_function=( + (lambda query, documents: request.app.state.RERANKING_FUNCTION(query, documents, user=user)) + if request.app.state.RERANKING_FUNCTION + else None + ), + k_reranker=form_data.k_reranker or request.app.state.config.TOP_K_RERANKER, + r=(form_data.r if form_data.r else request.app.state.config.RELEVANCE_THRESHOLD), + hybrid_bm25_weight=( + form_data.hybrid_bm25_weight + if form_data.hybrid_bm25_weight + else request.app.state.config.HYBRID_BM25_WEIGHT + ), + user=user, + ) + else: + query_embedding = await request.app.state.EMBEDDING_FUNCTION( + form_data.query, prefix=RAG_EMBEDDING_QUERY_PREFIX, user=user + ) + # query_doc wraps a blocking VECTOR_DB_CLIENT.search call; + # offload so the request's event loop stays responsive. + return await asyncio.to_thread( + query_doc, + collection_name=form_data.collection_name, + query_embedding=query_embedding, + k=form_data.k if form_data.k else request.app.state.config.TOP_K, + user=user, + ) + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +class QueryCollectionsForm(BaseModel): + collection_names: list[str] + query: str + k: Optional[int] = None + k_reranker: Optional[int] = None + r: Optional[float] = None + hybrid: Optional[bool] = None + hybrid_bm25_weight: Optional[float] = None + enable_enriched_texts: Optional[bool] = None + + +@router.post('/query/collection') +async def query_collection_handler( + request: Request, + form_data: QueryCollectionsForm, + user=Depends(get_verified_user), +): + await _validate_collection_access(form_data.collection_names, user) + + try: + if request.app.state.config.ENABLE_RAG_HYBRID_SEARCH and (form_data.hybrid is None or form_data.hybrid): + return await query_collection_with_hybrid_search( + collection_names=form_data.collection_names, + queries=[form_data.query], + embedding_function=lambda query, prefix: request.app.state.EMBEDDING_FUNCTION( + query, prefix=prefix, user=user + ), + k=form_data.k if form_data.k else request.app.state.config.TOP_K, + reranking_function=( + (lambda query, documents: request.app.state.RERANKING_FUNCTION(query, documents, user=user)) + if request.app.state.RERANKING_FUNCTION + else None + ), + k_reranker=form_data.k_reranker or request.app.state.config.TOP_K_RERANKER, + r=(form_data.r if form_data.r else request.app.state.config.RELEVANCE_THRESHOLD), + hybrid_bm25_weight=( + form_data.hybrid_bm25_weight + if form_data.hybrid_bm25_weight + else request.app.state.config.HYBRID_BM25_WEIGHT + ), + enable_enriched_texts=( + form_data.enable_enriched_texts + if form_data.enable_enriched_texts is not None + else request.app.state.config.ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS + ), + ) + else: + return await query_collection( + request, + collection_names=form_data.collection_names, + queries=[form_data.query], + embedding_function=lambda query, prefix: request.app.state.EMBEDDING_FUNCTION( + query, prefix=prefix, user=user + ), + k=form_data.k if form_data.k else request.app.state.config.TOP_K, + ) + + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +#################################### +# +# Vector DB operations +# +#################################### + + +class DeleteForm(BaseModel): + collection_name: str + file_id: str + + +@router.post('/delete') +async def delete_entries_from_collection( + form_data: DeleteForm, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + try: + if await ASYNC_VECTOR_DB_CLIENT.has_collection(collection_name=form_data.collection_name): + file = await Files.get_file_by_id(form_data.file_id, db=db) + if not file: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + hash = file.hash + + # Refuse to issue a `filter={'hash': None}` query — the + # match semantics of a null filter value are + # backend-dependent (some backends ignore the key, some + # match every row whose metadata lacks `hash`) and risk + # deleting unrelated entries. Files without a hash are + # typically unprocessed / failed / legacy records that + # can't be targeted by hash anyway. + if hash is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + 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. + await ASYNC_VECTOR_DB_CLIENT.delete( + collection_name=form_data.collection_name, + filter={'hash': hash}, + ) + return {'status': True} + else: + return {'status': False} + except HTTPException: + # Caller-meaningful errors (404/400 above) must not be + # swallowed and re-shaped as `{'status': False}`. + raise + except Exception as e: + log.exception(e) + return {'status': False} + + +@router.post('/reset/db') +async def reset_vector_db(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + await ASYNC_VECTOR_DB_CLIENT.reset() + await Knowledges.delete_all_knowledge(db=db) + + +@router.post('/reset/uploads') +async def reset_upload_dir(user=Depends(get_admin_user)) -> bool: + folder = f'{UPLOAD_DIR}' + try: + # Check if the directory exists + if os.path.exists(folder): + # Iterate over all the files and directories in the specified directory + for filename in os.listdir(folder): + file_path = os.path.join(folder, filename) + try: + if os.path.isfile(file_path) or os.path.islink(file_path): + os.unlink(file_path) # Remove the file or link + elif os.path.isdir(file_path): + shutil.rmtree(file_path) # Remove the directory + except Exception as e: + log.exception(f'Failed to delete {file_path}. Reason: {e}') + else: + log.warning(f'The directory {folder} does not exist') + except Exception as e: + log.exception(f'Failed to process the directory {folder}. Reason: {e}') + return True + + +if ENV == 'dev': + + @router.get('/ef/{text}') + async def get_embeddings(request: Request, text: Optional[str] = 'Hello World!'): + return {'result': await request.app.state.EMBEDDING_FUNCTION(text, prefix=RAG_EMBEDDING_QUERY_PREFIX)} + + +class BatchProcessFilesForm(BaseModel): + files: List[FileModel] + collection_name: str + + +class BatchProcessFilesResult(BaseModel): + file_id: str + status: str + error: Optional[str] = None + + +class BatchProcessFilesResponse(BaseModel): + results: List[BatchProcessFilesResult] + errors: List[BatchProcessFilesResult] + + +@router.post('/process/files/batch') +async def process_files_batch( + request: Request, + form_data: BatchProcessFilesForm, + user=Depends(get_verified_user), + db=None, +) -> BatchProcessFilesResponse: + """ + Process a batch of files and save them to the vector database. + + NOTE: We intentionally do NOT use Depends(get_async_session) here. + The save_docs_to_vector_db() call makes external embedding API calls which + can take 5-60+ seconds for batch operations. Database operations after + embedding (Files.update_file_by_id) manage their own short-lived sessions. + """ + + collection_name = form_data.collection_name + + file_results: List[BatchProcessFilesResult] = [] + file_errors: List[BatchProcessFilesResult] = [] + file_updates: List[FileUpdateForm] = [] + + # Prepare all documents first + all_docs: List[Document] = [] + + for file in form_data.files: + try: + # Ownership check: verify the requesting user owns the file or is an admin + db_file = await Files.get_file_by_id(file.id, db=db) + if not db_file: + file_errors.append( + BatchProcessFilesResult( + file_id=file.id, + status='failed', + error='File not found', + ) + ) + continue + if db_file.user_id != user.id and user.role != 'admin': + file_errors.append( + BatchProcessFilesResult( + file_id=file.id, + status='failed', + error='Permission denied: not file owner', + ) + ) + continue + + text_content = file.data.get('content', '') + docs: List[Document] = [ + Document( + page_content=text_content.replace('
', '\n'), + metadata={ + **file.meta, + 'name': file.filename, + 'created_by': file.user_id, + 'file_id': file.id, + 'source': file.filename, + }, + ) + ] + + all_docs.extend(docs) + + file_updates.append( + FileUpdateForm( + hash=calculate_sha256_string(text_content), + data={'content': text_content}, + ) + ) + file_results.append(BatchProcessFilesResult(file_id=file.id, status='prepared')) + + except Exception as e: + log.error(f'process_files_batch: Error processing file {file.id}: {str(e)}') + file_errors.append(BatchProcessFilesResult(file_id=file.id, status='failed', error=str(e))) + + # Save all documents in one batch + if all_docs: + try: + await run_in_threadpool( + save_docs_to_vector_db, + request, + all_docs, + collection_name, + add=True, + user=user, + ) + + # Update all files with collection name + for file_update, file_result in zip(file_updates, file_results): + await Files.update_file_by_id(id=file_result.file_id, form_data=file_update, db=db) + file_result.status = 'completed' + + except Exception as e: + log.error(f'process_files_batch: Error saving documents to vector DB: {str(e)}') + for file_result in file_results: + file_result.status = 'failed' + file_errors.append(BatchProcessFilesResult(file_id=file_result.file_id, status='failed', error=str(e))) + + return BatchProcessFilesResponse(results=file_results, errors=file_errors) diff --git a/backend/open_webui/routers/scim.py b/backend/open_webui/routers/scim.py new file mode 100644 index 0000000000000000000000000000000000000000..75f45bcaf9ae762ee567dfe06c32fe40d1b07507 --- /dev/null +++ b/backend/open_webui/routers/scim.py @@ -0,0 +1,1016 @@ +""" +Experimental SCIM 2.0 Implementation for Open WebUI +Provides System for Cross-domain Identity Management endpoints for users and groups + +NOTE: This is an experimental implementation and may not fully comply with SCIM 2.0 standards, and is subject to change. +""" + +import hmac +import logging +import uuid +import time +from typing import Optional, List, Dict, Any +from datetime import datetime, timezone + +from fastapi import APIRouter, Depends, HTTPException, Request, Query, Header, status +from fastapi.responses import JSONResponse +from pydantic import BaseModel, Field, ConfigDict + +from open_webui.models.users import Users, UserModel +from open_webui.models.groups import Groups, GroupModel +from open_webui.utils.auth import ( + get_admin_user, + get_current_user, + decode_token, + get_verified_user, +) +from open_webui.constants import ERROR_MESSAGES + +from open_webui.config import OAUTH_PROVIDERS +from open_webui.env import SCIM_AUTH_PROVIDER + + +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import get_async_session + +log = logging.getLogger(__name__) + +router = APIRouter() + +# SCIM 2.0 Schema URIs +SCIM_USER_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:User' +SCIM_GROUP_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:Group' +SCIM_LIST_RESPONSE_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:ListResponse' +SCIM_ERROR_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:Error' + +# SCIM Resource Types +SCIM_RESOURCE_TYPE_USER = 'User' +SCIM_RESOURCE_TYPE_GROUP = 'Group' + + +def scim_error(status_code: int, detail: str, scim_type: Optional[str] = None): + """Create a SCIM-compliant error response""" + error_body = { + 'schemas': [SCIM_ERROR_SCHEMA], + 'status': str(status_code), + 'detail': detail, + } + + if scim_type: + error_body['scimType'] = scim_type + elif status_code == 404: + error_body['scimType'] = 'invalidValue' + elif status_code == 409: + error_body['scimType'] = 'uniqueness' + elif status_code == 400: + error_body['scimType'] = 'invalidSyntax' + + return JSONResponse(status_code=status_code, content=error_body) + + +class SCIMError(BaseModel): + """SCIM Error Response""" + + schemas: List[str] = [SCIM_ERROR_SCHEMA] + status: str + scimType: Optional[str] = None + detail: Optional[str] = None + + +class SCIMMeta(BaseModel): + """SCIM Resource Metadata""" + + resourceType: str + created: str + lastModified: str + location: Optional[str] = None + version: Optional[str] = None + + +class SCIMName(BaseModel): + """SCIM User Name""" + + formatted: Optional[str] = None + familyName: Optional[str] = None + givenName: Optional[str] = None + middleName: Optional[str] = None + honorificPrefix: Optional[str] = None + honorificSuffix: Optional[str] = None + + +class SCIMEmail(BaseModel): + """SCIM Email""" + + value: str + type: Optional[str] = 'work' + primary: bool = True + display: Optional[str] = None + + +class SCIMPhoto(BaseModel): + """SCIM Photo""" + + value: str + type: Optional[str] = 'photo' + primary: bool = True + display: Optional[str] = None + + +class SCIMGroupMember(BaseModel): + """SCIM Group Member""" + + value: str # User ID + ref: Optional[str] = Field(None, alias='$ref') + type: Optional[str] = 'User' + display: Optional[str] = None + + +class SCIMUser(BaseModel): + """SCIM User Resource""" + + model_config = ConfigDict(populate_by_name=True) + + schemas: List[str] = [SCIM_USER_SCHEMA] + id: str + externalId: Optional[str] = None + userName: str + name: Optional[SCIMName] = None + displayName: str + emails: List[SCIMEmail] + active: bool = True + photos: Optional[List[SCIMPhoto]] = None + groups: Optional[List[Dict[str, str]]] = None + meta: SCIMMeta + + +class SCIMUserCreateRequest(BaseModel): + """SCIM User Create Request""" + + model_config = ConfigDict(populate_by_name=True) + + schemas: List[str] = [SCIM_USER_SCHEMA] + externalId: Optional[str] = None + userName: str + name: Optional[SCIMName] = None + displayName: str + emails: List[SCIMEmail] + active: bool = True + password: Optional[str] = None + photos: Optional[List[SCIMPhoto]] = None + + +class SCIMUserUpdateRequest(BaseModel): + """SCIM User Update Request""" + + model_config = ConfigDict(populate_by_name=True) + + schemas: List[str] = [SCIM_USER_SCHEMA] + id: Optional[str] = None + externalId: Optional[str] = None + userName: Optional[str] = None + name: Optional[SCIMName] = None + displayName: Optional[str] = None + emails: Optional[List[SCIMEmail]] = None + active: Optional[bool] = None + photos: Optional[List[SCIMPhoto]] = None + + +class SCIMGroup(BaseModel): + """SCIM Group Resource""" + + model_config = ConfigDict(populate_by_name=True) + + schemas: List[str] = [SCIM_GROUP_SCHEMA] + id: str + displayName: str + members: Optional[List[SCIMGroupMember]] = [] + meta: SCIMMeta + + +class SCIMGroupCreateRequest(BaseModel): + """SCIM Group Create Request""" + + model_config = ConfigDict(populate_by_name=True) + + schemas: List[str] = [SCIM_GROUP_SCHEMA] + displayName: str + members: Optional[List[SCIMGroupMember]] = [] + + +class SCIMGroupUpdateRequest(BaseModel): + """SCIM Group Update Request""" + + model_config = ConfigDict(populate_by_name=True) + + schemas: List[str] = [SCIM_GROUP_SCHEMA] + displayName: Optional[str] = None + members: Optional[List[SCIMGroupMember]] = None + + +class SCIMListResponse(BaseModel): + """SCIM List Response""" + + schemas: List[str] = [SCIM_LIST_RESPONSE_SCHEMA] + totalResults: int + itemsPerPage: int + startIndex: int + Resources: List[Any] + + +class SCIMPatchOperation(BaseModel): + """SCIM Patch Operation""" + + op: str # "add", "replace", "remove" + path: Optional[str] = None + value: Optional[Any] = None + + +class SCIMPatchRequest(BaseModel): + """SCIM Patch Request""" + + schemas: List[str] = ['urn:ietf:params:scim:api:messages:2.0:PatchOp'] + Operations: List[SCIMPatchOperation] + + +def get_scim_auth(request: Request, authorization: Optional[str] = Header(None)) -> bool: + """ + Verify SCIM authentication + Checks for SCIM-specific bearer token configured in the system + """ + if not authorization: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Authorization header required', + headers={'WWW-Authenticate': 'Bearer'}, + ) + + try: + parts = authorization.split() + if len(parts) != 2: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Invalid authorization format. Expected: Bearer ', + ) + + scheme, token = parts + if scheme.lower() != 'bearer': + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Invalid authentication scheme', + ) + + # Check if SCIM is enabled + enable_scim = getattr(request.app.state, 'ENABLE_SCIM', False) + log.info(f'SCIM auth check - raw ENABLE_SCIM: {enable_scim}, type: {type(enable_scim)}') + + # Handle both PersistentConfig and direct value + if hasattr(enable_scim, 'value'): + enable_scim = enable_scim.value + + if not enable_scim: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail='SCIM is not enabled', + ) + + # Verify the SCIM token + scim_token = getattr(request.app.state, 'SCIM_TOKEN', None) + # Handle both PersistentConfig and direct value + if hasattr(scim_token, 'value'): + scim_token = scim_token.value + log.debug(f'SCIM token configured: {bool(scim_token)}') + if not scim_token or not hmac.compare_digest(token, scim_token): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Invalid SCIM token', + ) + + return True + except HTTPException: + # Re-raise HTTP exceptions as-is + raise + except Exception as e: + log.error(f'SCIM authentication error: {e}') + import traceback + + log.error(f'Traceback: {traceback.format_exc()}') + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Authentication failed', + ) + + +def get_external_id(user: UserModel) -> Optional[str]: + """Extract externalId from a user's scim data. + + Checks all stored provider entries and returns the first external_id found. + """ + if not user.scim: + return None + for provider_data in user.scim.values(): + if isinstance(provider_data, dict) and 'external_id' in provider_data: + return provider_data['external_id'] + return None + + +def get_scim_provider() -> str: + """Return the configured SCIM auth provider. + + Requires SCIM_AUTH_PROVIDER env var to be set (e.g. 'microsoft', 'oidc'). + """ + if not SCIM_AUTH_PROVIDER: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='SCIM_AUTH_PROVIDER environment variable is required when SCIM is enabled', + ) + return SCIM_AUTH_PROVIDER + + +async def find_user_by_external_id(external_id: str, db=None) -> Optional[UserModel]: + """Find a user by SCIM externalId, falling back to OAuth sub match.""" + provider = get_scim_provider() + user = await Users.get_user_by_scim_external_id(provider, external_id, db=db) + if user: + return user + + # Fallback: check if externalId matches an existing OAuth sub (account linking) + return await Users.get_user_by_oauth_sub(provider, external_id, db=db) + + +async def user_to_scim(user: UserModel, request: Request, db=None) -> SCIMUser: + """Convert internal User model to SCIM User""" + # Parse display name into name components + name_parts = user.name.split(' ', 1) if user.name else ['', ''] + given_name = name_parts[0] if name_parts else '' + family_name = name_parts[1] if len(name_parts) > 1 else '' + + # Get user's groups + user_groups = await Groups.get_groups_by_member_id(user.id, db=db) + groups = [ + { + 'value': group.id, + 'display': group.name, + '$ref': f'{request.base_url}api/v1/scim/v2/Groups/{group.id}', + 'type': 'direct', + } + for group in user_groups + ] + + return SCIMUser( + id=user.id, + externalId=get_external_id(user), + userName=user.email, + name=SCIMName( + formatted=user.name, + givenName=given_name, + familyName=family_name, + ), + displayName=user.name, + emails=[SCIMEmail(value=user.email)], + active=user.role != 'pending', + photos=([SCIMPhoto(value=user.profile_image_url)] if user.profile_image_url else None), + groups=groups if groups else None, + meta=SCIMMeta( + resourceType=SCIM_RESOURCE_TYPE_USER, + created=datetime.fromtimestamp(user.created_at, tz=timezone.utc).isoformat(), + lastModified=datetime.fromtimestamp(user.updated_at, tz=timezone.utc).isoformat(), + location=f'{request.base_url}api/v1/scim/v2/Users/{user.id}', + ), + ) + + +async def group_to_scim(group: GroupModel, request: Request, db=None) -> SCIMGroup: + """Convert internal Group model to SCIM Group""" + member_ids = await Groups.get_group_user_ids_by_id(group.id, db) or [] + + # Batch-fetch all users to avoid N+1 queries + users = await Users.get_users_by_user_ids(member_ids, db=db) if member_ids else [] + members = [ + SCIMGroupMember( + value=user.id, + ref=f'{request.base_url}api/v1/scim/v2/Users/{user.id}', + display=user.name, + ) + for user in users + ] + + return SCIMGroup( + id=group.id, + displayName=group.name, + members=members, + meta=SCIMMeta( + resourceType=SCIM_RESOURCE_TYPE_GROUP, + created=datetime.fromtimestamp(group.created_at, tz=timezone.utc).isoformat(), + lastModified=datetime.fromtimestamp(group.updated_at, tz=timezone.utc).isoformat(), + location=f'{request.base_url}api/v1/scim/v2/Groups/{group.id}', + ), + ) + + +# SCIM Service Provider Config +@router.get('/ServiceProviderConfig') +async def get_service_provider_config(): + """Get SCIM Service Provider Configuration""" + return { + 'schemas': ['urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig'], + 'patch': {'supported': True}, + 'bulk': {'supported': False, 'maxOperations': 1000, 'maxPayloadSize': 1048576}, + 'filter': {'supported': True, 'maxResults': 200}, + 'changePassword': {'supported': False}, + 'sort': {'supported': False}, + 'etag': {'supported': False}, + 'authenticationSchemes': [ + { + 'type': 'oauthbearertoken', + 'name': 'OAuth Bearer Token', + 'description': 'Authentication using OAuth 2.0 Bearer Token', + } + ], + } + + +# SCIM Resource Types +@router.get('/ResourceTypes') +async def get_resource_types(request: Request): + """Get SCIM Resource Types""" + return [ + { + 'schemas': ['urn:ietf:params:scim:schemas:core:2.0:ResourceType'], + 'id': 'User', + 'name': 'User', + 'endpoint': '/Users', + 'schema': SCIM_USER_SCHEMA, + 'meta': { + 'location': f'{request.base_url}api/v1/scim/v2/ResourceTypes/User', + 'resourceType': 'ResourceType', + }, + }, + { + 'schemas': ['urn:ietf:params:scim:schemas:core:2.0:ResourceType'], + 'id': 'Group', + 'name': 'Group', + 'endpoint': '/Groups', + 'schema': SCIM_GROUP_SCHEMA, + 'meta': { + 'location': f'{request.base_url}api/v1/scim/v2/ResourceTypes/Group', + 'resourceType': 'ResourceType', + }, + }, + ] + + +# SCIM Schemas +@router.get('/Schemas') +async def get_schemas(): + """Get SCIM Schemas""" + return [ + { + 'schemas': ['urn:ietf:params:scim:schemas:core:2.0:Schema'], + 'id': SCIM_USER_SCHEMA, + 'name': 'User', + 'description': 'User Account', + 'attributes': [ + { + 'name': 'userName', + 'type': 'string', + 'required': True, + 'uniqueness': 'server', + }, + {'name': 'displayName', 'type': 'string', 'required': True}, + { + 'name': 'emails', + 'type': 'complex', + 'multiValued': True, + 'required': True, + }, + {'name': 'active', 'type': 'boolean', 'required': False}, + ], + }, + { + 'schemas': ['urn:ietf:params:scim:schemas:core:2.0:Schema'], + 'id': SCIM_GROUP_SCHEMA, + 'name': 'Group', + 'description': 'Group', + 'attributes': [ + {'name': 'displayName', 'type': 'string', 'required': True}, + { + 'name': 'members', + 'type': 'complex', + 'multiValued': True, + 'required': False, + }, + ], + }, + ] + + +# Users endpoints +@router.get('/Users', response_model=SCIMListResponse) +async def get_users( + request: Request, + startIndex: int = Query(1), + count: int = Query(20), + filter: Optional[str] = None, + _: bool = Depends(get_scim_auth), + db: AsyncSession = Depends(get_async_session), +): + """List SCIM Users""" + # Clamp per SCIM 2.0 spec (RFC 7644 §3.4.2.4): + # startIndex < 1 SHALL be treated as 1; count < 0 SHALL be treated as 0. + startIndex = max(1, startIndex) + count = max(0, min(100, count)) + skip = startIndex - 1 + limit = count + + # Get users from database + if filter: + # Simple filter parsing - supports userName eq, externalId eq + if 'userName eq' in filter: + email = filter.split('"')[1] + user = await Users.get_user_by_email(email, db=db) + users_list = [user] if user else [] + total = 1 if user else 0 + elif 'externalId eq' in filter: + external_id = filter.split('"')[1] + user = await find_user_by_external_id(external_id, db=db) + users_list = [user] if user else [] + total = 1 if user else 0 + else: + response = await Users.get_users(skip=skip, limit=limit, db=db) + users_list = response['users'] + total = response['total'] + else: + response = await Users.get_users(skip=skip, limit=limit, db=db) + users_list = response['users'] + total = response['total'] + + # Convert to SCIM format + scim_users = [await user_to_scim(user, request, db=db) for user in users_list] + + return SCIMListResponse( + totalResults=total, + itemsPerPage=len(scim_users), + startIndex=startIndex, + Resources=scim_users, + ) + + +@router.get('/Users/{user_id}', response_model=SCIMUser) +async def get_user( + user_id: str, + request: Request, + _: bool = Depends(get_scim_auth), + db: AsyncSession = Depends(get_async_session), +): + """Get SCIM User by ID""" + user = await Users.get_user_by_id(user_id, db=db) + if not user: + return scim_error(status_code=status.HTTP_404_NOT_FOUND, detail=f'User {user_id} not found') + + return await user_to_scim(user, request, db=db) + + +@router.post('/Users', response_model=SCIMUser, status_code=status.HTTP_201_CREATED) +async def create_user( + request: Request, + user_data: SCIMUserCreateRequest, + _: bool = Depends(get_scim_auth), + db: AsyncSession = Depends(get_async_session), +): + """Create SCIM User""" + # Check for duplicate by externalId + if user_data.externalId: + existing_user = await find_user_by_external_id(user_data.externalId, db=db) + if existing_user: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f'User with externalId {user_data.externalId} already exists', + ) + + # Determine primary email (lowercased per RFC 5321) + email = user_data.userName + for entry in user_data.emails: + if entry.primary: + email = entry.value + break + email = email.lower() + + # Check for duplicate by email + existing_user = await Users.get_user_by_email(email, db=db) + if existing_user: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f'User with email {email} already exists', + ) + + # Create user + user_id = str(uuid.uuid4()) + + # Parse name if provided + name = user_data.displayName + if user_data.name: + if user_data.name.formatted: + name = user_data.name.formatted + elif user_data.name.givenName or user_data.name.familyName: + name = f'{user_data.name.givenName or ""} {user_data.name.familyName or ""}'.strip() + + # Get profile image if provided + profile_image = '/user.png' + if user_data.photos and len(user_data.photos) > 0: + profile_image = user_data.photos[0].value + + new_user = await Users.insert_new_user( + id=user_id, + name=name, + email=email, + profile_image_url=profile_image, + role='user' if user_data.active else 'pending', + db=db, + ) + + if not new_user: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Failed to create user', + ) + + # Store externalId in the scim field + if user_data.externalId: + provider = get_scim_provider() + await Users.update_user_scim_by_id(user_id, provider, user_data.externalId, db=db) + new_user = await Users.get_user_by_id(user_id, db=db) + + return await user_to_scim(new_user, request, db=db) + + +@router.put('/Users/{user_id}', response_model=SCIMUser) +async def update_user( + user_id: str, + request: Request, + user_data: SCIMUserUpdateRequest, + _: bool = Depends(get_scim_auth), + db: AsyncSession = Depends(get_async_session), +): + """Update SCIM User (full update)""" + user = await Users.get_user_by_id(user_id, db=db) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f'User {user_id} not found', + ) + + # Build update dict + update_data = {} + + if user_data.userName: + update_data['email'] = user_data.userName + + if user_data.displayName: + update_data['name'] = user_data.displayName + elif user_data.name: + if user_data.name.formatted: + update_data['name'] = user_data.name.formatted + elif user_data.name.givenName or user_data.name.familyName: + update_data['name'] = f'{user_data.name.givenName or ""} {user_data.name.familyName or ""}'.strip() + + if user_data.emails and len(user_data.emails) > 0: + update_data['email'] = user_data.emails[0].value + + if user_data.active is not None: + update_data['role'] = 'user' if user_data.active else 'pending' + + if user_data.photos and len(user_data.photos) > 0: + update_data['profile_image_url'] = user_data.photos[0].value + + updated_user = await Users.update_user_by_id(user_id, update_data, db=db) + if not updated_user: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Failed to update user', + ) + + # Update externalId in the scim field + if user_data.externalId: + provider = get_scim_provider() + await Users.update_user_scim_by_id(user_id, provider, user_data.externalId, db=db) + updated_user = await Users.get_user_by_id(user_id, db=db) + + return await user_to_scim(updated_user, request, db=db) + + +@router.patch('/Users/{user_id}', response_model=SCIMUser) +async def patch_user( + user_id: str, + request: Request, + patch_data: SCIMPatchRequest, + _: bool = Depends(get_scim_auth), + db: AsyncSession = Depends(get_async_session), +): + """Update SCIM User (partial update)""" + user = await Users.get_user_by_id(user_id, db=db) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f'User {user_id} not found', + ) + + update_data = {} + + for operation in patch_data.Operations: + op = operation.op.lower() + path = operation.path + value = operation.value + + if op == 'replace': + if path == 'active': + update_data['role'] = 'user' if value else 'pending' + elif path == 'userName': + update_data['email'] = value + elif path == 'displayName': + update_data['name'] = value + elif path == 'emails[primary eq true].value': + update_data['email'] = value + elif path == 'name.formatted': + update_data['name'] = value + elif path == 'externalId': + provider = get_scim_provider() + await Users.update_user_scim_by_id(user_id, provider, value, db=db) + + # Update user + if update_data: + updated_user = await Users.update_user_by_id(user_id, update_data, db=db) + if not updated_user: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Failed to update user', + ) + else: + updated_user = user + + return await user_to_scim(updated_user, request, db=db) + + +@router.delete('/Users/{user_id}', status_code=status.HTTP_204_NO_CONTENT) +async def delete_user( + user_id: str, + request: Request, + _: bool = Depends(get_scim_auth), + db: AsyncSession = Depends(get_async_session), +): + """Delete SCIM User""" + user = await Users.get_user_by_id(user_id, db=db) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f'User {user_id} not found', + ) + + success = await Users.delete_user_by_id(user_id, db=db) + if not success: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Failed to delete user', + ) + + return None + + +# Groups endpoints +@router.get('/Groups', response_model=SCIMListResponse) +async def get_groups( + request: Request, + startIndex: int = Query(1), + count: int = Query(20), + filter: Optional[str] = None, + _: bool = Depends(get_scim_auth), + db: AsyncSession = Depends(get_async_session), +): + """List SCIM Groups""" + # Clamp per SCIM 2.0 spec (RFC 7644 §3.4.2.4): + # startIndex < 1 SHALL be treated as 1; count < 0 SHALL be treated as 0. + startIndex = max(1, startIndex) + count = max(0, min(100, count)) + + # Get groups, applying filter if provided + if filter: + if 'displayName eq' in filter: + display_name = filter.split('"')[1] + group = await Groups.get_group_by_name(display_name, db=db) + groups_list = [group] if group else [] + else: + # Unrecognized filter — fall back to all groups + groups_list = await Groups.get_all_groups(db=db) + else: + groups_list = await Groups.get_all_groups(db=db) + + # Apply pagination + total = len(groups_list) + start = startIndex - 1 + end = start + count + paginated_groups = groups_list[start:end] + + # Convert to SCIM format + scim_groups = [await group_to_scim(group, request, db=db) for group in paginated_groups] + + return SCIMListResponse( + totalResults=total, + itemsPerPage=len(scim_groups), + startIndex=startIndex, + Resources=scim_groups, + ) + + +@router.get('/Groups/{group_id}', response_model=SCIMGroup) +async def get_group( + group_id: str, + request: Request, + _: bool = Depends(get_scim_auth), + db: AsyncSession = Depends(get_async_session), +): + """Get SCIM Group by ID""" + group = await Groups.get_group_by_id(group_id, db=db) + if not group: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f'Group {group_id} not found', + ) + + return await group_to_scim(group, request, db=db) + + +@router.post('/Groups', response_model=SCIMGroup, status_code=status.HTTP_201_CREATED) +async def create_group( + request: Request, + group_data: SCIMGroupCreateRequest, + _: bool = Depends(get_scim_auth), + db: AsyncSession = Depends(get_async_session), +): + """Create SCIM Group""" + # Extract member IDs + member_ids = [] + if group_data.members: + for member in group_data.members: + member_ids.append(member.value) + + # Create group + from open_webui.models.groups import GroupForm + + form = GroupForm( + name=group_data.displayName, + description='', + ) + + # Need to get the creating user's ID - we'll use the first admin + admin_user = await Users.get_super_admin_user(db=db) + if not admin_user: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='No admin user found', + ) + + new_group = await Groups.insert_new_group(admin_user.id, form, db=db) + if not new_group: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Failed to create group', + ) + + # Add members if provided + if member_ids: + from open_webui.models.groups import GroupUpdateForm + + update_form = GroupUpdateForm( + name=new_group.name, + description=new_group.description, + ) + + await Groups.update_group_by_id(new_group.id, update_form, db=db) + await Groups.set_group_user_ids_by_id(new_group.id, member_ids, db=db) + + new_group = await Groups.get_group_by_id(new_group.id, db=db) + + return await group_to_scim(new_group, request, db=db) + + +@router.put('/Groups/{group_id}', response_model=SCIMGroup) +async def update_group( + group_id: str, + request: Request, + group_data: SCIMGroupUpdateRequest, + _: bool = Depends(get_scim_auth), + db: AsyncSession = Depends(get_async_session), +): + """Update SCIM Group (full update)""" + group = await Groups.get_group_by_id(group_id, db=db) + if not group: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f'Group {group_id} not found', + ) + + # Build update form + from open_webui.models.groups import GroupUpdateForm + + update_form = GroupUpdateForm( + name=group_data.displayName if group_data.displayName else group.name, + description=group.description, + ) + + # Handle members if provided + if group_data.members is not None: + member_ids = [member.value for member in group_data.members] + await Groups.set_group_user_ids_by_id(group_id, member_ids, db=db) + + # Update group + updated_group = await Groups.update_group_by_id(group_id, update_form, db=db) + if not updated_group: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Failed to update group', + ) + + return await group_to_scim(updated_group, request, db=db) + + +@router.patch('/Groups/{group_id}', response_model=SCIMGroup) +async def patch_group( + group_id: str, + request: Request, + patch_data: SCIMPatchRequest, + _: bool = Depends(get_scim_auth), + db: AsyncSession = Depends(get_async_session), +): + """Update SCIM Group (partial update)""" + group = await Groups.get_group_by_id(group_id, db=db) + if not group: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f'Group {group_id} not found', + ) + + from open_webui.models.groups import GroupUpdateForm + + update_form = GroupUpdateForm( + name=group.name, + description=group.description, + ) + + for operation in patch_data.Operations: + op = operation.op.lower() + path = operation.path + value = operation.value + + if op == 'replace': + if path == 'displayName': + update_form.name = value + elif path == 'members': + # Replace all members + await Groups.set_group_user_ids_by_id(group_id, [member['value'] for member in value], db=db) + + elif op == 'add': + if path == 'members': + # Add members + if isinstance(value, list): + for member in value: + if isinstance(member, dict) and 'value' in member: + await Groups.add_users_to_group(group_id, [member['value']], db=db) + elif op == 'remove': + if path and path.startswith('members[value eq'): + # Remove specific member + member_id = path.split('"')[1] + await Groups.remove_users_from_group(group_id, [member_id], db=db) + + # Update group + updated_group = await Groups.update_group_by_id(group_id, update_form, db=db) + if not updated_group: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Failed to update group', + ) + + return await group_to_scim(updated_group, request, db=db) + + +@router.delete('/Groups/{group_id}', status_code=status.HTTP_204_NO_CONTENT) +async def delete_group( + group_id: str, + request: Request, + _: bool = Depends(get_scim_auth), + db: AsyncSession = Depends(get_async_session), +): + """Delete SCIM Group""" + group = await Groups.get_group_by_id(group_id, db=db) + if not group: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f'Group {group_id} not found', + ) + + success = await Groups.delete_group_by_id(group_id, db=db) + if not success: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Failed to delete group', + ) + + return None diff --git a/backend/open_webui/routers/skills.py b/backend/open_webui/routers/skills.py new file mode 100644 index 0000000000000000000000000000000000000000..490d1706d5e16ce82c3bdef873ef6b150a02b3cf --- /dev/null +++ b/backend/open_webui/routers/skills.py @@ -0,0 +1,430 @@ +import logging +from typing import Optional + +from open_webui.models.groups import Groups +from pydantic import BaseModel + +from fastapi import APIRouter, Depends, HTTPException, Request, status +from sqlalchemy.ext.asyncio import AsyncSession + +from open_webui.internal.db import get_async_session +from open_webui.models.skills import ( + SkillForm, + SkillModel, + SkillResponse, + SkillUserResponse, + SkillAccessResponse, + SkillAccessListResponse, + Skills, +) +from open_webui.models.access_grants import AccessGrants +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.access_control import has_permission, filter_allowed_access_grants + +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL +from open_webui.constants import ERROR_MESSAGES + +log = logging.getLogger(__name__) + +PAGE_ITEM_COUNT = 30 + +router = APIRouter() + + +############################ +# GetSkills +############################ + + +@router.get('/', response_model=list[SkillUserResponse]) +async def get_skills( + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: + skills = await Skills.get_skills(db=db) + else: + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} + all_skills = await Skills.get_skills(db=db) + skills = [ + skill + for skill in all_skills + if skill.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='skill', + resource_id=skill.id, + permission='read', + user_group_ids=user_group_ids, + db=db, + ) + ] + + return skills + + +############################ +# GetSkillList +############################ + + +@router.get('/list', response_model=SkillAccessListResponse) +async def get_skill_list( + query: Optional[str] = None, + view_option: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + limit = PAGE_ITEM_COUNT + + page = max(1, page) + skip = (page - 1) * limit + + filter = {} + if query: + filter['query'] = query + if view_option: + filter['view_option'] = view_option + + if not (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL): + groups = await Groups.get_groups_by_member_id(user.id, db=db) + if groups: + filter['group_ids'] = [group.id for group in groups] + + filter['user_id'] = user.id + + result = await Skills.search_skills(user.id, filter=filter, skip=skip, limit=limit, db=db) + + return SkillAccessListResponse( + items=[ + SkillAccessResponse( + **skill.model_dump(), + write_access=( + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == skill.user_id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='skill', + resource_id=skill.id, + permission='write', + db=db, + ) + ), + ) + for skill in result.items + ], + total=result.total, + ) + + +############################ +# ExportSkills +############################ + + +@router.get('/export', response_model=list[SkillModel]) +async def export_skills( + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, + 'workspace.skills', + request.app.state.config.USER_PERMISSIONS, + db=db, + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: + return await Skills.get_skills(db=db) + else: + return await Skills.get_skills_by_user_id(user.id, 'read', db=db) + + +############################ +# CreateNewSkill +############################ + + +@router.post('/create', response_model=Optional[SkillResponse]) +async def create_new_skill( + request: Request, + form_data: SkillForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, 'workspace.skills', request.app.state.config.USER_PERMISSIONS, db=db + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + form_data.id = form_data.id.lower().replace(' ', '-') + + existing = await Skills.get_skill_by_id(form_data.id, db=db) + if existing is not None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ID_TAKEN, + ) + + try: + skill = await Skills.insert_new_skill(user.id, form_data, db=db) + if skill: + return skill + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error creating skill'), + ) + except Exception as e: + log.exception(f'Failed to create skill: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(str(e)), + ) + + +############################ +# GetSkillById +############################ + + +@router.get('/id/{id}', response_model=Optional[SkillAccessResponse]) +async def get_skill_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + skill = await Skills.get_skill_by_id(id, db=db) + + if skill: + if ( + user.role == 'admin' + or skill.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='skill', + resource_id=skill.id, + permission='read', + db=db, + ) + ): + return SkillAccessResponse( + **skill.model_dump(), + write_access=( + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == skill.user_id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='skill', + resource_id=skill.id, + permission='write', + db=db, + ) + ), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# UpdateSkillById +############################ + + +@router.post('/id/{id}/update', response_model=Optional[SkillModel]) +async def update_skill_by_id( + request: Request, + id: str, + form_data: SkillForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + skill = await Skills.get_skill_by_id(id, db=db) + if not skill: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + skill.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='skill', + resource_id=skill.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + try: + updated = { + **form_data.model_dump(exclude={'id'}), + } + + skill = await Skills.update_skill_by_id(id, updated, db=db) + + if skill: + return skill + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error updating skill'), + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(str(e)), + ) + + +############################ +# UpdateSkillAccessById +############################ + + +class SkillAccessGrantsForm(BaseModel): + access_grants: list[dict] + + +@router.post('/id/{id}/access/update', response_model=Optional[SkillModel]) +async def update_skill_access_by_id( + request: Request, + id: str, + form_data: SkillAccessGrantsForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + skill = await Skills.get_skill_by_id(id, db=db) + if not skill: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + skill.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='skill', + resource_id=skill.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_skills', + ) + + await AccessGrants.set_access_grants('skill', id, form_data.access_grants, db=db) + + return await Skills.get_skill_by_id(id, db=db) + + +############################ +# ToggleSkillById +############################ + + +@router.post('/id/{id}/toggle', response_model=Optional[SkillModel]) +async def toggle_skill_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + skill = await Skills.get_skill_by_id(id, db=db) + if skill: + if ( + user.role == 'admin' + or skill.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='skill', + resource_id=skill.id, + permission='write', + db=db, + ) + ): + skill = await Skills.toggle_skill_by_id(id, db=db) + + if skill: + return skill + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error toggling skill'), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# DeleteSkillById +############################ + + +@router.delete('/id/{id}/delete', response_model=bool) +async def delete_skill_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + skill = await Skills.get_skill_by_id(id, db=db) + if not skill: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + skill.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='skill', + resource_id=skill.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + result = await Skills.delete_skill_by_id(id, db=db) + return result diff --git a/backend/open_webui/routers/tasks.py b/backend/open_webui/routers/tasks.py new file mode 100644 index 0000000000000000000000000000000000000000..b921f7b3e660b51a920fe917be49d6072af2545d --- /dev/null +++ b/backend/open_webui/routers/tasks.py @@ -0,0 +1,699 @@ +from fastapi import APIRouter, Depends, HTTPException, Response, status, Request +from fastapi.responses import JSONResponse, RedirectResponse + +from pydantic import BaseModel +from typing import Optional +import logging +import re + +from open_webui.utils.chat import generate_chat_completion +from open_webui.utils.task import ( + title_generation_template, + follow_up_generation_template, + query_generation_template, + image_prompt_generation_template, + autocomplete_generation_template, + tags_generation_template, + emoji_generation_template, + moa_response_generation_template, +) +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.constants import ERROR_MESSAGES, TASKS + +from open_webui.routers.pipelines import process_pipeline_inlet_filter + +from open_webui.utils.task import get_task_model_id + +from open_webui.config import ( + DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE, + DEFAULT_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE, + DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE, + DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE, + DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE, + DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE, + DEFAULT_EMOJI_GENERATION_PROMPT_TEMPLATE, + DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE, + DEFAULT_VOICE_MODE_PROMPT_TEMPLATE, +) + +log = logging.getLogger(__name__) + +router = APIRouter() + + +################################## +# +# Task Endpoints +# +################################## + + +class ActiveChatsForm(BaseModel): + chat_ids: list[str] + + +@router.post('/active/chats') +async def check_active_chats(request: Request, form_data: ActiveChatsForm, user=Depends(get_verified_user)): + """Check which chat IDs have active tasks.""" + from open_webui.tasks import get_active_chat_ids + + active = await get_active_chat_ids(request.app.state.redis, form_data.chat_ids) + return {'active_chat_ids': active} + + +@router.get('/config') +async def get_task_config(request: Request, user=Depends(get_verified_user)): + return { + 'TASK_MODEL': request.app.state.config.TASK_MODEL, + 'TASK_MODEL_EXTERNAL': request.app.state.config.TASK_MODEL_EXTERNAL, + 'TITLE_GENERATION_PROMPT_TEMPLATE': request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE, + 'IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE': request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE, + 'ENABLE_AUTOCOMPLETE_GENERATION': request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION, + 'AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH': request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH, + 'TAGS_GENERATION_PROMPT_TEMPLATE': request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE, + 'FOLLOW_UP_GENERATION_PROMPT_TEMPLATE': request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE, + 'ENABLE_FOLLOW_UP_GENERATION': request.app.state.config.ENABLE_FOLLOW_UP_GENERATION, + 'ENABLE_TAGS_GENERATION': request.app.state.config.ENABLE_TAGS_GENERATION, + 'ENABLE_TITLE_GENERATION': request.app.state.config.ENABLE_TITLE_GENERATION, + 'ENABLE_SEARCH_QUERY_GENERATION': request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION, + 'ENABLE_RETRIEVAL_QUERY_GENERATION': request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION, + 'QUERY_GENERATION_PROMPT_TEMPLATE': request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE, + 'TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE': request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, + 'VOICE_MODE_PROMPT_TEMPLATE': request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE, + } + + +class TaskConfigForm(BaseModel): + TASK_MODEL: Optional[str] + TASK_MODEL_EXTERNAL: Optional[str] + ENABLE_TITLE_GENERATION: bool + TITLE_GENERATION_PROMPT_TEMPLATE: str + IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE: str + ENABLE_AUTOCOMPLETE_GENERATION: bool + AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH: int + TAGS_GENERATION_PROMPT_TEMPLATE: str + FOLLOW_UP_GENERATION_PROMPT_TEMPLATE: str + ENABLE_FOLLOW_UP_GENERATION: bool + ENABLE_TAGS_GENERATION: bool + ENABLE_SEARCH_QUERY_GENERATION: bool + ENABLE_RETRIEVAL_QUERY_GENERATION: bool + QUERY_GENERATION_PROMPT_TEMPLATE: str + TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE: str + VOICE_MODE_PROMPT_TEMPLATE: Optional[str] + + +@router.post('/config/update') +async def update_task_config(request: Request, form_data: TaskConfigForm, user=Depends(get_admin_user)): + request.app.state.config.TASK_MODEL = form_data.TASK_MODEL + request.app.state.config.TASK_MODEL_EXTERNAL = form_data.TASK_MODEL_EXTERNAL + request.app.state.config.ENABLE_TITLE_GENERATION = form_data.ENABLE_TITLE_GENERATION + request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = form_data.TITLE_GENERATION_PROMPT_TEMPLATE + + request.app.state.config.ENABLE_FOLLOW_UP_GENERATION = form_data.ENABLE_FOLLOW_UP_GENERATION + request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = form_data.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE + + request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = form_data.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE + + request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = form_data.ENABLE_AUTOCOMPLETE_GENERATION + request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = ( + form_data.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH + ) + + request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = form_data.TAGS_GENERATION_PROMPT_TEMPLATE + request.app.state.config.ENABLE_TAGS_GENERATION = form_data.ENABLE_TAGS_GENERATION + request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION = form_data.ENABLE_SEARCH_QUERY_GENERATION + request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = form_data.ENABLE_RETRIEVAL_QUERY_GENERATION + + request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE = form_data.QUERY_GENERATION_PROMPT_TEMPLATE + request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = form_data.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE + + request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE = form_data.VOICE_MODE_PROMPT_TEMPLATE + + return { + 'TASK_MODEL': request.app.state.config.TASK_MODEL, + 'TASK_MODEL_EXTERNAL': request.app.state.config.TASK_MODEL_EXTERNAL, + 'ENABLE_TITLE_GENERATION': request.app.state.config.ENABLE_TITLE_GENERATION, + 'TITLE_GENERATION_PROMPT_TEMPLATE': request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE, + 'IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE': request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE, + 'ENABLE_AUTOCOMPLETE_GENERATION': request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION, + 'AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH': request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH, + 'TAGS_GENERATION_PROMPT_TEMPLATE': request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE, + 'ENABLE_TAGS_GENERATION': request.app.state.config.ENABLE_TAGS_GENERATION, + 'ENABLE_FOLLOW_UP_GENERATION': request.app.state.config.ENABLE_FOLLOW_UP_GENERATION, + 'FOLLOW_UP_GENERATION_PROMPT_TEMPLATE': request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE, + 'ENABLE_SEARCH_QUERY_GENERATION': request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION, + 'ENABLE_RETRIEVAL_QUERY_GENERATION': request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION, + 'QUERY_GENERATION_PROMPT_TEMPLATE': request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE, + 'TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE': request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, + 'VOICE_MODE_PROMPT_TEMPLATE': request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE, + } + + +@router.post('/title/completions') +async def generate_title(request: Request, form_data: dict, user=Depends(get_verified_user)): + if not request.app.state.config.ENABLE_TITLE_GENERATION: + return JSONResponse( + status_code=status.HTTP_200_OK, + content={'detail': 'Title generation is disabled'}, + ) + + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): + models = { + request.state.model['id']: request.state.model, + } + else: + models = request.app.state.MODELS + + model_id = form_data['model'] + if model_id not in models: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), + ) + + # Check if the user has a custom task model + # If the user has a custom task model, use that model + task_model_id = get_task_model_id( + model_id, + request.app.state.config.TASK_MODEL, + request.app.state.config.TASK_MODEL_EXTERNAL, + models, + ) + + log.debug(f'generating chat title using model {task_model_id} for user {user.email} ') + + if request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE != '': + template = request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE + else: + template = DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE + + content = title_generation_template(template, form_data['messages'], user) + + max_tokens = models[task_model_id].get('info', {}).get('params', {}).get('max_tokens', 1000) + + payload = { + 'model': task_model_id, + 'messages': [{'role': 'user', 'content': content}], + 'stream': False, + **( + {'max_tokens': max_tokens} + if models[task_model_id].get('owned_by') == 'ollama' + else { + 'max_completion_tokens': max_tokens, + } + ), + 'metadata': { + **(request.state.metadata if hasattr(request.state, 'metadata') else {}), + 'task': str(TASKS.TITLE_GENERATION), + 'task_body': form_data, + 'chat_id': form_data.get('chat_id', None), + }, + } + + # Process the payload through the pipeline + try: + payload = await process_pipeline_inlet_filter(request, payload, user, models) + except Exception as e: + raise e + + try: + return await generate_chat_completion(request, form_data=payload, user=user) + except Exception as e: + log.error('Exception occurred', exc_info=True) + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={'detail': 'An internal error has occurred.'}, + ) + + +@router.post('/follow_up/completions') +async def generate_follow_ups(request: Request, form_data: dict, user=Depends(get_verified_user)): + if not request.app.state.config.ENABLE_FOLLOW_UP_GENERATION: + return JSONResponse( + status_code=status.HTTP_200_OK, + content={'detail': 'Follow-up generation is disabled'}, + ) + + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): + models = { + request.state.model['id']: request.state.model, + } + else: + models = request.app.state.MODELS + + model_id = form_data['model'] + if model_id not in models: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), + ) + + # Check if the user has a custom task model + # If the user has a custom task model, use that model + task_model_id = get_task_model_id( + model_id, + request.app.state.config.TASK_MODEL, + request.app.state.config.TASK_MODEL_EXTERNAL, + models, + ) + + log.debug(f'generating chat title using model {task_model_id} for user {user.email} ') + + if request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE != '': + template = request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE + else: + template = DEFAULT_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE + + content = follow_up_generation_template(template, form_data['messages'], user) + + payload = { + 'model': task_model_id, + 'messages': [{'role': 'user', 'content': content}], + 'stream': False, + 'metadata': { + **(request.state.metadata if hasattr(request.state, 'metadata') else {}), + 'task': str(TASKS.FOLLOW_UP_GENERATION), + 'task_body': form_data, + 'chat_id': form_data.get('chat_id', None), + }, + } + + # Process the payload through the pipeline + try: + payload = await process_pipeline_inlet_filter(request, payload, user, models) + except Exception as e: + raise e + + try: + return await generate_chat_completion(request, form_data=payload, user=user) + except Exception as e: + log.error('Exception occurred', exc_info=True) + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={'detail': 'An internal error has occurred.'}, + ) + + +@router.post('/tags/completions') +async def generate_chat_tags(request: Request, form_data: dict, user=Depends(get_verified_user)): + if not request.app.state.config.ENABLE_TAGS_GENERATION: + return JSONResponse( + status_code=status.HTTP_200_OK, + content={'detail': 'Tags generation is disabled'}, + ) + + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): + models = { + request.state.model['id']: request.state.model, + } + else: + models = request.app.state.MODELS + + model_id = form_data['model'] + if model_id not in models: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), + ) + + # Check if the user has a custom task model + # If the user has a custom task model, use that model + task_model_id = get_task_model_id( + model_id, + request.app.state.config.TASK_MODEL, + request.app.state.config.TASK_MODEL_EXTERNAL, + models, + ) + + log.debug(f'generating chat tags using model {task_model_id} for user {user.email} ') + + if request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE != '': + template = request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE + else: + template = DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE + + content = tags_generation_template(template, form_data['messages'], user) + + payload = { + 'model': task_model_id, + 'messages': [{'role': 'user', 'content': content}], + 'stream': False, + 'metadata': { + **(request.state.metadata if hasattr(request.state, 'metadata') else {}), + 'task': str(TASKS.TAGS_GENERATION), + 'task_body': form_data, + 'chat_id': form_data.get('chat_id', None), + }, + } + + # Process the payload through the pipeline + try: + payload = await process_pipeline_inlet_filter(request, payload, user, models) + except Exception as e: + raise e + + try: + return await generate_chat_completion(request, form_data=payload, user=user) + except Exception as e: + log.error(f'Error generating chat completion: {e}') + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={'detail': 'An internal error has occurred.'}, + ) + + +@router.post('/image_prompt/completions') +async def generate_image_prompt(request: Request, form_data: dict, user=Depends(get_verified_user)): + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): + models = { + request.state.model['id']: request.state.model, + } + else: + models = request.app.state.MODELS + + model_id = form_data['model'] + if model_id not in models: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), + ) + + # Check if the user has a custom task model + # If the user has a custom task model, use that model + task_model_id = get_task_model_id( + model_id, + request.app.state.config.TASK_MODEL, + request.app.state.config.TASK_MODEL_EXTERNAL, + models, + ) + + log.debug(f'generating image prompt using model {task_model_id} for user {user.email} ') + + if request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE != '': + template = request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE + else: + template = DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE + + content = image_prompt_generation_template(template, form_data['messages'], user) + + payload = { + 'model': task_model_id, + 'messages': [{'role': 'user', 'content': content}], + 'stream': False, + 'metadata': { + **(request.state.metadata if hasattr(request.state, 'metadata') else {}), + 'task': str(TASKS.IMAGE_PROMPT_GENERATION), + 'task_body': form_data, + 'chat_id': form_data.get('chat_id', None), + }, + } + + # Process the payload through the pipeline + try: + payload = await process_pipeline_inlet_filter(request, payload, user, models) + except Exception as e: + raise e + + try: + return await generate_chat_completion(request, form_data=payload, user=user) + except Exception as e: + log.error('Exception occurred', exc_info=True) + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={'detail': 'An internal error has occurred.'}, + ) + + +@router.post('/queries/completions') +async def generate_queries(request: Request, form_data: dict, user=Depends(get_verified_user)): + type = form_data.get('type') + if type == 'web_search': + if not request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.FEATURE_DISABLED('Search query generation'), + ) + elif type == 'retrieval': + if not request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.FEATURE_DISABLED('Query generation'), + ) + + if getattr(request.state, 'cached_queries', None): + log.info(f'Reusing cached queries: {request.state.cached_queries}') + return request.state.cached_queries + + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): + models = { + request.state.model['id']: request.state.model, + } + else: + models = request.app.state.MODELS + + model_id = form_data['model'] + if model_id not in models: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), + ) + + # Check if the user has a custom task model + # If the user has a custom task model, use that model + task_model_id = get_task_model_id( + model_id, + request.app.state.config.TASK_MODEL, + request.app.state.config.TASK_MODEL_EXTERNAL, + models, + ) + + log.debug(f'generating {type} queries using model {task_model_id} for user {user.email}') + + if (request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE).strip() != '': + template = request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE + else: + template = DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE + + content = query_generation_template(template, form_data['messages'], user) + + payload = { + 'model': task_model_id, + 'messages': [{'role': 'user', 'content': content}], + 'stream': False, + 'metadata': { + **(request.state.metadata if hasattr(request.state, 'metadata') else {}), + 'task': str(TASKS.QUERY_GENERATION), + 'task_body': form_data, + 'chat_id': form_data.get('chat_id', None), + }, + } + + # Process the payload through the pipeline + try: + payload = await process_pipeline_inlet_filter(request, payload, user, models) + except Exception as e: + raise e + + try: + return await generate_chat_completion(request, form_data=payload, user=user) + except Exception as e: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={'detail': str(e)}, + ) + + +@router.post('/auto/completions') +async def generate_autocompletion(request: Request, form_data: dict, user=Depends(get_verified_user)): + if not request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.FEATURE_DISABLED('Autocompletion generation'), + ) + + type = form_data.get('type') + prompt = form_data.get('prompt') + messages = form_data.get('messages') + + if request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH > 0: + if len(prompt) > request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.INPUT_TOO_LONG(request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH), + ) + + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): + models = { + request.state.model['id']: request.state.model, + } + else: + models = request.app.state.MODELS + + model_id = form_data['model'] + if model_id not in models: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), + ) + + # Check if the user has a custom task model + # If the user has a custom task model, use that model + task_model_id = get_task_model_id( + model_id, + request.app.state.config.TASK_MODEL, + request.app.state.config.TASK_MODEL_EXTERNAL, + models, + ) + + log.debug(f'generating autocompletion using model {task_model_id} for user {user.email}') + + if (request.app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE).strip() != '': + template = request.app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE + else: + template = DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE + + content = autocomplete_generation_template(template, prompt, messages, type, user) + + payload = { + 'model': task_model_id, + 'messages': [{'role': 'user', 'content': content}], + 'stream': False, + 'metadata': { + **(request.state.metadata if hasattr(request.state, 'metadata') else {}), + 'task': str(TASKS.AUTOCOMPLETE_GENERATION), + 'task_body': form_data, + 'chat_id': form_data.get('chat_id', None), + }, + } + + # Process the payload through the pipeline + try: + payload = await process_pipeline_inlet_filter(request, payload, user, models) + except Exception as e: + raise e + + try: + return await generate_chat_completion(request, form_data=payload, user=user) + except Exception as e: + log.error(f'Error generating chat completion: {e}') + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={'detail': 'An internal error has occurred.'}, + ) + + +@router.post('/emoji/completions') +async def generate_emoji(request: Request, form_data: dict, user=Depends(get_verified_user)): + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): + models = { + request.state.model['id']: request.state.model, + } + else: + models = request.app.state.MODELS + + model_id = form_data['model'] + if model_id not in models: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), + ) + + # Check if the user has a custom task model + # If the user has a custom task model, use that model + task_model_id = get_task_model_id( + model_id, + request.app.state.config.TASK_MODEL, + request.app.state.config.TASK_MODEL_EXTERNAL, + models, + ) + + log.debug(f'generating emoji using model {task_model_id} for user {user.email} ') + + template = DEFAULT_EMOJI_GENERATION_PROMPT_TEMPLATE + + content = emoji_generation_template(template, form_data['prompt'], user) + + payload = { + 'model': task_model_id, + 'messages': [{'role': 'user', 'content': content}], + 'stream': False, + **( + {'max_tokens': 4} + if models[task_model_id].get('owned_by') == 'ollama' + else { + 'max_completion_tokens': 4, + } + ), + 'metadata': { + **(request.state.metadata if hasattr(request.state, 'metadata') else {}), + 'task': str(TASKS.EMOJI_GENERATION), + 'task_body': form_data, + 'chat_id': form_data.get('chat_id', None), + }, + } + + # Process the payload through the pipeline + try: + payload = await process_pipeline_inlet_filter(request, payload, user, models) + except Exception as e: + raise e + + try: + return await generate_chat_completion(request, form_data=payload, user=user) + except Exception as e: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={'detail': str(e)}, + ) + + +@router.post('/moa/completions') +async def generate_moa_response(request: Request, form_data: dict, user=Depends(get_verified_user)): + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): + models = { + request.state.model['id']: request.state.model, + } + else: + models = request.app.state.MODELS + + model_id = form_data['model'] + + if model_id not in models: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), + ) + + template = DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE + + content = moa_response_generation_template( + template, + form_data['prompt'], + form_data['responses'], + ) + + payload = { + 'model': model_id, + 'messages': [{'role': 'user', 'content': content}], + 'stream': form_data.get('stream', False), + 'metadata': { + **(request.state.metadata if hasattr(request.state, 'metadata') else {}), + 'chat_id': form_data.get('chat_id', None), + 'task': str(TASKS.MOA_RESPONSE_GENERATION), + 'task_body': form_data, + }, + } + + # Process the payload through the pipeline + try: + payload = await process_pipeline_inlet_filter(request, payload, user, models) + except Exception as e: + raise e + + try: + return await generate_chat_completion(request, form_data=payload, user=user) + except Exception as e: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={'detail': str(e)}, + ) diff --git a/backend/open_webui/routers/terminals.py b/backend/open_webui/routers/terminals.py new file mode 100644 index 0000000000000000000000000000000000000000..003db069681c1926a6229d891adb155cf9a2c0da --- /dev/null +++ b/backend/open_webui/routers/terminals.py @@ -0,0 +1,336 @@ +"""Reverse proxy for admin-configured terminal servers. + +Routes: + GET / — list terminals the user has access to + * /{server_id}/{path:path} — proxy request to terminal server +""" + +import logging +import posixpath +from urllib.parse import unquote + +import aiohttp +from fastapi import APIRouter, Depends, Request, Response, WebSocket +from fastapi.responses import JSONResponse, StreamingResponse +from starlette.background import BackgroundTask + +from open_webui.utils.auth import get_verified_user +from open_webui.utils.access_control import has_connection_access +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL +from open_webui.models.groups import Groups +from open_webui.models.users import Users + +log = logging.getLogger(__name__) + +router = APIRouter() + +STREAMING_CONTENT_TYPES = ('application/octet-stream', 'image/', 'application/pdf') +STRIPPED_RESPONSE_HEADERS = frozenset(('transfer-encoding', 'connection', 'content-encoding', 'content-length')) + + +def _sanitize_proxy_path(path: str) -> str | None: + """Sanitize a proxy path to prevent directory traversal / SSRF. + + Returns the cleaned path, or None if the path is invalid. + Trailing slashes are preserved — many upstream frameworks treat + ``/path`` and ``/path/`` differently. + """ + decoded = unquote(path) + had_trailing_slash = decoded.endswith('/') + normalized = posixpath.normpath(decoded) + # Remove any leading slashes that would reset the base + cleaned = normalized.lstrip('/') + # Reject if normpath resolved to parent traversal or current-dir only + if cleaned.startswith('..') or cleaned == '.': + return None + # Restore trailing slash if the original path had one + if had_trailing_slash and cleaned and not cleaned.endswith('/'): + cleaned += '/' + return cleaned + + +@router.get('/') +async def list_terminal_servers(request: Request, user=Depends(get_verified_user)): + """Return terminal servers the authenticated user has access to.""" + connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or [] + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id)} + + return [ + { + 'id': connection.get('id', ''), + 'url': connection.get('url', ''), + 'name': connection.get('name', ''), + } + for connection in connections + if connection.get('enabled', True) and await has_connection_access(user, connection, user_group_ids) + ] + + +PROXY_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] + + +@router.api_route('/{server_id}/{path:path}', methods=PROXY_METHODS) +async def proxy_terminal( + server_id: str, + path: str, + request: Request, + user=Depends(get_verified_user), +): + """Proxy a request to the admin terminal server identified by *server_id*.""" + connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or [] + connection = next((c for c in connections if c.get('id') == server_id), None) + + if connection is None: + return JSONResponse({'error': f"Terminal server '{server_id}' not found"}, status_code=404) + + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id)} + if not await has_connection_access(user, connection, user_group_ids): + return JSONResponse({'error': 'Access denied'}, status_code=403) + + base_url = (connection.get('url') or '').rstrip('/') + if not base_url: + return JSONResponse({'error': 'Terminal server URL not configured'}, status_code=503) + + safe_path = _sanitize_proxy_path(path) + if safe_path is None: + return JSONResponse({'error': 'Invalid path'}, status_code=400) + + target_url = f'{base_url}/{safe_path}' + + # Route through orchestrator policy endpoint if policy_id is set + policy_id = connection.get('policy_id') + if policy_id: + target_url = f'{base_url}/p/{policy_id}/{safe_path}' + + if request.query_params: + target_url += f'?{request.query_params}' + + headers = {'X-User-Id': user.id} + # Forward per-session cwd tracking header + session_id = request.headers.get('x-session-id') + if session_id: + headers['X-Session-Id'] = session_id + cookies = {} + auth_type = connection.get('auth_type', 'bearer') + + if auth_type == 'bearer': + headers['Authorization'] = f'Bearer {connection.get("key", "")}' + elif auth_type == 'session': + cookies = request.cookies + headers['Authorization'] = f'Bearer {request.state.token.credentials}' + elif auth_type == 'system_oauth': + cookies = request.cookies + oauth_token = request.headers.get('x-oauth-access-token', '') + if oauth_token: + headers['Authorization'] = f'Bearer {oauth_token}' + # auth_type == "none": no Authorization header + + content_type = request.headers.get('content-type') + if content_type: + headers['Content-Type'] = content_type + + body = await request.body() + session = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=300, connect=10), + trust_env=True, + ) + + try: + upstream_response = await session.request( + method=request.method, + url=target_url, + headers=headers, + cookies=cookies, + data=body or None, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) + + upstream_content_type = upstream_response.headers.get('content-type', '') + filtered_headers = { + key: value + for key, value in upstream_response.headers.items() + if key.lower() not in STRIPPED_RESPONSE_HEADERS + } + + # Stream binary responses directly + if any(t in upstream_content_type for t in STREAMING_CONTENT_TYPES): + + async def cleanup(): + await upstream_response.release() + await session.close() + + return StreamingResponse( + content=upstream_response.content.iter_any(), + status_code=upstream_response.status, + headers=filtered_headers, + background=BackgroundTask(cleanup), + ) + + # Buffer text/JSON responses + response_body = await upstream_response.read() + status_code = upstream_response.status + await upstream_response.release() + await session.close() + + return Response(content=response_body, status_code=status_code, headers=filtered_headers) + + except Exception as error: + await session.close() + log.exception('Terminal proxy error: %s', error) + 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): + """Authenticate a WebSocket via first-message auth and resolve the terminal server. + + The client must send ``{"type": "auth", "token": ""}`` as its first + message after connecting. + + Returns ``(user, connection)`` on success, or ``None`` after closing *ws* + with an appropriate error code. + """ + import asyncio + import json + from open_webui.utils.auth import decode_token + + # First-message authentication + try: + raw = await asyncio.wait_for(ws.receive_text(), timeout=10.0) + payload = json.loads(raw) + if payload.get('type') != 'auth': + await ws.close(code=4001, reason='Expected auth message') + return None + token = payload.get('token', '') + data = decode_token(token) + if data is None or 'id' not in data: + await ws.close(code=4001, reason='Invalid token') + return None + user = await Users.get_user_by_id(data['id']) + if user is None: + await ws.close(code=4001, reason='User not found') + return None + except (asyncio.TimeoutError, json.JSONDecodeError): + await ws.close(code=4001, reason='Auth timeout or invalid payload') + return None + except Exception: + await ws.close(code=4001, reason='Invalid token') + return None + + # Resolve terminal server + connections = ws.app.state.config.TERMINAL_SERVER_CONNECTIONS or [] + connection = next((c for c in connections if c.get('id') == server_id), None) + + if connection is None: + await ws.close(code=4004, reason='Terminal server not found') + return None + + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id)} + if not await has_connection_access(user, connection, user_group_ids): + await ws.close(code=4003, reason='Access denied') + return None + + return user, connection + + +@router.websocket('/{server_id}/api/terminals/{session_id}') +async def ws_terminal( + ws: WebSocket, + server_id: str, + session_id: str, +): + """Proxy an interactive WebSocket terminal session to a terminal server. + + Uses first-message auth: the client sends ``{"type": "auth", "token": ""}`` + as its first message. The proxy validates the JWT, then connects to the + upstream terminal server and authenticates with the server's API key. + """ + await ws.accept() + + result = await _resolve_authenticated_connection(ws, server_id) + if result is None: + return + user, connection = result + + base_url = (connection.get('url') or '').rstrip('/') + if not base_url: + await ws.close(code=4003, reason='Terminal server URL not configured') + return + + # Build upstream WebSocket URL (no token in URL) + ws_base = base_url.replace('https://', 'wss://').replace('http://', 'ws://') + + # Route through orchestrator policy endpoint if policy_id is set + policy_id = connection.get('policy_id') + upstream_params = {} + # For orchestrator-backed servers, pass user_id + upstream_params['user_id'] = user.id + + import urllib.parse + + if policy_id: + upstream_url = f'{ws_base}/p/{policy_id}/api/terminals/{session_id}' + else: + upstream_url = f'{ws_base}/api/terminals/{session_id}' + if upstream_params: + upstream_url += f'?{urllib.parse.urlencode(upstream_params)}' + + session = aiohttp.ClientSession() + try: + async with session.ws_connect(upstream_url, ssl=AIOHTTP_CLIENT_SESSION_SSL) as upstream: + import asyncio + import json as _json + + # First-message auth to upstream terminal server + auth_type = connection.get('auth_type', 'bearer') + if auth_type == 'bearer': + key = connection.get('key', '') + await upstream.send_str(_json.dumps({'type': 'auth', 'token': key})) + + async def _client_to_upstream(): + """Forward client → upstream.""" + try: + while True: + msg = await ws.receive() + if msg['type'] == 'websocket.disconnect': + break + elif 'bytes' in msg and msg['bytes']: + await upstream.send_bytes(msg['bytes']) + elif 'text' in msg and msg['text']: + await upstream.send_str(msg['text']) + except Exception: + pass + + async def _upstream_to_client(): + """Forward upstream → client.""" + try: + async for msg in upstream: + if msg.type == aiohttp.WSMsgType.BINARY: + await ws.send_bytes(msg.data) + elif msg.type == aiohttp.WSMsgType.TEXT: + await ws.send_text(msg.data) + elif msg.type in ( + aiohttp.WSMsgType.CLOSE, + aiohttp.WSMsgType.ERROR, + ): + break + except Exception: + pass + + await asyncio.gather( + _client_to_upstream(), + _upstream_to_client(), + return_exceptions=True, + ) + except Exception as e: + log.exception('Terminal WebSocket proxy error: %s', e) + finally: + await session.close() + try: + await ws.close() + except Exception: + pass diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..04d845c3deae7e1a5d3f257c03554891ea8515c1 --- /dev/null +++ b/backend/open_webui/routers/tools.py @@ -0,0 +1,919 @@ +import logging +from pathlib import Path +from typing import Optional +import time +import re +import aiohttp +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT +from open_webui.models.groups import Groups +from pydantic import BaseModel, HttpUrl +from fastapi import APIRouter, Depends, HTTPException, Request, status +from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.internal.db import get_async_session + + +from open_webui.models.oauth_sessions import OAuthSessions +from open_webui.models.tools import ( + ToolForm, + ToolModel, + ToolResponse, + ToolUserResponse, + ToolAccessResponse, + Tools, +) +from open_webui.models.access_grants import AccessGrants +from open_webui.utils.plugin import ( + load_tool_module_by_id, + replace_imports, + get_tool_module_from_cache, + resolve_valves_schema_options, +) +from open_webui.utils.tools import get_tool_specs +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.access_control import ( + has_permission, + has_access, + filter_allowed_access_grants, +) +from open_webui.utils.tools import get_tool_servers + +from open_webui.config import CACHE_DIR, BYPASS_ADMIN_ACCESS_CONTROL +from open_webui.constants import ERROR_MESSAGES + +log = logging.getLogger(__name__) + + +router = APIRouter() + + +async def get_tool_module(request, tool_id, load_from_db=True): + """ + Get the tool module by its ID. + """ + tool_module, _ = await get_tool_module_from_cache(request, tool_id, load_from_db) + return tool_module + + +############################ +# GetTools +# 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]) +async def get_tools( + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + tools = [] + + # Local Tools + for tool in await Tools.get_tools(defer_content=True, db=db): + tool_module = request.app.state.TOOLS.get(tool.id) if hasattr(request.app.state, 'TOOLS') else None + tools.append( + ToolUserResponse( + **{ + **tool.model_dump(), + 'has_user_valves': (hasattr(tool_module, 'UserValves') if tool_module else False), + } + ) + ) + + # OpenAPI Tool Servers + server_access_grants = {} + for server in await get_tool_servers(request): + server_idx = server.get('idx', 0) + connections = request.app.state.config.TOOL_SERVER_CONNECTIONS + if server_idx >= len(connections): + log.warning( + f'Tool server index {server_idx} out of range ' + f'(have {len(connections)} connections), skipping server {server.get("id")}' + ) + continue + connection = connections[server_idx] + server_config = connection.get('config', {}) + + server_id = f'server:{server.get("id")}' + server_access_grants[server_id] = server_config.get('access_grants', []) + + tools.append( + ToolUserResponse( + **{ + 'id': server_id, + 'user_id': server_id, + 'name': server.get('openapi', {}).get('info', {}).get('title', 'Tool Server'), + 'meta': { + 'description': server.get('openapi', {}).get('info', {}).get('description', ''), + }, + 'updated_at': int(time.time()), + 'created_at': int(time.time()), + } + ) + ) + + # MCP Tool Servers + for server in request.app.state.config.TOOL_SERVER_CONNECTIONS: + if server.get('type', 'openapi') == 'mcp' and server.get('config', {}).get('enable'): + server_id = server.get('info', {}).get('id') + auth_type = server.get('auth_type', 'none') + + session_token = None + if auth_type in ('oauth_2.1', 'oauth_2.1_static'): + splits = server_id.split(':') + server_id = splits[-1] if len(splits) > 1 else server_id + + session_token = await request.app.state.oauth_client_manager.get_oauth_token( + user.id, f'mcp:{server_id}' + ) + + server_config = server.get('config', {}) + + tool_id = f'server:mcp:{server.get("info", {}).get("id")}' + server_access_grants[tool_id] = server_config.get('access_grants', []) + + tools.append( + ToolUserResponse( + **{ + 'id': tool_id, + 'user_id': tool_id, + 'name': server.get('info', {}).get('name', 'MCP Tool Server'), + 'meta': { + 'description': server.get('info', {}).get('description', ''), + }, + 'updated_at': int(time.time()), + 'created_at': int(time.time()), + **( + { + 'authenticated': session_token is not None, + } + if auth_type in ('oauth_2.1', 'oauth_2.1_static') + else {} + ), + } + ) + ) + + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: + # Admin can see all tools + return tools + else: + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} + filtered_tools = [] + for tool in tools: + if tool.user_id == user.id: + filtered_tools.append(tool) + elif str(tool.id).startswith('server:'): + if await has_access( + user.id, + 'read', + server_access_grants.get(str(tool.id), []), + user_group_ids, + db=db, + ): + filtered_tools.append(tool) + elif await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tool.id, + permission='read', + user_group_ids=user_group_ids, + db=db, + ): + filtered_tools.append(tool) + return filtered_tools + + +############################ +# GetToolList +############################ + + +@router.get('/list', response_model=list[ToolAccessResponse]) +async def get_tool_list(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: + tools = await Tools.get_tools(defer_content=True, db=db) + else: + tools = await Tools.get_tools_by_user_id(user.id, 'read', defer_content=True, db=db) + + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} + + result = [] + for tool in tools: + has_write = ( + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == tool.user_id + or any( + g.permission == 'write' + and ( + (g.principal_type == 'user' and (g.principal_id == user.id or g.principal_id == '*')) + or (g.principal_type == 'group' and g.principal_id in user_group_ids) + ) + for g in tool.access_grants + ) + ) + result.append( + ToolAccessResponse( + **tool.model_dump(), + write_access=has_write, + ) + ) + return result + + +############################ +# LoadFunctionFromLink +############################ + + +class LoadUrlForm(BaseModel): + url: HttpUrl + + +def github_url_to_raw_url(url: str) -> str: + # Handle 'tree' (folder) URLs (add main.py at the end) + m1 = re.match(r'https://github\.com/([^/]+)/([^/]+)/tree/([^/]+)/(.*)', url) + if m1: + org, repo, branch, path = m1.groups() + return f'https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path.rstrip("/")}/main.py' + + # Handle 'blob' (file) URLs + m2 = re.match(r'https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)', url) + if m2: + org, repo, branch, path = m2.groups() + return f'https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path}' + + # No match; return as-is + return url + + +@router.post('/load/url', response_model=Optional[dict]) +async def load_tool_from_url(request: Request, form_data: LoadUrlForm, user=Depends(get_admin_user)): + # NOTE: This is NOT a SSRF vulnerability: + # This endpoint is admin-only (see get_admin_user), meant for *trusted* internal use, + # and does NOT accept untrusted user input. Access is enforced by authentication. + + url = str(form_data.url) + if not url: + raise HTTPException(status_code=400, detail='Please enter a valid URL') + + url = github_url_to_raw_url(url) + url_parts = url.rstrip('/').split('/') + + file_name = url_parts[-1] + tool_name = ( + file_name[:-3] + if (file_name.endswith('.py') and (not file_name.startswith(('main.py', 'index.py', '__init__.py')))) + else url_parts[-2] + if len(url_parts) > 1 + else 'function' + ) + + try: + async with aiohttp.ClientSession( + trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + ) as session: + async with session.get( + url, headers={'Content-Type': 'application/json'}, ssl=AIOHTTP_CLIENT_SESSION_SSL + ) as resp: + if resp.status != 200: + raise HTTPException(status_code=resp.status, detail='Failed to fetch the tool') + data = await resp.text() + if not data: + raise HTTPException(status_code=400, detail='No data received from the URL') + return { + 'name': tool_name, + 'content': data, + } + except Exception as e: + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.DEFAULT(e)) + + +############################ +# ExportTools +############################ + + +@router.get('/export', response_model=list[ToolModel]) +async def export_tools( + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, + 'workspace.tools_export', + request.app.state.config.USER_PERMISSIONS, + db=db, + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: + return await Tools.get_tools(db=db) + else: + return await Tools.get_tools_by_user_id(user.id, 'read', db=db) + + +############################ +# CreateNewTools +############################ + + +@router.post('/create', response_model=Optional[ToolResponse]) +async def create_new_tools( + request: Request, + form_data: ToolForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not ( + await has_permission(user.id, 'workspace.tools', request.app.state.config.USER_PERMISSIONS, db=db) + or await has_permission( + user.id, + 'workspace.tools_import', + request.app.state.config.USER_PERMISSIONS, + db=db, + ) + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + if not form_data.id.isidentifier(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Only alphanumeric characters and underscores are allowed in the id', + ) + + form_data.id = form_data.id.lower() + + tools = await Tools.get_tool_by_id(form_data.id, db=db) + if tools is None: + try: + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_tools', + ) + + form_data.content = replace_imports(form_data.content) + tool_module, frontmatter = await load_tool_module_by_id(form_data.id, content=form_data.content) + form_data.meta.manifest = frontmatter + + TOOLS = request.app.state.TOOLS + TOOLS[form_data.id] = tool_module + + specs = get_tool_specs(TOOLS[form_data.id]) + tools = await Tools.insert_new_tool(user.id, form_data, specs, db=db) + + tool_cache_dir = CACHE_DIR / 'tools' / form_data.id + tool_cache_dir.mkdir(parents=True, exist_ok=True) + + if tools: + return tools + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error creating tools'), + ) + except Exception as e: + log.exception(f'Failed to load the tool by id {form_data.id}: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(str(e)), + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ID_TAKEN, + ) + + +############################ +# GetToolsById +############################ + + +@router.get('/id/{id}', response_model=Optional[ToolAccessResponse]) +async def get_tools_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + tools = await Tools.get_tool_by_id(id, db=db) + + if tools: + if ( + user.role == 'admin' + or tools.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='read', + db=db, + ) + ): + return ToolAccessResponse( + **tools.model_dump(), + write_access=( + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == tools.user_id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='write', + db=db, + ) + ), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# UpdateToolsById +############################ + + +@router.post('/id/{id}/update', response_model=Optional[ToolModel]) +async def update_tools_by_id( + request: Request, + id: str, + form_data: ToolForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + tools = await Tools.get_tool_by_id(id, db=db) + if not tools: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + # Is the user the original creator, in a group with write access, or an admin + if ( + tools.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + try: + form_data.content = replace_imports(form_data.content) + tool_module, frontmatter = await load_tool_module_by_id(id, content=form_data.content) + form_data.meta.manifest = frontmatter + + TOOLS = request.app.state.TOOLS + TOOLS[id] = tool_module + + specs = get_tool_specs(TOOLS[id]) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_tools', + ) + + updated = { + **form_data.model_dump(exclude={'id'}), + 'specs': specs, + } + + log.debug(updated) + tools = await Tools.update_tool_by_id(id, updated, db=db) + + if tools: + return tools + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error updating tools'), + ) + + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(str(e)), + ) + + +############################ +# UpdateToolAccessById +############################ + + +class ToolAccessGrantsForm(BaseModel): + access_grants: list[dict] + + +@router.post('/id/{id}/access/update', response_model=Optional[ToolModel]) +async def update_tool_access_by_id( + request: Request, + id: str, + form_data: ToolAccessGrantsForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + tools = await Tools.get_tool_by_id(id, db=db) + if not tools: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + tools.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_tools', + ) + + await AccessGrants.set_access_grants('tool', id, form_data.access_grants, db=db) + + return await Tools.get_tool_by_id(id, db=db) + + +############################ +# DeleteToolsById +############################ + + +@router.delete('/id/{id}/delete', response_model=bool) +async def delete_tools_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + tools = await Tools.get_tool_by_id(id, db=db) + if not tools: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + tools.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + result = await Tools.delete_tool_by_id(id, db=db) + if result: + TOOLS = request.app.state.TOOLS + if id in TOOLS: + del TOOLS[id] + + return result + + +############################ +# GetToolValves +############################ + + +@router.get('/id/{id}/valves', response_model=Optional[dict]) +async def get_tools_valves_by_id( + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + tools = await Tools.get_tool_by_id(id, db=db) + if not tools: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + tools.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + try: + valves = await Tools.get_tool_valves_by_id(id, db=db) + return valves + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(str(e)), + ) + + +############################ +# GetToolValvesSpec +############################ + + +@router.get('/id/{id}/valves/spec', response_model=Optional[dict]) +async def get_tools_valves_spec_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + tools = await Tools.get_tool_by_id(id, db=db) + if not tools: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + tools.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + if id in request.app.state.TOOLS: + tools_module = request.app.state.TOOLS[id] + else: + tools_module, _ = await load_tool_module_by_id(id) + request.app.state.TOOLS[id] = tools_module + + if hasattr(tools_module, 'Valves'): + Valves = tools_module.Valves + schema = Valves.schema() + # Resolve dynamic options for select dropdowns + schema = resolve_valves_schema_options(Valves, schema, user) + return schema + return None + + +############################ +# UpdateToolValves +############################ + + +@router.post('/id/{id}/valves/update', response_model=Optional[dict]) +async def update_tools_valves_by_id( + request: Request, + id: str, + form_data: dict, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + tools = await Tools.get_tool_by_id(id, db=db) + if not tools: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + tools.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + if id in request.app.state.TOOLS: + tools_module = request.app.state.TOOLS[id] + else: + tools_module, _ = await load_tool_module_by_id(id) + request.app.state.TOOLS[id] = tools_module + + if not hasattr(tools_module, 'Valves'): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + Valves = tools_module.Valves + + try: + form_data = {k: v for k, v in form_data.items() if v is not None} + valves = Valves(**form_data) + valves_dict = valves.model_dump(exclude_unset=True) + await Tools.update_tool_valves_by_id(id, valves_dict, db=db) + return valves_dict + except Exception as e: + log.exception(f'Failed to update tool valves by id {id}: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(str(e)), + ) + + +############################ +# ToolUserValves +############################ + + +@router.get('/id/{id}/valves/user', response_model=Optional[dict]) +async def get_tools_user_valves_by_id( + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + tools = await Tools.get_tool_by_id(id, db=db) + if not tools: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + tools.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='read', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + try: + user_valves = await Tools.get_user_valves_by_id_and_user_id(id, user.id, db=db) + return user_valves + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(str(e)), + ) + + +@router.get('/id/{id}/valves/user/spec', response_model=Optional[dict]) +async def get_tools_user_valves_spec_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + tools = await Tools.get_tool_by_id(id, db=db) + if not tools: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + tools.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='read', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + if id in request.app.state.TOOLS: + tools_module = request.app.state.TOOLS[id] + else: + tools_module, _ = await load_tool_module_by_id(id) + request.app.state.TOOLS[id] = tools_module + + if hasattr(tools_module, 'UserValves'): + UserValves = tools_module.UserValves + schema = UserValves.schema() + # Resolve dynamic options for select dropdowns + schema = resolve_valves_schema_options(UserValves, schema, user) + return schema + return None + + +@router.post('/id/{id}/valves/user/update', response_model=Optional[dict]) +async def update_tools_user_valves_by_id( + request: Request, + id: str, + form_data: dict, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + tools = await Tools.get_tool_by_id(id, db=db) + if not tools: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + tools.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='read', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + if id in request.app.state.TOOLS: + tools_module = request.app.state.TOOLS[id] + else: + tools_module, _ = await load_tool_module_by_id(id) + request.app.state.TOOLS[id] = tools_module + + if hasattr(tools_module, 'UserValves'): + UserValves = tools_module.UserValves + + try: + form_data = {k: v for k, v in form_data.items() if v is not None} + user_valves = UserValves(**form_data) + user_valves_dict = user_valves.model_dump(exclude_unset=True) + await Tools.update_user_valves_by_id_and_user_id(id, user.id, user_valves_dict, db=db) + return user_valves_dict + except Exception as e: + log.exception(f'Failed to update user valves by id {id}: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(str(e)), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py new file mode 100644 index 0000000000000000000000000000000000000000..04be89c92f4c6a9d30597e32cfd6cd266e7b8be9 --- /dev/null +++ b/backend/open_webui/routers/users.py @@ -0,0 +1,673 @@ +import logging +from typing import Optional +from sqlalchemy.ext.asyncio import AsyncSession +import base64 +import io + + +from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi.responses import Response, StreamingResponse, FileResponse +from pydantic import BaseModel, ConfigDict + + +from open_webui.models.auths import Auths +from open_webui.models.oauth_sessions import OAuthSessions + +from open_webui.models.groups import Groups + +from open_webui.models.users import ( + UserModel, + UserGroupIdsModel, + UserGroupIdsListResponse, + UserInfoResponse, + UserInfoListResponse, + UserRoleUpdateForm, + UserStatus, + Users, + UserSettings, + UserUpdateForm, +) + +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import STATIC_DIR +from open_webui.internal.db import get_async_session + + +from open_webui.utils.auth import ( + get_admin_user, + get_password_hash, + get_verified_user, + validate_password, +) +from open_webui.utils.access_control import get_permissions, has_permission +from open_webui.socket.main import disconnect_user_sessions + +log = logging.getLogger(__name__) + +router = APIRouter() + + +############################ +# GetUsers +# A house is only as strong as its care for the least of +# its members. Let none here be counted without being served. +############################ + + +PAGE_ITEM_COUNT = 30 + + +@router.get('/', response_model=UserGroupIdsListResponse) +async def get_users( + query: Optional[str] = None, + order_by: Optional[str] = None, + direction: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + limit = PAGE_ITEM_COUNT + + page = max(1, page) + skip = (page - 1) * limit + + filter = {} + if query: + filter['query'] = query + if order_by: + filter['order_by'] = order_by + if direction: + filter['direction'] = direction + + filter['direction'] = direction + + result = await Users.get_users(filter=filter, skip=skip, limit=limit, db=db) + + users = result['users'] + total = result['total'] + + # Fetch groups for all users in a single query to avoid N+1 + user_ids = [user.id for user in users] + user_groups = await Groups.get_groups_by_member_ids(user_ids, db=db) + + return { + 'users': [ + UserGroupIdsModel( + **{ + **user.model_dump(), + 'group_ids': [group.id for group in user_groups.get(user.id, [])], + } + ) + for user in users + ], + 'total': total, + } + + +@router.get('/all', response_model=UserInfoListResponse) +async def get_all_users( + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + return await Users.get_users(db=db) + + +@router.get('/search', response_model=UserInfoListResponse) +async def search_users( + query: Optional[str] = None, + order_by: Optional[str] = None, + direction: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + limit = PAGE_ITEM_COUNT + + page = max(1, page) + skip = (page - 1) * limit + + filter = {} + if query: + filter['query'] = query + if order_by: + filter['order_by'] = order_by + if direction: + filter['direction'] = direction + + return await Users.get_users(filter=filter, skip=skip, limit=limit, db=db) + + +############################ +# User Groups +############################ + + +@router.get('/groups') +async def get_user_groups(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + return await Groups.get_groups_by_member_id(user.id, db=db) + + +############################ +# User Permissions +############################ + + +@router.get('/permissions') +async def get_user_permissisions( + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + user_permissions = await get_permissions(user.id, request.app.state.config.USER_PERMISSIONS, db=db) + + return user_permissions + + +############################ +# User Default Permissions +############################ +class WorkspacePermissions(BaseModel): + models: bool = False + knowledge: bool = False + prompts: bool = False + tools: bool = False + skills: bool = False + models_import: bool = False + models_export: bool = False + prompts_import: bool = False + prompts_export: bool = False + tools_import: bool = False + tools_export: bool = False + + +class SharingPermissions(BaseModel): + models: bool = False + public_models: bool = False + knowledge: bool = False + public_knowledge: bool = False + prompts: bool = False + public_prompts: bool = False + tools: bool = False + public_tools: bool = True + skills: bool = False + public_skills: bool = False + notes: bool = False + public_notes: bool = True + + +class AccessGrantsPermissions(BaseModel): + allow_users: bool = True + + +class ChatPermissions(BaseModel): + controls: bool = True + valves: bool = True + system_prompt: bool = True + params: bool = True + file_upload: bool = True + web_upload: bool = True + delete: bool = True + delete_message: bool = True + continue_response: bool = True + regenerate_response: bool = True + rate_response: bool = True + edit: bool = True + share: bool = True + export: bool = True + stt: bool = True + tts: bool = True + call: bool = True + multiple_models: bool = True + temporary: bool = True + temporary_enforced: bool = False + + +class FeaturesPermissions(BaseModel): + api_keys: bool = False + notes: bool = True + channels: bool = True + folders: bool = True + direct_tool_servers: bool = False + + web_search: bool = True + image_generation: bool = True + code_interpreter: bool = True + memories: bool = True + automations: bool = False + + +class SettingsPermissions(BaseModel): + interface: bool = True + + +class UserPermissions(BaseModel): + workspace: WorkspacePermissions + sharing: SharingPermissions + access_grants: AccessGrantsPermissions + chat: ChatPermissions + features: FeaturesPermissions + settings: SettingsPermissions + + +@router.get('/default/permissions', response_model=UserPermissions) +async def get_default_user_permissions(request: Request, user=Depends(get_admin_user)): + return { + 'workspace': WorkspacePermissions(**request.app.state.config.USER_PERMISSIONS.get('workspace', {})), + 'sharing': SharingPermissions(**request.app.state.config.USER_PERMISSIONS.get('sharing', {})), + 'access_grants': AccessGrantsPermissions(**request.app.state.config.USER_PERMISSIONS.get('access_grants', {})), + 'chat': ChatPermissions(**request.app.state.config.USER_PERMISSIONS.get('chat', {})), + 'features': FeaturesPermissions(**request.app.state.config.USER_PERMISSIONS.get('features', {})), + 'settings': SettingsPermissions(**request.app.state.config.USER_PERMISSIONS.get('settings', {})), + } + + +@router.post('/default/permissions') +async def update_default_user_permissions(request: Request, form_data: UserPermissions, user=Depends(get_admin_user)): + request.app.state.config.USER_PERMISSIONS = form_data.model_dump() + return request.app.state.config.USER_PERMISSIONS + + +############################ +# GetUserSettingsBySessionUser +############################ + + +@router.get('/user/settings', response_model=Optional[UserSettings]) +async def get_user_settings_by_session_user( + user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + # user already fetched by get_verified_user — no need to refetch + return user.settings + + +############################ +# UpdateUserSettingsBySessionUser +############################ + + +@router.post('/user/settings/update', response_model=UserSettings) +async def update_user_settings_by_session_user( + request: Request, + form_data: UserSettings, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + updated_user_settings = form_data.model_dump() + ui_settings = updated_user_settings.get('ui') + if ( + user.role != 'admin' + and ui_settings is not None + and 'toolServers' in ui_settings.keys() + and not await has_permission( + user.id, + 'features.direct_tool_servers', + request.app.state.config.USER_PERMISSIONS, + ) + ): + # If the user is not an admin and does not have permission to use tool servers, remove the key + updated_user_settings['ui'].pop('toolServers', None) + + user = await Users.update_user_settings_by_id(user.id, updated_user_settings, db=db) + if user: + return user.settings + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + +############################ +# GetUserStatusBySessionUser +############################ + + +@router.get('/user/status') +async def get_user_status_by_session_user( + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if not request.app.state.config.ENABLE_USER_STATUS: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACTION_PROHIBITED, + ) + # user already fetched by get_verified_user — no need to refetch + return user + + +############################ +# UpdateUserStatusBySessionUser +############################ + + +@router.post('/user/status/update') +async def update_user_status_by_session_user( + request: Request, + form_data: UserStatus, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if not request.app.state.config.ENABLE_USER_STATUS: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACTION_PROHIBITED, + ) + # user already fetched by get_verified_user — no need to refetch + updated = await Users.update_user_status_by_id(user.id, form_data, db=db) + if updated: + return updated + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + +############################ +# GetUserInfoBySessionUser +############################ + + +@router.get('/user/info', response_model=Optional[dict]) +async def get_user_info_by_session_user(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + # user already fetched by get_verified_user — no need to refetch + return user.info + + +############################ +# UpdateUserInfoBySessionUser +############################ + + +@router.post('/user/info/update', response_model=Optional[dict]) +async def update_user_info_by_session_user( + form_data: dict, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + # Merges against the auth-time snapshot of user.info. The previous pre-merge + # refetch only narrowed (did not eliminate) the lost-update window on concurrent + # same-user writes; real safety needs row locking or a version column. + existing_info = user.info or {} + updated = await Users.update_user_by_id(user.id, {'info': {**existing_info, **form_data}}, db=db) + if updated: + return updated.info + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + +############################ +# GetUserById +############################ + + +class UserActiveResponse(UserStatus): + name: str + profile_image_url: Optional[str] = None + groups: Optional[list] = [] + + is_active: bool + model_config = ConfigDict(extra='allow') + + +@router.get('/{user_id}', response_model=UserActiveResponse) +async def get_user_by_id(user_id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + + user = await Users.get_user_by_id(user_id, db=db) + if user: + groups = await Groups.get_groups_by_member_id(user_id, db=db) + return UserActiveResponse( + **{ + **user.model_dump(), + 'groups': [{'id': group.id, 'name': group.name} for group in groups], + 'is_active': await Users.is_user_active(user_id, db=db), + } + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + +@router.get('/{user_id}/info', response_model=UserInfoResponse) +async def get_user_info_by_id( + user_id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + user = await Users.get_user_by_id(user_id, db=db) + if user: + groups = await Groups.get_groups_by_member_id(user_id, db=db) + return UserInfoResponse( + **{ + **user.model_dump(), + 'groups': [{'id': group.id, 'name': group.name} for group in groups], + 'is_active': await Users.is_user_active(user_id, db=db), + } + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + +@router.get('/{user_id}/oauth/sessions') +async def get_user_oauth_sessions_by_id( + user_id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session) +): + sessions = await OAuthSessions.get_sessions_by_user_id(user_id, db=db) + if sessions and len(sessions) > 0: + return sessions + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + +############################ +# GetUserProfileImageById +############################ + + +@router.get('/{user_id}/profile/image') +async def get_user_profile_image_by_id(user_id: str, user=Depends(get_verified_user)): + user = await Users.get_user_by_id(user_id) + if user: + if user.profile_image_url: + # check if it's url or base64 + if user.profile_image_url.startswith('http'): + return Response( + status_code=status.HTTP_302_FOUND, + headers={'Location': user.profile_image_url}, + ) + elif user.profile_image_url.startswith('data:image'): + try: + header, base64_data = user.profile_image_url.split(',', 1) + image_data = base64.b64decode(base64_data) + image_buffer = io.BytesIO(image_data) + media_type = header.split(';')[0].lstrip('data:') + + return StreamingResponse( + image_buffer, + media_type=media_type, + headers={'Content-Disposition': 'inline'}, + ) + except Exception as e: + pass + return FileResponse(f'{STATIC_DIR}/user.png') + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + +############################ +# GetUserActiveStatusById +############################ + + +@router.get('/{user_id}/active', response_model=dict) +async def get_user_active_status_by_id( + user_id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + return { + 'active': await Users.is_user_active(user_id, db=db), + } + + +############################ +# UpdateUserById +############################ + + +@router.post('/{user_id}/update', response_model=Optional[UserModel]) +async def update_user_by_id( + user_id: str, + form_data: UserUpdateForm, + session_user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + # Prevent modification of the primary admin user by other admins + try: + first_user = await Users.get_first_user(db=db) + if first_user: + if user_id == first_user.id: + if session_user.id != user_id: + # If the user trying to update is the primary admin, and they are not the primary admin themselves + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACTION_PROHIBITED, + ) + + if form_data.role is not None and form_data.role != 'admin': + # If the primary admin is trying to change their own role, prevent it + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACTION_PROHIBITED, + ) + + except HTTPException: + raise + except Exception as e: + log.error(f'Error checking primary admin status: {e}') + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Could not verify primary admin status.', + ) + + user = await Users.get_user_by_id(user_id, db=db) + + if user: + if form_data.email is not None and form_data.email.lower() != user.email: + email_user = await Users.get_user_by_email(form_data.email.lower(), db=db) + if email_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.EMAIL_TAKEN, + ) + + if form_data.password: + try: + validate_password(form_data.password) + except Exception as e: + raise HTTPException(400, detail=str(e)) + + hashed = get_password_hash(form_data.password) + await Auths.update_user_password_by_id(user_id, hashed, db=db) + + # Build update dict from only the provided fields + update_data = {} + if form_data.role is not None: + update_data['role'] = form_data.role + if form_data.name is not None: + update_data['name'] = form_data.name + if form_data.email is not None: + update_data['email'] = form_data.email.lower() + await Auths.update_email_by_id(user_id, form_data.email.lower(), db=db) + if form_data.profile_image_url is not None: + update_data['profile_image_url'] = form_data.profile_image_url + + if update_data: + updated_user = await Users.update_user_by_id( + user_id, + update_data, + db=db, + ) + else: + updated_user = user + + if updated_user: + # If the role changed, disconnect all socket sessions so stale + # privileges cached in SESSION_POOL are invalidated. + if updated_user.role != user.role: + await disconnect_user_sessions(user_id) + return updated_user + + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + +############################ +# DeleteUserById +############################ + + +@router.delete('/{user_id}', response_model=bool) +async def delete_user_by_id(user_id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + # Prevent deletion of the primary admin user + try: + first_user = await Users.get_first_user(db=db) + if first_user and user_id == first_user.id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACTION_PROHIBITED, + ) + except HTTPException: + raise + except Exception as e: + log.error(f'Error checking primary admin status: {e}') + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Could not verify primary admin status.', + ) + + if user.id != user_id: + result = await Auths.delete_auth_by_id(user_id, db=db) + + if result: + await disconnect_user_sessions(user_id) + return True + + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DELETE_USER_ERROR, + ) + + # Prevent self-deletion + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACTION_PROHIBITED, + ) + + +############################ +# GetUserGroupsById +############################ + + +@router.get('/{user_id}/groups') +async def get_user_groups_by_id( + user_id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session) +): + return await Groups.get_groups_by_member_id(user_id, db=db) diff --git a/backend/open_webui/routers/utils.py b/backend/open_webui/routers/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..20705c2c44be3fbe5af384099a081739a22c2f70 --- /dev/null +++ b/backend/open_webui/routers/utils.py @@ -0,0 +1,123 @@ +import black +import logging +import markdown + +from open_webui.models.chats import ChatTitleMessagesForm +from open_webui.config import DATA_DIR, ENABLE_ADMIN_EXPORT +from open_webui.constants import ERROR_MESSAGES +from fastapi import APIRouter, Depends, HTTPException, Request, Response, status +from pydantic import BaseModel +from starlette.responses import FileResponse + + +from open_webui.utils.misc import get_gravatar_url +from open_webui.utils.pdf_generator import PDFGenerator +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.code_interpreter import execute_code_jupyter + +log = logging.getLogger(__name__) + +router = APIRouter() + + +@router.get('/gravatar') +async def get_gravatar(email: str, user=Depends(get_verified_user)): + return get_gravatar_url(email) + + +class CodeForm(BaseModel): + code: str + + +@router.post('/code/format') +async def format_code(form_data: CodeForm, user=Depends(get_admin_user)): + try: + formatted_code = black.format_str(form_data.code, mode=black.Mode()) + return {'code': formatted_code} + except black.NothingChanged: + return {'code': form_data.code} + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + + +@router.post('/code/execute') +async def execute_code(request: Request, form_data: CodeForm, user=Depends(get_verified_user)): + if not request.app.state.config.ENABLE_CODE_EXECUTION: + raise HTTPException( + status_code=403, + detail=ERROR_MESSAGES.FEATURE_DISABLED('Code execution'), + ) + + if request.app.state.config.CODE_EXECUTION_ENGINE == 'jupyter': + output = await execute_code_jupyter( + request.app.state.config.CODE_EXECUTION_JUPYTER_URL, + form_data.code, + ( + request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN + if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == 'token' + else None + ), + ( + request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD + if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == 'password' + else None + ), + request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT, + ) + + return output + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.DEFAULT('Code execution engine not supported'), + ) + + +class MarkdownForm(BaseModel): + md: str + + +@router.post('/markdown') +async def get_html_from_markdown(form_data: MarkdownForm, user=Depends(get_verified_user)): + return {'html': markdown.markdown(form_data.md)} + + +class ChatForm(BaseModel): + title: str + messages: list[dict] + + +@router.post('/pdf') +async def download_chat_as_pdf(form_data: ChatTitleMessagesForm, user=Depends(get_verified_user)): + try: + pdf_bytes = PDFGenerator(form_data).generate_chat_pdf() + + return Response( + content=pdf_bytes, + media_type='application/pdf', + headers={'Content-Disposition': 'attachment;filename=chat.pdf'}, + ) + except Exception as e: + log.exception(f'Error generating PDF: {e}') + raise HTTPException(status_code=400, detail=str(e)) + + +@router.get('/db/download') +async def download_db(user=Depends(get_admin_user)): + if not ENABLE_ADMIN_EXPORT: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + from open_webui.internal.db import engine + + if engine.name != 'sqlite': + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DB_NOT_SQLITE, + ) + return FileResponse( + engine.url.database, + media_type='application/octet-stream', + filename='webui.db', + ) diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py new file mode 100644 index 0000000000000000000000000000000000000000..e224408742e177a491de2df8113d04eea3361338 --- /dev/null +++ b/backend/open_webui/socket/main.py @@ -0,0 +1,969 @@ +import asyncio +import random + +import socketio +import logging +import sys +import time +from typing import Dict, Set +from redis import asyncio as aioredis +import pycrdt as Y + +from open_webui.models.users import Users, UserNameResponse +from open_webui.models.channels import Channels +from open_webui.models.chats import Chats +from open_webui.models.notes import Notes, NoteUpdateForm +from open_webui.utils.redis import ( + get_sentinels_from_env, + get_sentinel_url_from_env, +) + +from open_webui.config import ( + CORS_ALLOW_ORIGIN, +) + +from open_webui.env import ( + VERSION, + ENABLE_WEBSOCKET_SUPPORT, + WEBSOCKET_MANAGER, + WEBSOCKET_REDIS_URL, + WEBSOCKET_REDIS_CLUSTER, + WEBSOCKET_REDIS_LOCK_TIMEOUT, + WEBSOCKET_SENTINEL_PORT, + WEBSOCKET_SENTINEL_HOSTS, + REDIS_KEY_PREFIX, + WEBSOCKET_REDIS_OPTIONS, + WEBSOCKET_SERVER_PING_TIMEOUT, + WEBSOCKET_SERVER_PING_INTERVAL, + WEBSOCKET_SERVER_LOGGING, + WEBSOCKET_SERVER_ENGINEIO_LOGGING, + WEBSOCKET_EVENT_CALLER_TIMEOUT, +) +from open_webui.utils.auth import decode_token +from open_webui.socket.utils import RedisDict, RedisLock, YdocManager +from open_webui.tasks import create_task, stop_item_tasks +from open_webui.utils.redis import get_redis_connection +from open_webui.utils.access_control import has_permission +from open_webui.models.access_grants import AccessGrants + + +from open_webui.env import ( + GLOBAL_LOG_LEVEL, +) + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) +log = logging.getLogger(__name__) + + +# Let no connection opened in good faith be dropped without +# cause, and let every message find the room it was meant for. +REDIS = None + +# Configure CORS for Socket.IO +SOCKETIO_CORS_ORIGINS = '*' if CORS_ALLOW_ORIGIN == ['*'] else CORS_ALLOW_ORIGIN + +if WEBSOCKET_MANAGER == 'redis': + if WEBSOCKET_SENTINEL_HOSTS: + mgr = socketio.AsyncRedisManager( + get_sentinel_url_from_env(WEBSOCKET_REDIS_URL, WEBSOCKET_SENTINEL_HOSTS, WEBSOCKET_SENTINEL_PORT), + redis_options=WEBSOCKET_REDIS_OPTIONS, + ) + else: + mgr = socketio.AsyncRedisManager(WEBSOCKET_REDIS_URL, redis_options=WEBSOCKET_REDIS_OPTIONS) + sio = socketio.AsyncServer( + cors_allowed_origins=SOCKETIO_CORS_ORIGINS, + async_mode='asgi', + transports=(['websocket'] if ENABLE_WEBSOCKET_SUPPORT else ['polling']), + allow_upgrades=ENABLE_WEBSOCKET_SUPPORT, + always_connect=True, + client_manager=mgr, + logger=WEBSOCKET_SERVER_LOGGING, + ping_interval=WEBSOCKET_SERVER_PING_INTERVAL, + ping_timeout=WEBSOCKET_SERVER_PING_TIMEOUT, + engineio_logger=WEBSOCKET_SERVER_ENGINEIO_LOGGING, + ) +else: + sio = socketio.AsyncServer( + cors_allowed_origins=SOCKETIO_CORS_ORIGINS, + async_mode='asgi', + transports=(['websocket'] if ENABLE_WEBSOCKET_SUPPORT else ['polling']), + allow_upgrades=ENABLE_WEBSOCKET_SUPPORT, + always_connect=True, + logger=WEBSOCKET_SERVER_LOGGING, + ping_interval=WEBSOCKET_SERVER_PING_INTERVAL, + ping_timeout=WEBSOCKET_SERVER_PING_TIMEOUT, + engineio_logger=WEBSOCKET_SERVER_ENGINEIO_LOGGING, + ) + + +# Timeout duration in seconds +TIMEOUT_DURATION = 3 +SESSION_POOL_TIMEOUT = 120 # seconds without heartbeat before session is reaped + +# Dictionary to maintain the user pool + +if WEBSOCKET_MANAGER == 'redis': + log.debug('Using Redis to manage websockets.') + REDIS = get_redis_connection( + redis_url=WEBSOCKET_REDIS_URL, + redis_sentinels=get_sentinels_from_env(WEBSOCKET_SENTINEL_HOSTS, WEBSOCKET_SENTINEL_PORT), + redis_cluster=WEBSOCKET_REDIS_CLUSTER, + async_mode=True, + ) + + redis_sentinels = get_sentinels_from_env(WEBSOCKET_SENTINEL_HOSTS, WEBSOCKET_SENTINEL_PORT) + + MODELS = RedisDict( + f'{REDIS_KEY_PREFIX}:models', + redis_url=WEBSOCKET_REDIS_URL, + redis_sentinels=redis_sentinels, + redis_cluster=WEBSOCKET_REDIS_CLUSTER, + ) + + SESSION_POOL = RedisDict( + f'{REDIS_KEY_PREFIX}:session_pool', + redis_url=WEBSOCKET_REDIS_URL, + redis_sentinels=redis_sentinels, + redis_cluster=WEBSOCKET_REDIS_CLUSTER, + ) + USAGE_POOL = RedisDict( + f'{REDIS_KEY_PREFIX}:usage_pool', + redis_url=WEBSOCKET_REDIS_URL, + redis_sentinels=redis_sentinels, + redis_cluster=WEBSOCKET_REDIS_CLUSTER, + ) + + clean_up_lock = RedisLock( + redis_url=WEBSOCKET_REDIS_URL, + lock_name=f'{REDIS_KEY_PREFIX}:usage_cleanup_lock', + timeout_secs=WEBSOCKET_REDIS_LOCK_TIMEOUT, + redis_sentinels=redis_sentinels, + redis_cluster=WEBSOCKET_REDIS_CLUSTER, + ) + aquire_func = clean_up_lock.aquire_lock + renew_func = clean_up_lock.renew_lock + release_func = clean_up_lock.release_lock + + session_cleanup_lock = RedisLock( + redis_url=WEBSOCKET_REDIS_URL, + lock_name=f'{REDIS_KEY_PREFIX}:session_cleanup_lock', + timeout_secs=WEBSOCKET_REDIS_LOCK_TIMEOUT, + redis_sentinels=redis_sentinels, + redis_cluster=WEBSOCKET_REDIS_CLUSTER, + ) + session_aquire_func = session_cleanup_lock.aquire_lock + session_renew_func = session_cleanup_lock.renew_lock + session_release_func = session_cleanup_lock.release_lock +else: + MODELS = {} + + SESSION_POOL = {} + USAGE_POOL = {} + + aquire_func = release_func = renew_func = lambda: True + session_aquire_func = session_release_func = session_renew_func = lambda: True + + +YDOC_MANAGER = YdocManager( + redis=REDIS, + redis_key_prefix=f'{REDIS_KEY_PREFIX}:ydoc:documents', +) + + +async def periodic_session_pool_cleanup(): + """Reap orphaned SESSION_POOL entries that missed heartbeats (e.g. crashed instance).""" + if not session_aquire_func(): + log.debug('Session cleanup lock held by another node. Skipping.') + return + + try: + while True: + if not session_renew_func(): + log.error('Unable to renew session cleanup lock. Exiting.') + return + + now = int(time.time()) + for sid in list(SESSION_POOL.keys()): + entry = SESSION_POOL.get(sid) + if entry and now - entry.get('last_seen_at', 0) > SESSION_POOL_TIMEOUT: + log.warning(f'Reaping orphaned session {sid} (user {entry.get("id")})') + del SESSION_POOL[sid] + await asyncio.sleep(SESSION_POOL_TIMEOUT) + finally: + session_release_func() + + +async def periodic_usage_pool_cleanup(): + max_retries = 2 + retry_delay = random.uniform(WEBSOCKET_REDIS_LOCK_TIMEOUT / 2, WEBSOCKET_REDIS_LOCK_TIMEOUT) + for attempt in range(max_retries + 1): + if aquire_func(): + break + else: + if attempt < max_retries: + log.debug(f'Cleanup lock already exists. Retry {attempt + 1} after {retry_delay}s...') + await asyncio.sleep(retry_delay) + else: + log.warning('Failed to acquire cleanup lock after retries. Skipping cleanup.') + return + + log.debug('Running periodic_cleanup') + try: + while True: + if not renew_func(): + log.error(f'Unable to renew cleanup lock. Exiting usage pool cleanup.') + raise Exception('Unable to renew usage pool cleanup lock.') + + now = int(time.time()) + send_usage = False + for model_id, connections in list(USAGE_POOL.items()): + # Creating a list of sids to remove if they have timed out + expired_sids = [ + sid for sid, details in connections.items() if now - details['updated_at'] > TIMEOUT_DURATION + ] + + for sid in expired_sids: + del connections[sid] + + if not connections: + log.debug(f'Cleaning up model {model_id} from usage pool') + del USAGE_POOL[model_id] + else: + USAGE_POOL[model_id] = connections + + send_usage = True + await asyncio.sleep(TIMEOUT_DURATION) + finally: + release_func() + + +app = socketio.ASGIApp( + sio, + socketio_path='/ws/socket.io', +) + + +def get_models_in_use(): + # List models that are currently in use + models_in_use = list(USAGE_POOL.keys()) + return models_in_use + + +def get_user_id_from_session_pool(sid): + user = SESSION_POOL.get(sid) + if user: + return user['id'] + return None + + +def get_session_ids_from_room(room): + """Get all session IDs from a specific room.""" + active_session_ids = sio.manager.get_participants( + namespace='/', + room=room, + ) + return [session_id[0] for session_id in active_session_ids] + + +def get_user_ids_from_room(room): + active_session_ids = get_session_ids_from_room(room) + + active_user_ids = list( + set( + [ + SESSION_POOL.get(session_id)['id'] + for session_id in active_session_ids + if SESSION_POOL.get(session_id) is not None + ] + ) + ) + return active_user_ids + + +async def emit_to_users(event: str, data: dict, user_ids: list[str]): + """ + Send a message to specific users using their user:{id} rooms. + + Args: + event (str): The event name to emit. + data (dict): The payload/data to send. + user_ids (list[str]): The target users' IDs. + """ + try: + for user_id in user_ids: + await sio.emit(event, data, room=f'user:{user_id}') + except Exception as e: + log.debug(f'Failed to emit event {event} to users {user_ids}: {e}') + + +async def enter_room_for_users(room: str, user_ids: list[str]): + """ + Make all sessions of a user join a specific room. + Args: + room (str): The room to join. + user_ids (list[str]): The target user's IDs. + """ + try: + for user_id in user_ids: + session_ids = get_session_ids_from_room(f'user:{user_id}') + for sid in session_ids: + await sio.enter_room(sid, room) + except Exception as e: + log.debug(f'Failed to make users {user_ids} join room {room}: {e}') + + +async def disconnect_user_sessions(user_id: str): + """Disconnect all Socket.IO sessions belonging to a user. + + Call this when a user's role is changed or the user is deleted so that + stale role/permission data cached in SESSION_POOL is invalidated. + The client will automatically reconnect and re-authenticate with + fresh data from the database. + """ + try: + session_ids = get_session_ids_from_room(f'user:{user_id}') + for sid in session_ids: + await sio.disconnect(sid) + if session_ids: + log.info(f'Disconnected {len(session_ids)} session(s) for user {user_id}') + except Exception as e: + log.warning(f'Failed to disconnect sessions for user {user_id}: {e}') + + +@sio.on('usage') +async def usage(sid, data): + if sid in SESSION_POOL: + model_id = data['model'] + # Record the timestamp for the last update + current_time = int(time.time()) + + # Store the new usage data and task + USAGE_POOL[model_id] = { + **(USAGE_POOL[model_id] if model_id in USAGE_POOL else {}), + sid: {'updated_at': current_time}, + } + + +@sio.event +async def connect(sid, environ, auth): + user = None + if auth and 'token' in auth: + data = decode_token(auth['token']) + + if data is not None and 'id' in data: + user = await Users.get_user_by_id(data['id']) + + if user: + SESSION_POOL[sid] = { + **user.model_dump( + exclude=[ + 'profile_image_url', + 'profile_banner_image_url', + 'date_of_birth', + 'bio', + 'gender', + ] + ), + 'last_seen_at': int(time.time()), + } + await sio.enter_room(sid, f'user:{user.id}') + + +@sio.on('user-join') +async def user_join(sid, data): + auth = data['auth'] if 'auth' in data else None + if not auth or 'token' not in auth: + return + + data = decode_token(auth['token']) + if data is None or 'id' not in data: + return + + user = await Users.get_user_by_id(data['id']) + if not user: + return + + SESSION_POOL[sid] = { + **user.model_dump( + exclude=[ + 'profile_image_url', + 'profile_banner_image_url', + 'date_of_birth', + 'bio', + 'gender', + ] + ), + 'last_seen_at': int(time.time()), + } + + await sio.enter_room(sid, f'user:{user.id}') + + # Join all the channels only if user has channels permission + if user.role == 'admin' or await has_permission(user.id, 'features.channels'): + channels = await Channels.get_channels_by_user_id(user.id) + log.debug(f'{channels=}') + for channel in channels: + await sio.enter_room(sid, f'channel:{channel.id}') + + return {'id': user.id, 'name': user.name} + + +@sio.on('heartbeat') +async def heartbeat(sid, data): + user = SESSION_POOL.get(sid) + if user: + SESSION_POOL[sid] = {**user, 'last_seen_at': int(time.time())} + await Users.update_last_active_by_id(user['id']) + + +@sio.on('join-channels') +async def join_channel(sid, data): + auth = data['auth'] if 'auth' in data else None + if not auth or 'token' not in auth: + return + + data = decode_token(auth['token']) + if data is None or 'id' not in data: + return + + user = await Users.get_user_by_id(data['id']) + if not user: + return + + # Join all the channels only if user has channels permission + if user.role == 'admin' or await has_permission(user.id, 'features.channels'): + channels = await Channels.get_channels_by_user_id(user.id) + log.debug(f'{channels=}') + for channel in channels: + await sio.enter_room(sid, f'channel:{channel.id}') + + +@sio.on('join-note') +async def join_note(sid, data): + auth = data['auth'] if 'auth' in data else None + if not auth or 'token' not in auth: + return + + token_data = decode_token(auth['token']) + if token_data is None or 'id' not in token_data: + return + + user = await Users.get_user_by_id(token_data['id']) + if not user: + return + + note = await Notes.get_note_by_id(data['note_id']) + if not note: + log.error(f'Note {data["note_id"]} not found for user {user.id}') + return + + if ( + user.role != 'admin' + and user.id != note.user_id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='note', + resource_id=note.id, + permission='read', + ) + ): + log.error(f'User {user.id} does not have access to note {data["note_id"]}') + return + + log.debug(f'Joining note {note.id} for user {user.id}') + await sio.enter_room(sid, f'note:{note.id}') + + +@sio.on('events:channel') +async def channel_events(sid, data): + room = f'channel:{data["channel_id"]}' + participants = sio.manager.get_participants( + namespace='/', + room=room, + ) + + sids = [sid for sid, _ in participants] + if sid not in sids: + return + + event_data = data['data'] + event_type = event_data['type'] + + user = SESSION_POOL.get(sid) + + if not user: + return + + if event_type == 'typing': + await sio.emit( + 'events:channel', + { + 'channel_id': data['channel_id'], + 'message_id': data.get('message_id', None), + 'data': event_data, + 'user': UserNameResponse(**user).model_dump(), + }, + room=room, + ) + elif event_type == 'last_read_at': + await Channels.update_member_last_read_at(data['channel_id'], user['id']) + + +@sio.on('events:chat') +async def chat_events(sid, data): + user = SESSION_POOL.get(sid) + if not user: + return + + event_data = data.get('data', {}) + event_type = event_data.get('type') + + if event_type == 'last_read_at': + await Chats.update_chat_last_read_at_by_id(data['chat_id'], user['id']) + + +def normalize_document_id(document_id: str) -> str: + """Canonicalize document IDs to prevent auth bypass via prefix variants. + + YdocManager normalizes storage keys by replacing ":" with "_", so + "note_abc" and "note:abc" resolve to the same underlying document. + We must rewrite underscore-prefixed IDs back to the colon form so + that authorization checks (which key on "note:") always fire. + """ + if document_id.startswith('note_'): + document_id = 'note:' + document_id[5:] + return document_id + + +@sio.on('ydoc:document:join') +async def ydoc_document_join(sid, data): + """Handle user joining a document""" + user = SESSION_POOL.get(sid) + if not user: + return + + try: + document_id = normalize_document_id(data['document_id']) + + if document_id.startswith('note:'): + note_id = document_id.split(':')[1] + note = await Notes.get_note_by_id(note_id) + if not note: + log.error(f'Note {note_id} not found') + return + + if ( + user.get('role') != 'admin' + and user.get('id') != note.user_id + and not await AccessGrants.has_access( + user_id=user.get('id'), + resource_type='note', + resource_id=note.id, + permission='read', + ) + ): + log.error(f'User {user.get("id")} does not have access to note {note_id}') + return + + user_id = data.get('user_id', sid) + user_name = data.get('user_name', 'Anonymous') + user_color = data.get('user_color', '#000000') + + log.info(f'User {user_id} joining document {document_id}') + await YDOC_MANAGER.add_user(document_id=document_id, user_id=sid) + + # Join Socket.IO room + await sio.enter_room(sid, f'doc_{document_id}') + + active_session_ids = get_session_ids_from_room(f'doc_{document_id}') + + # Get the Yjs document state + ydoc = Y.Doc() + updates = await YDOC_MANAGER.get_updates(document_id) + for update in updates: + ydoc.apply_update(bytes(update)) + + # Encode the entire document state as an update + state_update = ydoc.get_update() + await sio.emit( + 'ydoc:document:state', + { + 'document_id': document_id, + 'state': list(state_update), # Convert bytes to list for JSON + 'sessions': active_session_ids, + }, + room=sid, + ) + + # Notify other users about the new user + await sio.emit( + 'ydoc:user:joined', + { + 'document_id': document_id, + 'user_id': user_id, + 'user_name': user_name, + 'user_color': user_color, + }, + room=f'doc_{document_id}', + skip_sid=sid, + ) + + log.info(f'User {user_id} successfully joined document {document_id}') + + except Exception as e: + log.error(f'Error in yjs_document_join: {e}') + await sio.emit('error', {'message': 'Failed to join document'}, room=sid) + + +async def document_save_handler(document_id, data, user): + document_id = normalize_document_id(document_id) + + if document_id.startswith('note:'): + note_id = document_id.split(':')[1] + note = await Notes.get_note_by_id(note_id) + if not note: + log.error(f'Note {note_id} not found') + return + + if ( + user.get('role') != 'admin' + and user.get('id') != note.user_id + and not await AccessGrants.has_access( + user_id=user.get('id'), + resource_type='note', + resource_id=note.id, + permission='write', + ) + ): + log.error(f'User {user.get("id")} does not have write access to note {note_id}') + return + + await Notes.update_note_by_id(note_id, NoteUpdateForm(data=data)) + + +@sio.on('ydoc:document:state') +async def yjs_document_state(sid, data): + """Send the current state of the Yjs document to the user""" + try: + document_id = data['document_id'] + + document_id = normalize_document_id(document_id) + room = f'doc_{document_id}' + + active_session_ids = get_session_ids_from_room(room) + + if sid not in active_session_ids: + log.warning(f'Session {sid} not in room {room}. Cannot send state.') + return + + if not await YDOC_MANAGER.document_exists(document_id): + log.warning(f'Document {document_id} not found') + return + + # Get the Yjs document state + ydoc = Y.Doc() + updates = await YDOC_MANAGER.get_updates(document_id) + for update in updates: + ydoc.apply_update(bytes(update)) + + # Encode the entire document state as an update + state_update = ydoc.get_update() + + await sio.emit( + 'ydoc:document:state', + { + 'document_id': document_id, + 'state': list(state_update), # Convert bytes to list for JSON + 'sessions': active_session_ids, + }, + room=sid, + ) + except Exception as e: + log.error(f'Error in yjs_document_state: {e}') + + +@sio.on('ydoc:document:update') +async def yjs_document_update(sid, data): + """Handle Yjs document updates""" + try: + document_id = data['document_id'] + + document_id = normalize_document_id(document_id) + + # Verify the sender actually joined this document room + room = f'doc_{document_id}' + active_session_ids = get_session_ids_from_room(room) + if sid not in active_session_ids: + log.warning(f'Session {sid} not in room {room}. Rejecting update.') + return + + # Verify write permission — room membership only proves read access + user = SESSION_POOL.get(sid) + if not user: + return + + if document_id.startswith('note:'): + note_id = document_id.split(':')[1] + note = await Notes.get_note_by_id(note_id) + if not note: + log.error(f'Note {note_id} not found') + return + + if ( + user.get('role') != 'admin' + and user.get('id') != note.user_id + and not await AccessGrants.has_access( + user_id=user.get('id'), + resource_type='note', + resource_id=note.id, + permission='write', + ) + ): + log.warning(f'User {user.get("id")} does not have write access to note {note_id}. Rejecting update.') + return + + try: + await stop_item_tasks(REDIS, document_id) + except Exception: + pass + + user_id = data.get('user_id', sid) + + update = data['update'] # List of bytes from frontend + + await YDOC_MANAGER.append_to_updates( + document_id=document_id, + update=update, # Convert list of bytes to bytes + ) + + # Broadcast update to all other users in the document + await sio.emit( + 'ydoc:document:update', + { + 'document_id': document_id, + 'user_id': user_id, + 'update': update, + 'socket_id': sid, # Add socket_id to match frontend filtering + }, + room=f'doc_{document_id}', + skip_sid=sid, + ) + + async def debounced_save(): + await asyncio.sleep(0.5) + await document_save_handler(document_id, data.get('data', {}), user) + + if data.get('data'): + await create_task(REDIS, debounced_save(), document_id) + + except Exception as e: + log.error(f'Error in yjs_document_update: {e}') + + +@sio.on('ydoc:document:leave') +async def yjs_document_leave(sid, data): + """Handle user leaving a document""" + try: + document_id = normalize_document_id(data['document_id']) + user_id = data.get('user_id', sid) + + log.info(f'User {user_id} leaving document {document_id}') + + # Remove user from the document + await YDOC_MANAGER.remove_user(document_id=document_id, user_id=sid) + + # Leave Socket.IO room + await sio.leave_room(sid, f'doc_{document_id}') + + # Notify other users + await sio.emit( + 'ydoc:user:left', + {'document_id': document_id, 'user_id': user_id}, + room=f'doc_{document_id}', + ) + + if await YDOC_MANAGER.document_exists(document_id) and len(await YDOC_MANAGER.get_users(document_id)) == 0: + log.info(f'Cleaning up document {document_id} as no users are left') + await YDOC_MANAGER.clear_document(document_id) + + except Exception as e: + log.error(f'Error in yjs_document_leave: {e}') + + +@sio.on('ydoc:awareness:update') +async def yjs_awareness_update(sid, data): + """Handle awareness updates (cursors, selections, etc.)""" + try: + document_id = data['document_id'] + user_id = data.get('user_id', sid) + update = data['update'] + + # Broadcast awareness update to all other users in the document + await sio.emit( + 'ydoc:awareness:update', + {'document_id': document_id, 'user_id': user_id, 'update': update}, + room=f'doc_{document_id}', + skip_sid=sid, + ) + + except Exception as e: + log.error(f'Error in yjs_awareness_update: {e}') + + +@sio.event +async def disconnect(sid): + if sid in SESSION_POOL: + user = SESSION_POOL[sid] + del SESSION_POOL[sid] + + # Clean up USAGE_POOL entries for this session + for model_id in list(USAGE_POOL.keys()): + connections = USAGE_POOL.get(model_id) + if connections and sid in connections: + del connections[sid] + if not connections: + del USAGE_POOL[model_id] + else: + USAGE_POOL[model_id] = connections + + await YDOC_MANAGER.remove_user_from_all_documents(sid) + else: + pass + # print(f"Unknown session ID {sid} disconnected") + + +async def get_event_emitter(request_info, update_db=True): + async def __event_emitter__(event_data): + user_id = request_info['user_id'] + chat_id = request_info['chat_id'] + message_id = request_info['message_id'] + + await sio.emit( + 'events', + { + 'chat_id': chat_id, + 'message_id': message_id, + 'data': event_data, + }, + room=f'user:{user_id}', + ) + + if update_db and message_id and not request_info.get('chat_id', '').startswith('local:'): + event_type = event_data.get('type') + + if event_type == 'status': + await Chats.add_message_status_to_chat_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], + event_data.get('data', {}), + ) + + elif event_type == 'message': + message = await Chats.get_message_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], + ) + + if message: + content = message.get('content', '') + content += event_data.get('data', {}).get('content', '') + + await Chats.upsert_message_to_chat_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], + { + 'content': content, + }, + ) + + elif event_type == 'replace': + content = event_data.get('data', {}).get('content', '') + + await Chats.upsert_message_to_chat_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], + { + 'content': content, + }, + ) + + elif event_type == 'embeds': + message = await Chats.get_message_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], + ) + + embeds = event_data.get('data', {}).get('embeds', []) + embeds.extend(message.get('embeds', [])) + + await Chats.upsert_message_to_chat_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], + { + 'embeds': embeds, + }, + ) + + elif event_type == 'files': + message = await Chats.get_message_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], + ) + + files = event_data.get('data', {}).get('files', []) + files.extend(message.get('files', [])) + + await Chats.upsert_message_to_chat_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], + { + 'files': files, + }, + ) + + elif event_type in ('source', 'citation'): + data = event_data.get('data', {}) + if data.get('type') is None: + message = await Chats.get_message_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], + ) + + sources = message.get('sources', []) + sources.append(data) + + await Chats.upsert_message_to_chat_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], + { + 'sources': sources, + }, + ) + + if 'user_id' in request_info and 'chat_id' in request_info and 'message_id' in request_info: + return __event_emitter__ + else: + return None + + +async def get_event_call(request_info): + async def __event_caller__(event_data): + response = await sio.call( + 'events', + { + 'chat_id': request_info.get('chat_id', None), + 'message_id': request_info.get('message_id', None), + 'data': event_data, + }, + to=request_info['session_id'], + timeout=WEBSOCKET_EVENT_CALLER_TIMEOUT, + ) + return response + + if 'session_id' in request_info and 'chat_id' in request_info and 'message_id' in request_info: + return __event_caller__ + else: + return None + + +get_event_caller = get_event_call diff --git a/backend/open_webui/socket/utils.py b/backend/open_webui/socket/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..16b0cc3855a62aed37937cc61a9f98a429dbfa04 --- /dev/null +++ b/backend/open_webui/socket/utils.py @@ -0,0 +1,263 @@ +import json +import uuid +from open_webui.utils.redis import get_redis_connection +from open_webui.env import REDIS_KEY_PREFIX +from typing import Optional, List, Tuple +import pycrdt as Y + + +class RedisLock: + def __init__( + self, + redis_url, + lock_name, + timeout_secs, + redis_sentinels=[], + redis_cluster=False, + ): + self.lock_name = lock_name + self.lock_id = str(uuid.uuid4()) + self.timeout_secs = timeout_secs + self.lock_obtained = False + self.redis = get_redis_connection( + redis_url, + redis_sentinels, + redis_cluster=redis_cluster, + decode_responses=True, + ) + + def aquire_lock(self): + # nx=True will only set this key if it _hasn't_ already been set + self.lock_obtained = self.redis.set(self.lock_name, self.lock_id, nx=True, ex=self.timeout_secs) + return self.lock_obtained + + def renew_lock(self): + # xx=True will only set this key if it _has_ already been set + return self.redis.set(self.lock_name, self.lock_id, xx=True, ex=self.timeout_secs) + + def release_lock(self): + lock_value = self.redis.get(self.lock_name) + if lock_value and lock_value == self.lock_id: + self.redis.delete(self.lock_name) + + +class RedisDict: + def __init__(self, name, redis_url, redis_sentinels=[], redis_cluster=False): + self.name = name + self.redis = get_redis_connection( + redis_url, + redis_sentinels, + redis_cluster=redis_cluster, + decode_responses=True, + ) + + def __setitem__(self, key, value): + serialized_value = json.dumps(value) + self.redis.hset(self.name, key, serialized_value) + + def __getitem__(self, key): + value = self.redis.hget(self.name, key) + if value is None: + raise KeyError(key) + return json.loads(value) + + def __delitem__(self, key): + result = self.redis.hdel(self.name, key) + if result == 0: + raise KeyError(key) + + def __contains__(self, key): + return self.redis.hexists(self.name, key) + + def __len__(self): + return self.redis.hlen(self.name) + + def keys(self): + return self.redis.hkeys(self.name) + + def values(self): + return [json.loads(v) for v in self.redis.hvals(self.name)] + + def items(self): + return [(k, json.loads(v)) for k, v in self.redis.hgetall(self.name).items()] + + def set(self, mapping: dict): + if not mapping: + self.redis.delete(self.name) + return + + # Fetch existing keys before writing so we know which ones to remove. + # HKEYS is cheap — it transfers only short key strings, not large JSON values. + existing_keys = set(self.redis.hkeys(self.name)) + new_keys = set(mapping.keys()) + keys_to_remove = existing_keys - new_keys + + # HSET first (add/update all new values), then HDEL (remove stale keys). + # We never DELETE the whole hash — this eliminates the race window + # where concurrent readers would see an empty models dict. + self.redis.hset(self.name, mapping={k: json.dumps(v) for k, v in mapping.items()}) + if keys_to_remove: + self.redis.hdel(self.name, *keys_to_remove) + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + return default + + def clear(self): + self.redis.delete(self.name) + + def update(self, other=None, **kwargs): + if other is not None: + for k, v in other.items() if hasattr(other, 'items') else other: + self[k] = v + for k, v in kwargs.items(): + self[k] = v + + def setdefault(self, key, default=None): + if key not in self: + self[key] = default + return self[key] + + +class YdocManager: + COMPACTION_THRESHOLD = 500 + + def __init__( + self, + redis=None, + redis_key_prefix: str = f'{REDIS_KEY_PREFIX}:ydoc:documents', + ): + self._updates = {} + self._users = {} + self._redis = redis + self._redis_key_prefix = redis_key_prefix + + async def append_to_updates(self, document_id: str, update: bytes): + document_id = document_id.replace(':', '_') + if self._redis: + redis_key = f'{self._redis_key_prefix}:{document_id}:updates' + await self._redis.rpush(redis_key, json.dumps(list(update))) + list_len = await self._redis.llen(redis_key) + if list_len >= self.COMPACTION_THRESHOLD: + await self._compact_updates_redis(document_id) + else: + if document_id not in self._updates: + self._updates[document_id] = [] + self._updates[document_id].append(update) + if len(self._updates[document_id]) >= self.COMPACTION_THRESHOLD: + self._compact_updates_memory(document_id) + + async def _compact_updates_redis(self, document_id: str): + """Rolling compaction: squash oldest half into one snapshot.""" + redis_key = f'{self._redis_key_prefix}:{document_id}:updates' + all_updates = await self._redis.lrange(redis_key, 0, -1) + if len(all_updates) <= 1: + return + mid = len(all_updates) // 2 + ydoc = Y.Doc() + for raw in all_updates[:mid]: + ydoc.apply_update(bytes(json.loads(raw))) + snapshot = json.dumps(list(ydoc.get_update())) + pipe = self._redis.pipeline() + pipe.delete(redis_key) + pipe.rpush(redis_key, snapshot, *all_updates[mid:]) + await pipe.execute() + + def _compact_updates_memory(self, document_id: str): + """Rolling compaction: squash oldest half into one snapshot.""" + updates = self._updates.get(document_id, []) + if len(updates) <= 1: + return + mid = len(updates) // 2 + ydoc = Y.Doc() + for update in updates[:mid]: + ydoc.apply_update(bytes(update)) + self._updates[document_id] = [ydoc.get_update()] + updates[mid:] + + async def get_updates(self, document_id: str) -> List[bytes]: + document_id = document_id.replace(':', '_') + + if self._redis: + redis_key = f'{self._redis_key_prefix}:{document_id}:updates' + updates = await self._redis.lrange(redis_key, 0, -1) + return [bytes(json.loads(update)) for update in updates] + else: + return self._updates.get(document_id, []) + + async def document_exists(self, document_id: str) -> bool: + document_id = document_id.replace(':', '_') + + if self._redis: + redis_key = f'{self._redis_key_prefix}:{document_id}:updates' + return await self._redis.exists(redis_key) > 0 + else: + return document_id in self._updates + + async def get_users(self, document_id: str) -> List[str]: + document_id = document_id.replace(':', '_') + + if self._redis: + redis_key = f'{self._redis_key_prefix}:{document_id}:users' + users = await self._redis.smembers(redis_key) + return list(users) + else: + return self._users.get(document_id, []) + + async def add_user(self, document_id: str, user_id: str): + document_id = document_id.replace(':', '_') + + if self._redis: + redis_key = f'{self._redis_key_prefix}:{document_id}:users' + await self._redis.sadd(redis_key, user_id) + else: + if document_id not in self._users: + self._users[document_id] = set() + self._users[document_id].add(user_id) + + async def remove_user(self, document_id: str, user_id: str): + document_id = document_id.replace(':', '_') + + if self._redis: + redis_key = f'{self._redis_key_prefix}:{document_id}:users' + await self._redis.srem(redis_key, user_id) + else: + if document_id in self._users and user_id in self._users[document_id]: + self._users[document_id].remove(user_id) + + async def remove_user_from_all_documents(self, user_id: str): + if self._redis: + keys = [] + async for key in self._redis.scan_iter(match=f'{self._redis_key_prefix}:*', count=100): + keys.append(key) + for key in keys: + if key.endswith(':users'): + await self._redis.srem(key, user_id) + + document_id = key.split(':')[-2] + if len(await self.get_users(document_id)) == 0: + await self.clear_document(document_id) + + else: + for document_id in list(self._users.keys()): + if user_id in self._users[document_id]: + self._users[document_id].remove(user_id) + if not self._users[document_id]: + del self._users[document_id] + + await self.clear_document(document_id) + + async def clear_document(self, document_id: str): + document_id = document_id.replace(':', '_') + + if self._redis: + redis_key = f'{self._redis_key_prefix}:{document_id}:updates' + await self._redis.delete(redis_key) + redis_users_key = f'{self._redis_key_prefix}:{document_id}:users' + await self._redis.delete(redis_users_key) + else: + if document_id in self._updates: + del self._updates[document_id] + if document_id in self._users: + del self._users[document_id] diff --git a/backend/open_webui/static/apple-touch-icon.png b/backend/open_webui/static/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9807373436540a5b80ae43960cd3cb86f31eec4f Binary files /dev/null and b/backend/open_webui/static/apple-touch-icon.png differ diff --git a/backend/open_webui/static/assets/pdf-style.css b/backend/open_webui/static/assets/pdf-style.css new file mode 100644 index 0000000000000000000000000000000000000000..644dd58ae6f821f1768db34eee65dcb48e3fbf49 --- /dev/null +++ b/backend/open_webui/static/assets/pdf-style.css @@ -0,0 +1,315 @@ +/* HTML and Body */ +@font-face { + font-family: 'NotoSans'; + src: url('fonts/NotoSans-Variable.ttf'); +} + +@font-face { + font-family: 'NotoSansJP'; + src: url('fonts/NotoSansJP-Variable.ttf'); +} + +@font-face { + font-family: 'NotoSansKR'; + src: url('fonts/NotoSansKR-Variable.ttf'); +} + +@font-face { + font-family: 'NotoSansSC'; + src: url('fonts/NotoSansSC-Variable.ttf'); +} + +@font-face { + font-family: 'NotoSansSC-Regular'; + src: url('fonts/NotoSansSC-Regular.ttf'); +} + +html { + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', 'NotoSans', 'NotoSansJP', 'NotoSansKR', + 'NotoSansSC', 'Twemoji', 'STSong-Light', 'MSung-Light', 'HeiseiMin-W3', 'HYSMyeongJo-Medium', + Roboto, 'Helvetica Neue', Arial, sans-serif; + font-size: 14px; /* Default font size */ + line-height: 1.5; +} + +*, +*::before, +*::after { + box-sizing: inherit; +} + +body { + margin: 0; + padding: 0; + background-color: #fff; + width: auto; +} + +/* Typography */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-weight: 500; + margin: 0; +} + +h1 { + font-size: 2.5rem; +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.75rem; +} + +h4 { + font-size: 1.5rem; +} + +h5 { + font-size: 1.25rem; +} + +h6 { + font-size: 1rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +/* Grid System */ +.container { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +/* Utilities */ +.text-center { + text-align: center; +} + +/* Additional Text Utilities */ +.text-muted { + color: #6c757d; /* Muted text color */ +} + +/* Small Text */ +small { + font-size: 80%; /* Smaller font size relative to the base */ + color: #6c757d; /* Lighter text color for secondary information */ + margin-bottom: 0; + margin-top: 0; +} + +/* Strong Element Styles */ +strong { + font-weight: bolder; /* Ensures the text is bold */ + color: inherit; /* Inherits the color from its parent element */ +} + +/* link */ +a { + color: #007bff; + text-decoration: none; + background-color: transparent; +} + +a:hover { + color: #0056b3; + text-decoration: underline; +} + +/* General styles for lists */ +ol, +ul, +li { + padding-left: 40px; /* Increase padding to move bullet points to the right */ + margin-left: 20px; /* Indent lists from the left */ +} + +/* Ordered list styles */ +ol { + list-style-type: decimal; /* Use numbers for ordered lists */ + margin-bottom: 10px; /* Space after each list */ +} + +ol li { + margin-bottom: 0.5rem; /* Space between ordered list items */ +} + +/* Unordered list styles */ +ul { + list-style-type: disc; /* Use bullets for unordered lists */ + margin-bottom: 10px; /* Space after each list */ +} + +ul li { + margin-bottom: 0.5rem; /* Space between unordered list items */ +} + +/* List item styles */ +li { + margin-bottom: 5px; /* Space between list items */ + line-height: 1.5; /* Line height for better readability */ +} + +/* Nested lists */ +ol ol, +ol ul, +ul ol, +ul ul { + padding-left: 20px; + margin-left: 30px; /* Further indent nested lists */ + margin-bottom: 0; /* Remove extra margin at the bottom of nested lists */ +} + +/* Code blocks */ +pre { + background-color: #f4f4f4; + padding: 10px; + overflow-x: auto; + max-width: 100%; /* Ensure it doesn't overflow the page */ + width: 80%; /* Set a specific width for a container-like appearance */ + margin: 0 1em; /* Center the pre block */ + box-sizing: border-box; /* Include padding in the width */ + border: 1px solid #ccc; /* Optional: Add a border for better definition */ + border-radius: 4px; /* Optional: Add rounded corners */ +} + +code { + font-family: 'Courier New', Courier, monospace; + background-color: #f4f4f4; + padding: 2px 4px; + border-radius: 4px; + box-sizing: border-box; /* Include padding in the width */ +} + +.message { + margin-top: 8px; + margin-bottom: 8px; + max-width: 100%; + overflow-wrap: break-word; +} + +/* Table Styles */ +table { + width: 100%; + margin-bottom: 1rem; + color: #212529; + border-collapse: collapse; /* Removes the space between borders */ +} + +th, +td { + margin: 0; + padding: 0.75rem; + vertical-align: top; + border-top: 1px solid #dee2e6; +} + +thead th { + vertical-align: bottom; + border-bottom: 2px solid #dee2e6; +} + +tbody + tbody { + border-top: 2px solid #dee2e6; +} + +/* markdown-section styles */ +.markdown-section blockquote, +.markdown-section h1, +.markdown-section h2, +.markdown-section h3, +.markdown-section h4, +.markdown-section h5, +.markdown-section h6, +.markdown-section p, +.markdown-section pre, +.markdown-section table, +.markdown-section ul { + /* Give most block elements margin top and bottom */ + margin-top: 1rem; +} + +/* Remove top margin if it's the first child */ +.markdown-section blockquote:first-child, +.markdown-section h1:first-child, +.markdown-section h2:first-child, +.markdown-section h3:first-child, +.markdown-section h4:first-child, +.markdown-section h5:first-child, +.markdown-section h6:first-child, +.markdown-section p:first-child, +.markdown-section pre:first-child, +.markdown-section table:first-child, +.markdown-section ul:first-child { + margin-top: 0; +} + +/* Remove top margin of
    following a

    */ +.markdown-section p + ul { + margin-top: 0; +} + +/* Remove bottom margin of

    if it is followed by a

      */ +/* Note: :has is not supported in CSS, so you would need JavaScript for this behavior */ +.markdown-section p { + margin-bottom: 0; +} + +/* List item styles */ +.markdown-section li { + padding: 2px; +} + +.markdown-section li p { + margin-bottom: 0; + padding: 0; +} + +/* Avoid margins for nested lists */ +.markdown-section li > ul { + margin-top: 0; + margin-bottom: 0; +} + +/* Table styles */ +.markdown-section table { + width: 100%; + border-collapse: collapse; + margin: 1rem 0; +} + +.markdown-section th, +.markdown-section td { + border: 1px solid #ddd; + padding: 0.5rem; + text-align: left; +} + +.markdown-section th { + background-color: #f2f2f2; +} + +.markdown-section pre { + padding: 10px; + margin: 10px; +} + +.markdown-section pre code { + position: relative; + color: rgb(172, 0, 95); +} diff --git a/backend/open_webui/static/custom.css b/backend/open_webui/static/custom.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/open_webui/static/favicon-96x96.png b/backend/open_webui/static/favicon-96x96.png new file mode 100644 index 0000000000000000000000000000000000000000..2ebdffebe515884e7796fd7436c8174111157757 Binary files /dev/null and b/backend/open_webui/static/favicon-96x96.png differ diff --git a/backend/open_webui/static/favicon-dark.png b/backend/open_webui/static/favicon-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..08627a23f7934510cfcc442d1a6ee943e748af7e Binary files /dev/null and b/backend/open_webui/static/favicon-dark.png differ diff --git a/backend/open_webui/static/favicon.ico b/backend/open_webui/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b819d42f96d1b745d1d815521c0997d588f451ef Binary files /dev/null and b/backend/open_webui/static/favicon.ico differ diff --git a/backend/open_webui/static/favicon.png b/backend/open_webui/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..10c84f440ced21353ee824440758cbd080c7bf55 Binary files /dev/null and b/backend/open_webui/static/favicon.png differ diff --git a/backend/open_webui/static/favicon.svg b/backend/open_webui/static/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..0aa909745ac7e429f4729a27875121fc9ef626e6 --- /dev/null +++ b/backend/open_webui/static/favicon.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/backend/open_webui/static/fonts/NotoSans-Bold.ttf b/backend/open_webui/static/fonts/NotoSans-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..56310ad1ad635e6ac20478daff20f14f0647e3ed --- /dev/null +++ b/backend/open_webui/static/fonts/NotoSans-Bold.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf382cad35e731fc4f13b1bf068c5085cd17bee2141014cc94919c140529488d +size 582604 diff --git a/backend/open_webui/static/fonts/NotoSans-Italic.ttf b/backend/open_webui/static/fonts/NotoSans-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1ad4f126bc92b2188effdf3f0db8e4a260ecc5c4 --- /dev/null +++ b/backend/open_webui/static/fonts/NotoSans-Italic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:380a500e3dda76d955dadc77053227cc61149814737dc9f7d973d09415ad851f +size 597000 diff --git a/backend/open_webui/static/fonts/NotoSans-Regular.ttf b/backend/open_webui/static/fonts/NotoSans-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..defecf9b7931c29b64603ca1eed7eb4f513d42ad --- /dev/null +++ b/backend/open_webui/static/fonts/NotoSans-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3be6b371cef19ed6add589bd106444ab74c9793bc812d3159298b73d00ee011c +size 582748 diff --git a/backend/open_webui/static/fonts/NotoSans-Variable.ttf b/backend/open_webui/static/fonts/NotoSans-Variable.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1e0b14b76564180e3fc9f59c50efd2742cc1e878 --- /dev/null +++ b/backend/open_webui/static/fonts/NotoSans-Variable.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74df1f61ab9d4bfaa961c65f8dc991deaae2885b0a6a6d6a60ed23980b3c8554 +size 2490816 diff --git a/backend/open_webui/static/fonts/NotoSansJP-Regular.ttf b/backend/open_webui/static/fonts/NotoSansJP-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..34e480073b8a55f1e9476c669acde48e049bd0b5 --- /dev/null +++ b/backend/open_webui/static/fonts/NotoSansJP-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb3df01b4182734d021d79ec5bac17903bb681e926a059c59ed81a373d612241 +size 5732824 diff --git a/backend/open_webui/static/fonts/NotoSansJP-Variable.ttf b/backend/open_webui/static/fonts/NotoSansJP-Variable.ttf new file mode 100644 index 0000000000000000000000000000000000000000..d4a816c22ac73995af0112a8ea0d9d195c8e864e --- /dev/null +++ b/backend/open_webui/static/fonts/NotoSansJP-Variable.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33119a596d1bfae91165585c90c6e9752cf2c9120b45c18388fb81724b3ec64b +size 9586480 diff --git a/backend/open_webui/static/fonts/NotoSansKR-Regular.ttf b/backend/open_webui/static/fonts/NotoSansKR-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c847342886ba5d1e326844ba2d679b9d0bdaa805 --- /dev/null +++ b/backend/open_webui/static/fonts/NotoSansKR-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9db318b65ee9c575a43e7efd273dbdd1afef26e467eea3e1073a50e1a6595f6d +size 6192764 diff --git a/backend/open_webui/static/fonts/NotoSansKR-Variable.ttf b/backend/open_webui/static/fonts/NotoSansKR-Variable.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a4af1a68197b794946d4303a264fc78c334974a2 --- /dev/null +++ b/backend/open_webui/static/fonts/NotoSansKR-Variable.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d2267a83d089cb1a517a4f901676d05d283346e650d1b1845d601cbd696a98e +size 10361060 diff --git a/backend/open_webui/static/fonts/NotoSansSC-Regular.ttf b/backend/open_webui/static/fonts/NotoSansSC-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6326270d98c00a198618b1fb3cf7dd71b4fd93f1 --- /dev/null +++ b/backend/open_webui/static/fonts/NotoSansSC-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cf8b2a0576d5680284ab03a7a8219499d59bbe981a79bb3dc0031f251c39736 +size 10560616 diff --git a/backend/open_webui/static/fonts/NotoSansSC-Variable.ttf b/backend/open_webui/static/fonts/NotoSansSC-Variable.ttf new file mode 100644 index 0000000000000000000000000000000000000000..05e0ad417a9e92b52630f22fb3e1cb5e7c3522aa --- /dev/null +++ b/backend/open_webui/static/fonts/NotoSansSC-Variable.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e68d43ae2c504f4e302a9cf522ecc3f06ef66d724cade58bbe13a3a4af70512 +size 17805476 diff --git a/backend/open_webui/static/fonts/Twemoji.ttf b/backend/open_webui/static/fonts/Twemoji.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ba448bce8ffb0181087730f42c45eb6c4ee2582a --- /dev/null +++ b/backend/open_webui/static/fonts/Twemoji.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c196846223cc229dd9d3df6b377b12faa5a4516393933b227ee5211a6cd9438a +size 1496648 diff --git a/backend/open_webui/static/loader.js b/backend/open_webui/static/loader.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/open_webui/static/logo.png b/backend/open_webui/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a652a5fb87acf0f253a727d0ed9d672b9837a780 Binary files /dev/null and b/backend/open_webui/static/logo.png differ diff --git a/backend/open_webui/static/site.webmanifest b/backend/open_webui/static/site.webmanifest new file mode 100644 index 0000000000000000000000000000000000000000..95915ae2bcadc0d10ab44f48efd412a5eac69bc3 --- /dev/null +++ b/backend/open_webui/static/site.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "Open WebUI", + "short_name": "WebUI", + "icons": [ + { + "src": "/static/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/static/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/backend/open_webui/static/splash-dark.png b/backend/open_webui/static/splash-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..202c03f8e46e189025b204b5bedc0552aec4ac82 Binary files /dev/null and b/backend/open_webui/static/splash-dark.png differ diff --git a/backend/open_webui/static/splash.png b/backend/open_webui/static/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..389196ca6a364b9e4b7daa0fc13be463b914b251 Binary files /dev/null and b/backend/open_webui/static/splash.png differ diff --git a/backend/open_webui/static/swagger-ui/favicon.png b/backend/open_webui/static/swagger-ui/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..e5b7c3ada7d093d237711560e03f6a841a4a41b2 Binary files /dev/null and b/backend/open_webui/static/swagger-ui/favicon.png differ diff --git a/backend/open_webui/static/swagger-ui/swagger-ui-bundle.js b/backend/open_webui/static/swagger-ui/swagger-ui-bundle.js new file mode 100644 index 0000000000000000000000000000000000000000..86568978728446b99559991cadbb3c4bca4f0af4 --- /dev/null +++ b/backend/open_webui/static/swagger-ui/swagger-ui-bundle.js @@ -0,0 +1,72362 @@ +/*! For license information please see swagger-ui-bundle.js.LICENSE.txt */ +!(function webpackUniversalModuleDefinition(s, o) { + 'object' == typeof exports && 'object' == typeof module + ? (module.exports = o()) + : 'function' == typeof define && define.amd + ? define([], o) + : 'object' == typeof exports + ? (exports.SwaggerUIBundle = o()) + : (s.SwaggerUIBundle = o()); +})(this, () => + (() => { + var s, + o, + i = { + 69119: (s, o) => { + 'use strict'; + (Object.defineProperty(o, '__esModule', { value: !0 }), + (o.BLANK_URL = + o.relativeFirstCharacters = + o.whitespaceEscapeCharsRegex = + o.urlSchemeRegex = + o.ctrlCharactersRegex = + o.htmlCtrlEntityRegex = + o.htmlEntitiesRegex = + o.invalidProtocolRegex = + void 0), + (o.invalidProtocolRegex = /^([^\w]*)(javascript|data|vbscript)/im), + (o.htmlEntitiesRegex = /&#(\w+)(^\w|;)?/g), + (o.htmlCtrlEntityRegex = /&(newline|tab);/gi), + (o.ctrlCharactersRegex = /[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim), + (o.urlSchemeRegex = /^.+(:|:)/gim), + (o.whitespaceEscapeCharsRegex = /(\\|%5[cC])((%(6[eE]|72|74))|[nrt])/g), + (o.relativeFirstCharacters = ['.', '/']), + (o.BLANK_URL = 'about:blank')); + }, + 16750: (s, o, i) => { + 'use strict'; + o.J = void 0; + var u = i(69119); + function decodeURI(s) { + try { + return decodeURIComponent(s); + } catch (o) { + return s; + } + } + o.J = function sanitizeUrl(s) { + if (!s) return u.BLANK_URL; + var o, + i, + _ = decodeURI(s); + do { + o = + (_ = decodeURI( + (_ = ((i = _), + i + .replace(u.ctrlCharactersRegex, '') + .replace(u.htmlEntitiesRegex, function (s, o) { + return String.fromCharCode(o); + })) + .replace(u.htmlCtrlEntityRegex, '') + .replace(u.ctrlCharactersRegex, '') + .replace(u.whitespaceEscapeCharsRegex, '') + .trim()) + )).match(u.ctrlCharactersRegex) || + _.match(u.htmlEntitiesRegex) || + _.match(u.htmlCtrlEntityRegex) || + _.match(u.whitespaceEscapeCharsRegex); + } while (o && o.length > 0); + var w = _; + if (!w) return u.BLANK_URL; + if ( + (function isRelativeUrlWithoutProtocol(s) { + return u.relativeFirstCharacters.indexOf(s[0]) > -1; + })(w) + ) + return w; + var x = w.match(u.urlSchemeRegex); + if (!x) return w; + var C = x[0]; + return u.invalidProtocolRegex.test(C) ? u.BLANK_URL : w; + }; + }, + 67526: (s, o) => { + 'use strict'; + ((o.byteLength = function byteLength(s) { + var o = getLens(s), + i = o[0], + u = o[1]; + return (3 * (i + u)) / 4 - u; + }), + (o.toByteArray = function toByteArray(s) { + var o, + i, + w = getLens(s), + x = w[0], + C = w[1], + j = new _( + (function _byteLength(s, o, i) { + return (3 * (o + i)) / 4 - i; + })(0, x, C) + ), + L = 0, + B = C > 0 ? x - 4 : x; + for (i = 0; i < B; i += 4) + ((o = + (u[s.charCodeAt(i)] << 18) | + (u[s.charCodeAt(i + 1)] << 12) | + (u[s.charCodeAt(i + 2)] << 6) | + u[s.charCodeAt(i + 3)]), + (j[L++] = (o >> 16) & 255), + (j[L++] = (o >> 8) & 255), + (j[L++] = 255 & o)); + 2 === C && + ((o = (u[s.charCodeAt(i)] << 2) | (u[s.charCodeAt(i + 1)] >> 4)), + (j[L++] = 255 & o)); + 1 === C && + ((o = + (u[s.charCodeAt(i)] << 10) | + (u[s.charCodeAt(i + 1)] << 4) | + (u[s.charCodeAt(i + 2)] >> 2)), + (j[L++] = (o >> 8) & 255), + (j[L++] = 255 & o)); + return j; + }), + (o.fromByteArray = function fromByteArray(s) { + for ( + var o, u = s.length, _ = u % 3, w = [], x = 16383, C = 0, j = u - _; + C < j; + C += x + ) + w.push(encodeChunk(s, C, C + x > j ? j : C + x)); + 1 === _ + ? ((o = s[u - 1]), w.push(i[o >> 2] + i[(o << 4) & 63] + '==')) + : 2 === _ && + ((o = (s[u - 2] << 8) + s[u - 1]), + w.push(i[o >> 10] + i[(o >> 4) & 63] + i[(o << 2) & 63] + '=')); + return w.join(''); + })); + for ( + var i = [], + u = [], + _ = 'undefined' != typeof Uint8Array ? Uint8Array : Array, + w = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', + x = 0; + x < 64; + ++x + ) + ((i[x] = w[x]), (u[w.charCodeAt(x)] = x)); + function getLens(s) { + var o = s.length; + if (o % 4 > 0) throw new Error('Invalid string. Length must be a multiple of 4'); + var i = s.indexOf('='); + return (-1 === i && (i = o), [i, i === o ? 0 : 4 - (i % 4)]); + } + function encodeChunk(s, o, u) { + for (var _, w, x = [], C = o; C < u; C += 3) + ((_ = ((s[C] << 16) & 16711680) + ((s[C + 1] << 8) & 65280) + (255 & s[C + 2])), + x.push(i[((w = _) >> 18) & 63] + i[(w >> 12) & 63] + i[(w >> 6) & 63] + i[63 & w])); + return x.join(''); + } + ((u['-'.charCodeAt(0)] = 62), (u['_'.charCodeAt(0)] = 63)); + }, + 48287: (s, o, i) => { + 'use strict'; + const u = i(67526), + _ = i(251), + w = + 'function' == typeof Symbol && 'function' == typeof Symbol.for + ? Symbol.for('nodejs.util.inspect.custom') + : null; + ((o.Buffer = Buffer), + (o.SlowBuffer = function SlowBuffer(s) { + +s != s && (s = 0); + return Buffer.alloc(+s); + }), + (o.INSPECT_MAX_BYTES = 50)); + const x = 2147483647; + function createBuffer(s) { + if (s > x) throw new RangeError('The value "' + s + '" is invalid for option "size"'); + const o = new Uint8Array(s); + return (Object.setPrototypeOf(o, Buffer.prototype), o); + } + function Buffer(s, o, i) { + if ('number' == typeof s) { + if ('string' == typeof o) + throw new TypeError( + 'The "string" argument must be of type string. Received type number' + ); + return allocUnsafe(s); + } + return from(s, o, i); + } + function from(s, o, i) { + if ('string' == typeof s) + return (function fromString(s, o) { + ('string' == typeof o && '' !== o) || (o = 'utf8'); + if (!Buffer.isEncoding(o)) throw new TypeError('Unknown encoding: ' + o); + const i = 0 | byteLength(s, o); + let u = createBuffer(i); + const _ = u.write(s, o); + _ !== i && (u = u.slice(0, _)); + return u; + })(s, o); + if (ArrayBuffer.isView(s)) + return (function fromArrayView(s) { + if (isInstance(s, Uint8Array)) { + const o = new Uint8Array(s); + return fromArrayBuffer(o.buffer, o.byteOffset, o.byteLength); + } + return fromArrayLike(s); + })(s); + if (null == s) + throw new TypeError( + 'The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type ' + + typeof s + ); + if (isInstance(s, ArrayBuffer) || (s && isInstance(s.buffer, ArrayBuffer))) + return fromArrayBuffer(s, o, i); + if ( + 'undefined' != typeof SharedArrayBuffer && + (isInstance(s, SharedArrayBuffer) || (s && isInstance(s.buffer, SharedArrayBuffer))) + ) + return fromArrayBuffer(s, o, i); + if ('number' == typeof s) + throw new TypeError( + 'The "value" argument must not be of type number. Received type number' + ); + const u = s.valueOf && s.valueOf(); + if (null != u && u !== s) return Buffer.from(u, o, i); + const _ = (function fromObject(s) { + if (Buffer.isBuffer(s)) { + const o = 0 | checked(s.length), + i = createBuffer(o); + return (0 === i.length || s.copy(i, 0, 0, o), i); + } + if (void 0 !== s.length) + return 'number' != typeof s.length || numberIsNaN(s.length) + ? createBuffer(0) + : fromArrayLike(s); + if ('Buffer' === s.type && Array.isArray(s.data)) return fromArrayLike(s.data); + })(s); + if (_) return _; + if ( + 'undefined' != typeof Symbol && + null != Symbol.toPrimitive && + 'function' == typeof s[Symbol.toPrimitive] + ) + return Buffer.from(s[Symbol.toPrimitive]('string'), o, i); + throw new TypeError( + 'The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type ' + + typeof s + ); + } + function assertSize(s) { + if ('number' != typeof s) throw new TypeError('"size" argument must be of type number'); + if (s < 0) throw new RangeError('The value "' + s + '" is invalid for option "size"'); + } + function allocUnsafe(s) { + return (assertSize(s), createBuffer(s < 0 ? 0 : 0 | checked(s))); + } + function fromArrayLike(s) { + const o = s.length < 0 ? 0 : 0 | checked(s.length), + i = createBuffer(o); + for (let u = 0; u < o; u += 1) i[u] = 255 & s[u]; + return i; + } + function fromArrayBuffer(s, o, i) { + if (o < 0 || s.byteLength < o) + throw new RangeError('"offset" is outside of buffer bounds'); + if (s.byteLength < o + (i || 0)) + throw new RangeError('"length" is outside of buffer bounds'); + let u; + return ( + (u = + void 0 === o && void 0 === i + ? new Uint8Array(s) + : void 0 === i + ? new Uint8Array(s, o) + : new Uint8Array(s, o, i)), + Object.setPrototypeOf(u, Buffer.prototype), + u + ); + } + function checked(s) { + if (s >= x) + throw new RangeError( + 'Attempt to allocate Buffer larger than maximum size: 0x' + + x.toString(16) + + ' bytes' + ); + return 0 | s; + } + function byteLength(s, o) { + if (Buffer.isBuffer(s)) return s.length; + if (ArrayBuffer.isView(s) || isInstance(s, ArrayBuffer)) return s.byteLength; + if ('string' != typeof s) + throw new TypeError( + 'The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type ' + + typeof s + ); + const i = s.length, + u = arguments.length > 2 && !0 === arguments[2]; + if (!u && 0 === i) return 0; + let _ = !1; + for (;;) + switch (o) { + case 'ascii': + case 'latin1': + case 'binary': + return i; + case 'utf8': + case 'utf-8': + return utf8ToBytes(s).length; + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return 2 * i; + case 'hex': + return i >>> 1; + case 'base64': + return base64ToBytes(s).length; + default: + if (_) return u ? -1 : utf8ToBytes(s).length; + ((o = ('' + o).toLowerCase()), (_ = !0)); + } + } + function slowToString(s, o, i) { + let u = !1; + if (((void 0 === o || o < 0) && (o = 0), o > this.length)) return ''; + if (((void 0 === i || i > this.length) && (i = this.length), i <= 0)) return ''; + if ((i >>>= 0) <= (o >>>= 0)) return ''; + for (s || (s = 'utf8'); ; ) + switch (s) { + case 'hex': + return hexSlice(this, o, i); + case 'utf8': + case 'utf-8': + return utf8Slice(this, o, i); + case 'ascii': + return asciiSlice(this, o, i); + case 'latin1': + case 'binary': + return latin1Slice(this, o, i); + case 'base64': + return base64Slice(this, o, i); + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return utf16leSlice(this, o, i); + default: + if (u) throw new TypeError('Unknown encoding: ' + s); + ((s = (s + '').toLowerCase()), (u = !0)); + } + } + function swap(s, o, i) { + const u = s[o]; + ((s[o] = s[i]), (s[i] = u)); + } + function bidirectionalIndexOf(s, o, i, u, _) { + if (0 === s.length) return -1; + if ( + ('string' == typeof i + ? ((u = i), (i = 0)) + : i > 2147483647 + ? (i = 2147483647) + : i < -2147483648 && (i = -2147483648), + numberIsNaN((i = +i)) && (i = _ ? 0 : s.length - 1), + i < 0 && (i = s.length + i), + i >= s.length) + ) { + if (_) return -1; + i = s.length - 1; + } else if (i < 0) { + if (!_) return -1; + i = 0; + } + if (('string' == typeof o && (o = Buffer.from(o, u)), Buffer.isBuffer(o))) + return 0 === o.length ? -1 : arrayIndexOf(s, o, i, u, _); + if ('number' == typeof o) + return ( + (o &= 255), + 'function' == typeof Uint8Array.prototype.indexOf + ? _ + ? Uint8Array.prototype.indexOf.call(s, o, i) + : Uint8Array.prototype.lastIndexOf.call(s, o, i) + : arrayIndexOf(s, [o], i, u, _) + ); + throw new TypeError('val must be string, number or Buffer'); + } + function arrayIndexOf(s, o, i, u, _) { + let w, + x = 1, + C = s.length, + j = o.length; + if ( + void 0 !== u && + ('ucs2' === (u = String(u).toLowerCase()) || + 'ucs-2' === u || + 'utf16le' === u || + 'utf-16le' === u) + ) { + if (s.length < 2 || o.length < 2) return -1; + ((x = 2), (C /= 2), (j /= 2), (i /= 2)); + } + function read(s, o) { + return 1 === x ? s[o] : s.readUInt16BE(o * x); + } + if (_) { + let u = -1; + for (w = i; w < C; w++) + if (read(s, w) === read(o, -1 === u ? 0 : w - u)) { + if ((-1 === u && (u = w), w - u + 1 === j)) return u * x; + } else (-1 !== u && (w -= w - u), (u = -1)); + } else + for (i + j > C && (i = C - j), w = i; w >= 0; w--) { + let i = !0; + for (let u = 0; u < j; u++) + if (read(s, w + u) !== read(o, u)) { + i = !1; + break; + } + if (i) return w; + } + return -1; + } + function hexWrite(s, o, i, u) { + i = Number(i) || 0; + const _ = s.length - i; + u ? (u = Number(u)) > _ && (u = _) : (u = _); + const w = o.length; + let x; + for (u > w / 2 && (u = w / 2), x = 0; x < u; ++x) { + const u = parseInt(o.substr(2 * x, 2), 16); + if (numberIsNaN(u)) return x; + s[i + x] = u; + } + return x; + } + function utf8Write(s, o, i, u) { + return blitBuffer(utf8ToBytes(o, s.length - i), s, i, u); + } + function asciiWrite(s, o, i, u) { + return blitBuffer( + (function asciiToBytes(s) { + const o = []; + for (let i = 0; i < s.length; ++i) o.push(255 & s.charCodeAt(i)); + return o; + })(o), + s, + i, + u + ); + } + function base64Write(s, o, i, u) { + return blitBuffer(base64ToBytes(o), s, i, u); + } + function ucs2Write(s, o, i, u) { + return blitBuffer( + (function utf16leToBytes(s, o) { + let i, u, _; + const w = []; + for (let x = 0; x < s.length && !((o -= 2) < 0); ++x) + ((i = s.charCodeAt(x)), (u = i >> 8), (_ = i % 256), w.push(_), w.push(u)); + return w; + })(o, s.length - i), + s, + i, + u + ); + } + function base64Slice(s, o, i) { + return 0 === o && i === s.length ? u.fromByteArray(s) : u.fromByteArray(s.slice(o, i)); + } + function utf8Slice(s, o, i) { + i = Math.min(s.length, i); + const u = []; + let _ = o; + for (; _ < i; ) { + const o = s[_]; + let w = null, + x = o > 239 ? 4 : o > 223 ? 3 : o > 191 ? 2 : 1; + if (_ + x <= i) { + let i, u, C, j; + switch (x) { + case 1: + o < 128 && (w = o); + break; + case 2: + ((i = s[_ + 1]), + 128 == (192 & i) && ((j = ((31 & o) << 6) | (63 & i)), j > 127 && (w = j))); + break; + case 3: + ((i = s[_ + 1]), + (u = s[_ + 2]), + 128 == (192 & i) && + 128 == (192 & u) && + ((j = ((15 & o) << 12) | ((63 & i) << 6) | (63 & u)), + j > 2047 && (j < 55296 || j > 57343) && (w = j))); + break; + case 4: + ((i = s[_ + 1]), + (u = s[_ + 2]), + (C = s[_ + 3]), + 128 == (192 & i) && + 128 == (192 & u) && + 128 == (192 & C) && + ((j = ((15 & o) << 18) | ((63 & i) << 12) | ((63 & u) << 6) | (63 & C)), + j > 65535 && j < 1114112 && (w = j))); + } + } + (null === w + ? ((w = 65533), (x = 1)) + : w > 65535 && + ((w -= 65536), u.push(((w >>> 10) & 1023) | 55296), (w = 56320 | (1023 & w))), + u.push(w), + (_ += x)); + } + return (function decodeCodePointsArray(s) { + const o = s.length; + if (o <= C) return String.fromCharCode.apply(String, s); + let i = '', + u = 0; + for (; u < o; ) i += String.fromCharCode.apply(String, s.slice(u, (u += C))); + return i; + })(u); + } + ((o.kMaxLength = x), + (Buffer.TYPED_ARRAY_SUPPORT = (function typedArraySupport() { + try { + const s = new Uint8Array(1), + o = { + foo: function () { + return 42; + } + }; + return ( + Object.setPrototypeOf(o, Uint8Array.prototype), + Object.setPrototypeOf(s, o), + 42 === s.foo() + ); + } catch (s) { + return !1; + } + })()), + Buffer.TYPED_ARRAY_SUPPORT || + 'undefined' == typeof console || + 'function' != typeof console.error || + console.error( + 'This browser lacks typed array (Uint8Array) support which is required by `buffer` v5.x. Use `buffer` v4.x if you require old browser support.' + ), + Object.defineProperty(Buffer.prototype, 'parent', { + enumerable: !0, + get: function () { + if (Buffer.isBuffer(this)) return this.buffer; + } + }), + Object.defineProperty(Buffer.prototype, 'offset', { + enumerable: !0, + get: function () { + if (Buffer.isBuffer(this)) return this.byteOffset; + } + }), + (Buffer.poolSize = 8192), + (Buffer.from = function (s, o, i) { + return from(s, o, i); + }), + Object.setPrototypeOf(Buffer.prototype, Uint8Array.prototype), + Object.setPrototypeOf(Buffer, Uint8Array), + (Buffer.alloc = function (s, o, i) { + return (function alloc(s, o, i) { + return ( + assertSize(s), + s <= 0 + ? createBuffer(s) + : void 0 !== o + ? 'string' == typeof i + ? createBuffer(s).fill(o, i) + : createBuffer(s).fill(o) + : createBuffer(s) + ); + })(s, o, i); + }), + (Buffer.allocUnsafe = function (s) { + return allocUnsafe(s); + }), + (Buffer.allocUnsafeSlow = function (s) { + return allocUnsafe(s); + }), + (Buffer.isBuffer = function isBuffer(s) { + return null != s && !0 === s._isBuffer && s !== Buffer.prototype; + }), + (Buffer.compare = function compare(s, o) { + if ( + (isInstance(s, Uint8Array) && (s = Buffer.from(s, s.offset, s.byteLength)), + isInstance(o, Uint8Array) && (o = Buffer.from(o, o.offset, o.byteLength)), + !Buffer.isBuffer(s) || !Buffer.isBuffer(o)) + ) + throw new TypeError( + 'The "buf1", "buf2" arguments must be one of type Buffer or Uint8Array' + ); + if (s === o) return 0; + let i = s.length, + u = o.length; + for (let _ = 0, w = Math.min(i, u); _ < w; ++_) + if (s[_] !== o[_]) { + ((i = s[_]), (u = o[_])); + break; + } + return i < u ? -1 : u < i ? 1 : 0; + }), + (Buffer.isEncoding = function isEncoding(s) { + switch (String(s).toLowerCase()) { + case 'hex': + case 'utf8': + case 'utf-8': + case 'ascii': + case 'latin1': + case 'binary': + case 'base64': + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return !0; + default: + return !1; + } + }), + (Buffer.concat = function concat(s, o) { + if (!Array.isArray(s)) + throw new TypeError('"list" argument must be an Array of Buffers'); + if (0 === s.length) return Buffer.alloc(0); + let i; + if (void 0 === o) for (o = 0, i = 0; i < s.length; ++i) o += s[i].length; + const u = Buffer.allocUnsafe(o); + let _ = 0; + for (i = 0; i < s.length; ++i) { + let o = s[i]; + if (isInstance(o, Uint8Array)) + _ + o.length > u.length + ? (Buffer.isBuffer(o) || (o = Buffer.from(o)), o.copy(u, _)) + : Uint8Array.prototype.set.call(u, o, _); + else { + if (!Buffer.isBuffer(o)) + throw new TypeError('"list" argument must be an Array of Buffers'); + o.copy(u, _); + } + _ += o.length; + } + return u; + }), + (Buffer.byteLength = byteLength), + (Buffer.prototype._isBuffer = !0), + (Buffer.prototype.swap16 = function swap16() { + const s = this.length; + if (s % 2 != 0) throw new RangeError('Buffer size must be a multiple of 16-bits'); + for (let o = 0; o < s; o += 2) swap(this, o, o + 1); + return this; + }), + (Buffer.prototype.swap32 = function swap32() { + const s = this.length; + if (s % 4 != 0) throw new RangeError('Buffer size must be a multiple of 32-bits'); + for (let o = 0; o < s; o += 4) (swap(this, o, o + 3), swap(this, o + 1, o + 2)); + return this; + }), + (Buffer.prototype.swap64 = function swap64() { + const s = this.length; + if (s % 8 != 0) throw new RangeError('Buffer size must be a multiple of 64-bits'); + for (let o = 0; o < s; o += 8) + (swap(this, o, o + 7), + swap(this, o + 1, o + 6), + swap(this, o + 2, o + 5), + swap(this, o + 3, o + 4)); + return this; + }), + (Buffer.prototype.toString = function toString() { + const s = this.length; + return 0 === s + ? '' + : 0 === arguments.length + ? utf8Slice(this, 0, s) + : slowToString.apply(this, arguments); + }), + (Buffer.prototype.toLocaleString = Buffer.prototype.toString), + (Buffer.prototype.equals = function equals(s) { + if (!Buffer.isBuffer(s)) throw new TypeError('Argument must be a Buffer'); + return this === s || 0 === Buffer.compare(this, s); + }), + (Buffer.prototype.inspect = function inspect() { + let s = ''; + const i = o.INSPECT_MAX_BYTES; + return ( + (s = this.toString('hex', 0, i) + .replace(/(.{2})/g, '$1 ') + .trim()), + this.length > i && (s += ' ... '), + '' + ); + }), + w && (Buffer.prototype[w] = Buffer.prototype.inspect), + (Buffer.prototype.compare = function compare(s, o, i, u, _) { + if ( + (isInstance(s, Uint8Array) && (s = Buffer.from(s, s.offset, s.byteLength)), + !Buffer.isBuffer(s)) + ) + throw new TypeError( + 'The "target" argument must be one of type Buffer or Uint8Array. Received type ' + + typeof s + ); + if ( + (void 0 === o && (o = 0), + void 0 === i && (i = s ? s.length : 0), + void 0 === u && (u = 0), + void 0 === _ && (_ = this.length), + o < 0 || i > s.length || u < 0 || _ > this.length) + ) + throw new RangeError('out of range index'); + if (u >= _ && o >= i) return 0; + if (u >= _) return -1; + if (o >= i) return 1; + if (this === s) return 0; + let w = (_ >>>= 0) - (u >>>= 0), + x = (i >>>= 0) - (o >>>= 0); + const C = Math.min(w, x), + j = this.slice(u, _), + L = s.slice(o, i); + for (let s = 0; s < C; ++s) + if (j[s] !== L[s]) { + ((w = j[s]), (x = L[s])); + break; + } + return w < x ? -1 : x < w ? 1 : 0; + }), + (Buffer.prototype.includes = function includes(s, o, i) { + return -1 !== this.indexOf(s, o, i); + }), + (Buffer.prototype.indexOf = function indexOf(s, o, i) { + return bidirectionalIndexOf(this, s, o, i, !0); + }), + (Buffer.prototype.lastIndexOf = function lastIndexOf(s, o, i) { + return bidirectionalIndexOf(this, s, o, i, !1); + }), + (Buffer.prototype.write = function write(s, o, i, u) { + if (void 0 === o) ((u = 'utf8'), (i = this.length), (o = 0)); + else if (void 0 === i && 'string' == typeof o) ((u = o), (i = this.length), (o = 0)); + else { + if (!isFinite(o)) + throw new Error( + 'Buffer.write(string, encoding, offset[, length]) is no longer supported' + ); + ((o >>>= 0), + isFinite(i) + ? ((i >>>= 0), void 0 === u && (u = 'utf8')) + : ((u = i), (i = void 0))); + } + const _ = this.length - o; + if ( + ((void 0 === i || i > _) && (i = _), + (s.length > 0 && (i < 0 || o < 0)) || o > this.length) + ) + throw new RangeError('Attempt to write outside buffer bounds'); + u || (u = 'utf8'); + let w = !1; + for (;;) + switch (u) { + case 'hex': + return hexWrite(this, s, o, i); + case 'utf8': + case 'utf-8': + return utf8Write(this, s, o, i); + case 'ascii': + case 'latin1': + case 'binary': + return asciiWrite(this, s, o, i); + case 'base64': + return base64Write(this, s, o, i); + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return ucs2Write(this, s, o, i); + default: + if (w) throw new TypeError('Unknown encoding: ' + u); + ((u = ('' + u).toLowerCase()), (w = !0)); + } + }), + (Buffer.prototype.toJSON = function toJSON() { + return { type: 'Buffer', data: Array.prototype.slice.call(this._arr || this, 0) }; + })); + const C = 4096; + function asciiSlice(s, o, i) { + let u = ''; + i = Math.min(s.length, i); + for (let _ = o; _ < i; ++_) u += String.fromCharCode(127 & s[_]); + return u; + } + function latin1Slice(s, o, i) { + let u = ''; + i = Math.min(s.length, i); + for (let _ = o; _ < i; ++_) u += String.fromCharCode(s[_]); + return u; + } + function hexSlice(s, o, i) { + const u = s.length; + ((!o || o < 0) && (o = 0), (!i || i < 0 || i > u) && (i = u)); + let _ = ''; + for (let u = o; u < i; ++u) _ += B[s[u]]; + return _; + } + function utf16leSlice(s, o, i) { + const u = s.slice(o, i); + let _ = ''; + for (let s = 0; s < u.length - 1; s += 2) + _ += String.fromCharCode(u[s] + 256 * u[s + 1]); + return _; + } + function checkOffset(s, o, i) { + if (s % 1 != 0 || s < 0) throw new RangeError('offset is not uint'); + if (s + o > i) throw new RangeError('Trying to access beyond buffer length'); + } + function checkInt(s, o, i, u, _, w) { + if (!Buffer.isBuffer(s)) + throw new TypeError('"buffer" argument must be a Buffer instance'); + if (o > _ || o < w) throw new RangeError('"value" argument is out of bounds'); + if (i + u > s.length) throw new RangeError('Index out of range'); + } + function wrtBigUInt64LE(s, o, i, u, _) { + checkIntBI(o, u, _, s, i, 7); + let w = Number(o & BigInt(4294967295)); + ((s[i++] = w), + (w >>= 8), + (s[i++] = w), + (w >>= 8), + (s[i++] = w), + (w >>= 8), + (s[i++] = w)); + let x = Number((o >> BigInt(32)) & BigInt(4294967295)); + return ( + (s[i++] = x), + (x >>= 8), + (s[i++] = x), + (x >>= 8), + (s[i++] = x), + (x >>= 8), + (s[i++] = x), + i + ); + } + function wrtBigUInt64BE(s, o, i, u, _) { + checkIntBI(o, u, _, s, i, 7); + let w = Number(o & BigInt(4294967295)); + ((s[i + 7] = w), + (w >>= 8), + (s[i + 6] = w), + (w >>= 8), + (s[i + 5] = w), + (w >>= 8), + (s[i + 4] = w)); + let x = Number((o >> BigInt(32)) & BigInt(4294967295)); + return ( + (s[i + 3] = x), + (x >>= 8), + (s[i + 2] = x), + (x >>= 8), + (s[i + 1] = x), + (x >>= 8), + (s[i] = x), + i + 8 + ); + } + function checkIEEE754(s, o, i, u, _, w) { + if (i + u > s.length) throw new RangeError('Index out of range'); + if (i < 0) throw new RangeError('Index out of range'); + } + function writeFloat(s, o, i, u, w) { + return ( + (o = +o), + (i >>>= 0), + w || checkIEEE754(s, 0, i, 4), + _.write(s, o, i, u, 23, 4), + i + 4 + ); + } + function writeDouble(s, o, i, u, w) { + return ( + (o = +o), + (i >>>= 0), + w || checkIEEE754(s, 0, i, 8), + _.write(s, o, i, u, 52, 8), + i + 8 + ); + } + ((Buffer.prototype.slice = function slice(s, o) { + const i = this.length; + ((s = ~~s) < 0 ? (s += i) < 0 && (s = 0) : s > i && (s = i), + (o = void 0 === o ? i : ~~o) < 0 ? (o += i) < 0 && (o = 0) : o > i && (o = i), + o < s && (o = s)); + const u = this.subarray(s, o); + return (Object.setPrototypeOf(u, Buffer.prototype), u); + }), + (Buffer.prototype.readUintLE = Buffer.prototype.readUIntLE = + function readUIntLE(s, o, i) { + ((s >>>= 0), (o >>>= 0), i || checkOffset(s, o, this.length)); + let u = this[s], + _ = 1, + w = 0; + for (; ++w < o && (_ *= 256); ) u += this[s + w] * _; + return u; + }), + (Buffer.prototype.readUintBE = Buffer.prototype.readUIntBE = + function readUIntBE(s, o, i) { + ((s >>>= 0), (o >>>= 0), i || checkOffset(s, o, this.length)); + let u = this[s + --o], + _ = 1; + for (; o > 0 && (_ *= 256); ) u += this[s + --o] * _; + return u; + }), + (Buffer.prototype.readUint8 = Buffer.prototype.readUInt8 = + function readUInt8(s, o) { + return ((s >>>= 0), o || checkOffset(s, 1, this.length), this[s]); + }), + (Buffer.prototype.readUint16LE = Buffer.prototype.readUInt16LE = + function readUInt16LE(s, o) { + return ( + (s >>>= 0), + o || checkOffset(s, 2, this.length), + this[s] | (this[s + 1] << 8) + ); + }), + (Buffer.prototype.readUint16BE = Buffer.prototype.readUInt16BE = + function readUInt16BE(s, o) { + return ( + (s >>>= 0), + o || checkOffset(s, 2, this.length), + (this[s] << 8) | this[s + 1] + ); + }), + (Buffer.prototype.readUint32LE = Buffer.prototype.readUInt32LE = + function readUInt32LE(s, o) { + return ( + (s >>>= 0), + o || checkOffset(s, 4, this.length), + (this[s] | (this[s + 1] << 8) | (this[s + 2] << 16)) + 16777216 * this[s + 3] + ); + }), + (Buffer.prototype.readUint32BE = Buffer.prototype.readUInt32BE = + function readUInt32BE(s, o) { + return ( + (s >>>= 0), + o || checkOffset(s, 4, this.length), + 16777216 * this[s] + ((this[s + 1] << 16) | (this[s + 2] << 8) | this[s + 3]) + ); + }), + (Buffer.prototype.readBigUInt64LE = defineBigIntMethod(function readBigUInt64LE(s) { + validateNumber((s >>>= 0), 'offset'); + const o = this[s], + i = this[s + 7]; + (void 0 !== o && void 0 !== i) || boundsError(s, this.length - 8); + const u = o + 256 * this[++s] + 65536 * this[++s] + this[++s] * 2 ** 24, + _ = this[++s] + 256 * this[++s] + 65536 * this[++s] + i * 2 ** 24; + return BigInt(u) + (BigInt(_) << BigInt(32)); + })), + (Buffer.prototype.readBigUInt64BE = defineBigIntMethod(function readBigUInt64BE(s) { + validateNumber((s >>>= 0), 'offset'); + const o = this[s], + i = this[s + 7]; + (void 0 !== o && void 0 !== i) || boundsError(s, this.length - 8); + const u = o * 2 ** 24 + 65536 * this[++s] + 256 * this[++s] + this[++s], + _ = this[++s] * 2 ** 24 + 65536 * this[++s] + 256 * this[++s] + i; + return (BigInt(u) << BigInt(32)) + BigInt(_); + })), + (Buffer.prototype.readIntLE = function readIntLE(s, o, i) { + ((s >>>= 0), (o >>>= 0), i || checkOffset(s, o, this.length)); + let u = this[s], + _ = 1, + w = 0; + for (; ++w < o && (_ *= 256); ) u += this[s + w] * _; + return ((_ *= 128), u >= _ && (u -= Math.pow(2, 8 * o)), u); + }), + (Buffer.prototype.readIntBE = function readIntBE(s, o, i) { + ((s >>>= 0), (o >>>= 0), i || checkOffset(s, o, this.length)); + let u = o, + _ = 1, + w = this[s + --u]; + for (; u > 0 && (_ *= 256); ) w += this[s + --u] * _; + return ((_ *= 128), w >= _ && (w -= Math.pow(2, 8 * o)), w); + }), + (Buffer.prototype.readInt8 = function readInt8(s, o) { + return ( + (s >>>= 0), + o || checkOffset(s, 1, this.length), + 128 & this[s] ? -1 * (255 - this[s] + 1) : this[s] + ); + }), + (Buffer.prototype.readInt16LE = function readInt16LE(s, o) { + ((s >>>= 0), o || checkOffset(s, 2, this.length)); + const i = this[s] | (this[s + 1] << 8); + return 32768 & i ? 4294901760 | i : i; + }), + (Buffer.prototype.readInt16BE = function readInt16BE(s, o) { + ((s >>>= 0), o || checkOffset(s, 2, this.length)); + const i = this[s + 1] | (this[s] << 8); + return 32768 & i ? 4294901760 | i : i; + }), + (Buffer.prototype.readInt32LE = function readInt32LE(s, o) { + return ( + (s >>>= 0), + o || checkOffset(s, 4, this.length), + this[s] | (this[s + 1] << 8) | (this[s + 2] << 16) | (this[s + 3] << 24) + ); + }), + (Buffer.prototype.readInt32BE = function readInt32BE(s, o) { + return ( + (s >>>= 0), + o || checkOffset(s, 4, this.length), + (this[s] << 24) | (this[s + 1] << 16) | (this[s + 2] << 8) | this[s + 3] + ); + }), + (Buffer.prototype.readBigInt64LE = defineBigIntMethod(function readBigInt64LE(s) { + validateNumber((s >>>= 0), 'offset'); + const o = this[s], + i = this[s + 7]; + (void 0 !== o && void 0 !== i) || boundsError(s, this.length - 8); + const u = this[s + 4] + 256 * this[s + 5] + 65536 * this[s + 6] + (i << 24); + return ( + (BigInt(u) << BigInt(32)) + + BigInt(o + 256 * this[++s] + 65536 * this[++s] + this[++s] * 2 ** 24) + ); + })), + (Buffer.prototype.readBigInt64BE = defineBigIntMethod(function readBigInt64BE(s) { + validateNumber((s >>>= 0), 'offset'); + const o = this[s], + i = this[s + 7]; + (void 0 !== o && void 0 !== i) || boundsError(s, this.length - 8); + const u = (o << 24) + 65536 * this[++s] + 256 * this[++s] + this[++s]; + return ( + (BigInt(u) << BigInt(32)) + + BigInt(this[++s] * 2 ** 24 + 65536 * this[++s] + 256 * this[++s] + i) + ); + })), + (Buffer.prototype.readFloatLE = function readFloatLE(s, o) { + return ((s >>>= 0), o || checkOffset(s, 4, this.length), _.read(this, s, !0, 23, 4)); + }), + (Buffer.prototype.readFloatBE = function readFloatBE(s, o) { + return ((s >>>= 0), o || checkOffset(s, 4, this.length), _.read(this, s, !1, 23, 4)); + }), + (Buffer.prototype.readDoubleLE = function readDoubleLE(s, o) { + return ((s >>>= 0), o || checkOffset(s, 8, this.length), _.read(this, s, !0, 52, 8)); + }), + (Buffer.prototype.readDoubleBE = function readDoubleBE(s, o) { + return ((s >>>= 0), o || checkOffset(s, 8, this.length), _.read(this, s, !1, 52, 8)); + }), + (Buffer.prototype.writeUintLE = Buffer.prototype.writeUIntLE = + function writeUIntLE(s, o, i, u) { + if (((s = +s), (o >>>= 0), (i >>>= 0), !u)) { + checkInt(this, s, o, i, Math.pow(2, 8 * i) - 1, 0); + } + let _ = 1, + w = 0; + for (this[o] = 255 & s; ++w < i && (_ *= 256); ) this[o + w] = (s / _) & 255; + return o + i; + }), + (Buffer.prototype.writeUintBE = Buffer.prototype.writeUIntBE = + function writeUIntBE(s, o, i, u) { + if (((s = +s), (o >>>= 0), (i >>>= 0), !u)) { + checkInt(this, s, o, i, Math.pow(2, 8 * i) - 1, 0); + } + let _ = i - 1, + w = 1; + for (this[o + _] = 255 & s; --_ >= 0 && (w *= 256); ) this[o + _] = (s / w) & 255; + return o + i; + }), + (Buffer.prototype.writeUint8 = Buffer.prototype.writeUInt8 = + function writeUInt8(s, o, i) { + return ( + (s = +s), + (o >>>= 0), + i || checkInt(this, s, o, 1, 255, 0), + (this[o] = 255 & s), + o + 1 + ); + }), + (Buffer.prototype.writeUint16LE = Buffer.prototype.writeUInt16LE = + function writeUInt16LE(s, o, i) { + return ( + (s = +s), + (o >>>= 0), + i || checkInt(this, s, o, 2, 65535, 0), + (this[o] = 255 & s), + (this[o + 1] = s >>> 8), + o + 2 + ); + }), + (Buffer.prototype.writeUint16BE = Buffer.prototype.writeUInt16BE = + function writeUInt16BE(s, o, i) { + return ( + (s = +s), + (o >>>= 0), + i || checkInt(this, s, o, 2, 65535, 0), + (this[o] = s >>> 8), + (this[o + 1] = 255 & s), + o + 2 + ); + }), + (Buffer.prototype.writeUint32LE = Buffer.prototype.writeUInt32LE = + function writeUInt32LE(s, o, i) { + return ( + (s = +s), + (o >>>= 0), + i || checkInt(this, s, o, 4, 4294967295, 0), + (this[o + 3] = s >>> 24), + (this[o + 2] = s >>> 16), + (this[o + 1] = s >>> 8), + (this[o] = 255 & s), + o + 4 + ); + }), + (Buffer.prototype.writeUint32BE = Buffer.prototype.writeUInt32BE = + function writeUInt32BE(s, o, i) { + return ( + (s = +s), + (o >>>= 0), + i || checkInt(this, s, o, 4, 4294967295, 0), + (this[o] = s >>> 24), + (this[o + 1] = s >>> 16), + (this[o + 2] = s >>> 8), + (this[o + 3] = 255 & s), + o + 4 + ); + }), + (Buffer.prototype.writeBigUInt64LE = defineBigIntMethod(function writeBigUInt64LE( + s, + o = 0 + ) { + return wrtBigUInt64LE(this, s, o, BigInt(0), BigInt('0xffffffffffffffff')); + })), + (Buffer.prototype.writeBigUInt64BE = defineBigIntMethod(function writeBigUInt64BE( + s, + o = 0 + ) { + return wrtBigUInt64BE(this, s, o, BigInt(0), BigInt('0xffffffffffffffff')); + })), + (Buffer.prototype.writeIntLE = function writeIntLE(s, o, i, u) { + if (((s = +s), (o >>>= 0), !u)) { + const u = Math.pow(2, 8 * i - 1); + checkInt(this, s, o, i, u - 1, -u); + } + let _ = 0, + w = 1, + x = 0; + for (this[o] = 255 & s; ++_ < i && (w *= 256); ) + (s < 0 && 0 === x && 0 !== this[o + _ - 1] && (x = 1), + (this[o + _] = (((s / w) | 0) - x) & 255)); + return o + i; + }), + (Buffer.prototype.writeIntBE = function writeIntBE(s, o, i, u) { + if (((s = +s), (o >>>= 0), !u)) { + const u = Math.pow(2, 8 * i - 1); + checkInt(this, s, o, i, u - 1, -u); + } + let _ = i - 1, + w = 1, + x = 0; + for (this[o + _] = 255 & s; --_ >= 0 && (w *= 256); ) + (s < 0 && 0 === x && 0 !== this[o + _ + 1] && (x = 1), + (this[o + _] = (((s / w) | 0) - x) & 255)); + return o + i; + }), + (Buffer.prototype.writeInt8 = function writeInt8(s, o, i) { + return ( + (s = +s), + (o >>>= 0), + i || checkInt(this, s, o, 1, 127, -128), + s < 0 && (s = 255 + s + 1), + (this[o] = 255 & s), + o + 1 + ); + }), + (Buffer.prototype.writeInt16LE = function writeInt16LE(s, o, i) { + return ( + (s = +s), + (o >>>= 0), + i || checkInt(this, s, o, 2, 32767, -32768), + (this[o] = 255 & s), + (this[o + 1] = s >>> 8), + o + 2 + ); + }), + (Buffer.prototype.writeInt16BE = function writeInt16BE(s, o, i) { + return ( + (s = +s), + (o >>>= 0), + i || checkInt(this, s, o, 2, 32767, -32768), + (this[o] = s >>> 8), + (this[o + 1] = 255 & s), + o + 2 + ); + }), + (Buffer.prototype.writeInt32LE = function writeInt32LE(s, o, i) { + return ( + (s = +s), + (o >>>= 0), + i || checkInt(this, s, o, 4, 2147483647, -2147483648), + (this[o] = 255 & s), + (this[o + 1] = s >>> 8), + (this[o + 2] = s >>> 16), + (this[o + 3] = s >>> 24), + o + 4 + ); + }), + (Buffer.prototype.writeInt32BE = function writeInt32BE(s, o, i) { + return ( + (s = +s), + (o >>>= 0), + i || checkInt(this, s, o, 4, 2147483647, -2147483648), + s < 0 && (s = 4294967295 + s + 1), + (this[o] = s >>> 24), + (this[o + 1] = s >>> 16), + (this[o + 2] = s >>> 8), + (this[o + 3] = 255 & s), + o + 4 + ); + }), + (Buffer.prototype.writeBigInt64LE = defineBigIntMethod(function writeBigInt64LE( + s, + o = 0 + ) { + return wrtBigUInt64LE( + this, + s, + o, + -BigInt('0x8000000000000000'), + BigInt('0x7fffffffffffffff') + ); + })), + (Buffer.prototype.writeBigInt64BE = defineBigIntMethod(function writeBigInt64BE( + s, + o = 0 + ) { + return wrtBigUInt64BE( + this, + s, + o, + -BigInt('0x8000000000000000'), + BigInt('0x7fffffffffffffff') + ); + })), + (Buffer.prototype.writeFloatLE = function writeFloatLE(s, o, i) { + return writeFloat(this, s, o, !0, i); + }), + (Buffer.prototype.writeFloatBE = function writeFloatBE(s, o, i) { + return writeFloat(this, s, o, !1, i); + }), + (Buffer.prototype.writeDoubleLE = function writeDoubleLE(s, o, i) { + return writeDouble(this, s, o, !0, i); + }), + (Buffer.prototype.writeDoubleBE = function writeDoubleBE(s, o, i) { + return writeDouble(this, s, o, !1, i); + }), + (Buffer.prototype.copy = function copy(s, o, i, u) { + if (!Buffer.isBuffer(s)) throw new TypeError('argument should be a Buffer'); + if ( + (i || (i = 0), + u || 0 === u || (u = this.length), + o >= s.length && (o = s.length), + o || (o = 0), + u > 0 && u < i && (u = i), + u === i) + ) + return 0; + if (0 === s.length || 0 === this.length) return 0; + if (o < 0) throw new RangeError('targetStart out of bounds'); + if (i < 0 || i >= this.length) throw new RangeError('Index out of range'); + if (u < 0) throw new RangeError('sourceEnd out of bounds'); + (u > this.length && (u = this.length), + s.length - o < u - i && (u = s.length - o + i)); + const _ = u - i; + return ( + this === s && 'function' == typeof Uint8Array.prototype.copyWithin + ? this.copyWithin(o, i, u) + : Uint8Array.prototype.set.call(s, this.subarray(i, u), o), + _ + ); + }), + (Buffer.prototype.fill = function fill(s, o, i, u) { + if ('string' == typeof s) { + if ( + ('string' == typeof o + ? ((u = o), (o = 0), (i = this.length)) + : 'string' == typeof i && ((u = i), (i = this.length)), + void 0 !== u && 'string' != typeof u) + ) + throw new TypeError('encoding must be a string'); + if ('string' == typeof u && !Buffer.isEncoding(u)) + throw new TypeError('Unknown encoding: ' + u); + if (1 === s.length) { + const o = s.charCodeAt(0); + (('utf8' === u && o < 128) || 'latin1' === u) && (s = o); + } + } else 'number' == typeof s ? (s &= 255) : 'boolean' == typeof s && (s = Number(s)); + if (o < 0 || this.length < o || this.length < i) + throw new RangeError('Out of range index'); + if (i <= o) return this; + let _; + if ( + ((o >>>= 0), + (i = void 0 === i ? this.length : i >>> 0), + s || (s = 0), + 'number' == typeof s) + ) + for (_ = o; _ < i; ++_) this[_] = s; + else { + const w = Buffer.isBuffer(s) ? s : Buffer.from(s, u), + x = w.length; + if (0 === x) + throw new TypeError('The value "' + s + '" is invalid for argument "value"'); + for (_ = 0; _ < i - o; ++_) this[_ + o] = w[_ % x]; + } + return this; + })); + const j = {}; + function E(s, o, i) { + j[s] = class NodeError extends i { + constructor() { + (super(), + Object.defineProperty(this, 'message', { + value: o.apply(this, arguments), + writable: !0, + configurable: !0 + }), + (this.name = `${this.name} [${s}]`), + this.stack, + delete this.name); + } + get code() { + return s; + } + set code(s) { + Object.defineProperty(this, 'code', { + configurable: !0, + enumerable: !0, + value: s, + writable: !0 + }); + } + toString() { + return `${this.name} [${s}]: ${this.message}`; + } + }; + } + function addNumericalSeparator(s) { + let o = '', + i = s.length; + const u = '-' === s[0] ? 1 : 0; + for (; i >= u + 4; i -= 3) o = `_${s.slice(i - 3, i)}${o}`; + return `${s.slice(0, i)}${o}`; + } + function checkIntBI(s, o, i, u, _, w) { + if (s > i || s < o) { + const u = 'bigint' == typeof o ? 'n' : ''; + let _; + throw ( + (_ = + w > 3 + ? 0 === o || o === BigInt(0) + ? `>= 0${u} and < 2${u} ** ${8 * (w + 1)}${u}` + : `>= -(2${u} ** ${8 * (w + 1) - 1}${u}) and < 2 ** ${8 * (w + 1) - 1}${u}` + : `>= ${o}${u} and <= ${i}${u}`), + new j.ERR_OUT_OF_RANGE('value', _, s) + ); + } + !(function checkBounds(s, o, i) { + (validateNumber(o, 'offset'), + (void 0 !== s[o] && void 0 !== s[o + i]) || boundsError(o, s.length - (i + 1))); + })(u, _, w); + } + function validateNumber(s, o) { + if ('number' != typeof s) throw new j.ERR_INVALID_ARG_TYPE(o, 'number', s); + } + function boundsError(s, o, i) { + if (Math.floor(s) !== s) + throw (validateNumber(s, i), new j.ERR_OUT_OF_RANGE(i || 'offset', 'an integer', s)); + if (o < 0) throw new j.ERR_BUFFER_OUT_OF_BOUNDS(); + throw new j.ERR_OUT_OF_RANGE(i || 'offset', `>= ${i ? 1 : 0} and <= ${o}`, s); + } + (E( + 'ERR_BUFFER_OUT_OF_BOUNDS', + function (s) { + return s + ? `${s} is outside of buffer bounds` + : 'Attempt to access memory outside buffer bounds'; + }, + RangeError + ), + E( + 'ERR_INVALID_ARG_TYPE', + function (s, o) { + return `The "${s}" argument must be of type number. Received type ${typeof o}`; + }, + TypeError + ), + E( + 'ERR_OUT_OF_RANGE', + function (s, o, i) { + let u = `The value of "${s}" is out of range.`, + _ = i; + return ( + Number.isInteger(i) && Math.abs(i) > 2 ** 32 + ? (_ = addNumericalSeparator(String(i))) + : 'bigint' == typeof i && + ((_ = String(i)), + (i > BigInt(2) ** BigInt(32) || i < -(BigInt(2) ** BigInt(32))) && + (_ = addNumericalSeparator(_)), + (_ += 'n')), + (u += ` It must be ${o}. Received ${_}`), + u + ); + }, + RangeError + )); + const L = /[^+/0-9A-Za-z-_]/g; + function utf8ToBytes(s, o) { + let i; + o = o || 1 / 0; + const u = s.length; + let _ = null; + const w = []; + for (let x = 0; x < u; ++x) { + if (((i = s.charCodeAt(x)), i > 55295 && i < 57344)) { + if (!_) { + if (i > 56319) { + (o -= 3) > -1 && w.push(239, 191, 189); + continue; + } + if (x + 1 === u) { + (o -= 3) > -1 && w.push(239, 191, 189); + continue; + } + _ = i; + continue; + } + if (i < 56320) { + ((o -= 3) > -1 && w.push(239, 191, 189), (_ = i)); + continue; + } + i = 65536 + (((_ - 55296) << 10) | (i - 56320)); + } else _ && (o -= 3) > -1 && w.push(239, 191, 189); + if (((_ = null), i < 128)) { + if ((o -= 1) < 0) break; + w.push(i); + } else if (i < 2048) { + if ((o -= 2) < 0) break; + w.push((i >> 6) | 192, (63 & i) | 128); + } else if (i < 65536) { + if ((o -= 3) < 0) break; + w.push((i >> 12) | 224, ((i >> 6) & 63) | 128, (63 & i) | 128); + } else { + if (!(i < 1114112)) throw new Error('Invalid code point'); + if ((o -= 4) < 0) break; + w.push( + (i >> 18) | 240, + ((i >> 12) & 63) | 128, + ((i >> 6) & 63) | 128, + (63 & i) | 128 + ); + } + } + return w; + } + function base64ToBytes(s) { + return u.toByteArray( + (function base64clean(s) { + if ((s = (s = s.split('=')[0]).trim().replace(L, '')).length < 2) return ''; + for (; s.length % 4 != 0; ) s += '='; + return s; + })(s) + ); + } + function blitBuffer(s, o, i, u) { + let _; + for (_ = 0; _ < u && !(_ + i >= o.length || _ >= s.length); ++_) o[_ + i] = s[_]; + return _; + } + function isInstance(s, o) { + return ( + s instanceof o || + (null != s && + null != s.constructor && + null != s.constructor.name && + s.constructor.name === o.name) + ); + } + function numberIsNaN(s) { + return s != s; + } + const B = (function () { + const s = '0123456789abcdef', + o = new Array(256); + for (let i = 0; i < 16; ++i) { + const u = 16 * i; + for (let _ = 0; _ < 16; ++_) o[u + _] = s[i] + s[_]; + } + return o; + })(); + function defineBigIntMethod(s) { + return 'undefined' == typeof BigInt ? BufferBigIntNotDefined : s; + } + function BufferBigIntNotDefined() { + throw new Error('BigInt not supported'); + } + }, + 17965: (s, o, i) => { + 'use strict'; + var u = i(16426), + _ = { 'text/plain': 'Text', 'text/html': 'Url', default: 'Text' }; + s.exports = function copy(s, o) { + var i, + w, + x, + C, + j, + L, + B = !1; + (o || (o = {}), (i = o.debug || !1)); + try { + if ( + ((x = u()), + (C = document.createRange()), + (j = document.getSelection()), + ((L = document.createElement('span')).textContent = s), + (L.ariaHidden = 'true'), + (L.style.all = 'unset'), + (L.style.position = 'fixed'), + (L.style.top = 0), + (L.style.clip = 'rect(0, 0, 0, 0)'), + (L.style.whiteSpace = 'pre'), + (L.style.webkitUserSelect = 'text'), + (L.style.MozUserSelect = 'text'), + (L.style.msUserSelect = 'text'), + (L.style.userSelect = 'text'), + L.addEventListener('copy', function (u) { + if ((u.stopPropagation(), o.format)) + if ((u.preventDefault(), void 0 === u.clipboardData)) { + (i && console.warn('unable to use e.clipboardData'), + i && console.warn('trying IE specific stuff'), + window.clipboardData.clearData()); + var w = _[o.format] || _.default; + window.clipboardData.setData(w, s); + } else (u.clipboardData.clearData(), u.clipboardData.setData(o.format, s)); + o.onCopy && (u.preventDefault(), o.onCopy(u.clipboardData)); + }), + document.body.appendChild(L), + C.selectNodeContents(L), + j.addRange(C), + !document.execCommand('copy')) + ) + throw new Error('copy command was unsuccessful'); + B = !0; + } catch (u) { + (i && console.error('unable to copy using execCommand: ', u), + i && console.warn('trying IE specific stuff')); + try { + (window.clipboardData.setData(o.format || 'text', s), + o.onCopy && o.onCopy(window.clipboardData), + (B = !0)); + } catch (u) { + (i && console.error('unable to copy using clipboardData: ', u), + i && console.error('falling back to prompt'), + (w = (function format(s) { + var o = (/mac os x/i.test(navigator.userAgent) ? '⌘' : 'Ctrl') + '+C'; + return s.replace(/#{\s*key\s*}/g, o); + })('message' in o ? o.message : 'Copy to clipboard: #{key}, Enter')), + window.prompt(w, s)); + } + } finally { + (j && ('function' == typeof j.removeRange ? j.removeRange(C) : j.removeAllRanges()), + L && document.body.removeChild(L), + x()); + } + return B; + }; + }, + 2205: function (s, o, i) { + var u; + ((u = void 0 !== i.g ? i.g : this), + (s.exports = (function (s) { + if (s.CSS && s.CSS.escape) return s.CSS.escape; + var cssEscape = function (s) { + if (0 == arguments.length) + throw new TypeError('`CSS.escape` requires an argument.'); + for ( + var o, i = String(s), u = i.length, _ = -1, w = '', x = i.charCodeAt(0); + ++_ < u; + ) + 0 != (o = i.charCodeAt(_)) + ? (w += + (o >= 1 && o <= 31) || + 127 == o || + (0 == _ && o >= 48 && o <= 57) || + (1 == _ && o >= 48 && o <= 57 && 45 == x) + ? '\\' + o.toString(16) + ' ' + : (0 == _ && 1 == u && 45 == o) || + !( + o >= 128 || + 45 == o || + 95 == o || + (o >= 48 && o <= 57) || + (o >= 65 && o <= 90) || + (o >= 97 && o <= 122) + ) + ? '\\' + i.charAt(_) + : i.charAt(_)) + : (w += '�'); + return w; + }; + return (s.CSS || (s.CSS = {}), (s.CSS.escape = cssEscape), cssEscape); + })(u))); + }, + 81919: (s, o, i) => { + 'use strict'; + var u = i(48287).Buffer; + function isSpecificValue(s) { + return s instanceof u || s instanceof Date || s instanceof RegExp; + } + function cloneSpecificValue(s) { + if (s instanceof u) { + var o = u.alloc ? u.alloc(s.length) : new u(s.length); + return (s.copy(o), o); + } + if (s instanceof Date) return new Date(s.getTime()); + if (s instanceof RegExp) return new RegExp(s); + throw new Error('Unexpected situation'); + } + function deepCloneArray(s) { + var o = []; + return ( + s.forEach(function (s, i) { + 'object' == typeof s && null !== s + ? Array.isArray(s) + ? (o[i] = deepCloneArray(s)) + : isSpecificValue(s) + ? (o[i] = cloneSpecificValue(s)) + : (o[i] = _({}, s)) + : (o[i] = s); + }), + o + ); + } + function safeGetProperty(s, o) { + return '__proto__' === o ? void 0 : s[o]; + } + var _ = (s.exports = function () { + if (arguments.length < 1 || 'object' != typeof arguments[0]) return !1; + if (arguments.length < 2) return arguments[0]; + var s, + o, + i = arguments[0]; + return ( + Array.prototype.slice.call(arguments, 1).forEach(function (u) { + 'object' != typeof u || + null === u || + Array.isArray(u) || + Object.keys(u).forEach(function (w) { + return ( + (o = safeGetProperty(i, w)), + (s = safeGetProperty(u, w)) === i + ? void 0 + : 'object' != typeof s || null === s + ? void (i[w] = s) + : Array.isArray(s) + ? void (i[w] = deepCloneArray(s)) + : isSpecificValue(s) + ? void (i[w] = cloneSpecificValue(s)) + : 'object' != typeof o || null === o || Array.isArray(o) + ? void (i[w] = _({}, s)) + : void (i[w] = _(o, s)) + ); + }); + }), + i + ); + }); + }, + 14744: (s) => { + 'use strict'; + var o = function isMergeableObject(s) { + return ( + (function isNonNullObject(s) { + return !!s && 'object' == typeof s; + })(s) && + !(function isSpecial(s) { + var o = Object.prototype.toString.call(s); + return ( + '[object RegExp]' === o || + '[object Date]' === o || + (function isReactElement(s) { + return s.$$typeof === i; + })(s) + ); + })(s) + ); + }; + var i = 'function' == typeof Symbol && Symbol.for ? Symbol.for('react.element') : 60103; + function cloneUnlessOtherwiseSpecified(s, o) { + return !1 !== o.clone && o.isMergeableObject(s) + ? deepmerge( + (function emptyTarget(s) { + return Array.isArray(s) ? [] : {}; + })(s), + s, + o + ) + : s; + } + function defaultArrayMerge(s, o, i) { + return s.concat(o).map(function (s) { + return cloneUnlessOtherwiseSpecified(s, i); + }); + } + function getKeys(s) { + return Object.keys(s).concat( + (function getEnumerableOwnPropertySymbols(s) { + return Object.getOwnPropertySymbols + ? Object.getOwnPropertySymbols(s).filter(function (o) { + return Object.propertyIsEnumerable.call(s, o); + }) + : []; + })(s) + ); + } + function propertyIsOnObject(s, o) { + try { + return o in s; + } catch (s) { + return !1; + } + } + function mergeObject(s, o, i) { + var u = {}; + return ( + i.isMergeableObject(s) && + getKeys(s).forEach(function (o) { + u[o] = cloneUnlessOtherwiseSpecified(s[o], i); + }), + getKeys(o).forEach(function (_) { + (function propertyIsUnsafe(s, o) { + return ( + propertyIsOnObject(s, o) && + !(Object.hasOwnProperty.call(s, o) && Object.propertyIsEnumerable.call(s, o)) + ); + })(s, _) || + (propertyIsOnObject(s, _) && i.isMergeableObject(o[_]) + ? (u[_] = (function getMergeFunction(s, o) { + if (!o.customMerge) return deepmerge; + var i = o.customMerge(s); + return 'function' == typeof i ? i : deepmerge; + })(_, i)(s[_], o[_], i)) + : (u[_] = cloneUnlessOtherwiseSpecified(o[_], i))); + }), + u + ); + } + function deepmerge(s, i, u) { + (((u = u || {}).arrayMerge = u.arrayMerge || defaultArrayMerge), + (u.isMergeableObject = u.isMergeableObject || o), + (u.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified)); + var _ = Array.isArray(i); + return _ === Array.isArray(s) + ? _ + ? u.arrayMerge(s, i, u) + : mergeObject(s, i, u) + : cloneUnlessOtherwiseSpecified(i, u); + } + deepmerge.all = function deepmergeAll(s, o) { + if (!Array.isArray(s)) throw new Error('first argument should be an array'); + return s.reduce(function (s, i) { + return deepmerge(s, i, o); + }, {}); + }; + var u = deepmerge; + s.exports = u; + }, + 42838: function (s) { + s.exports = (function () { + 'use strict'; + const { + entries: s, + setPrototypeOf: o, + isFrozen: i, + getPrototypeOf: u, + getOwnPropertyDescriptor: _ + } = Object; + let { freeze: w, seal: x, create: C } = Object, + { apply: j, construct: L } = 'undefined' != typeof Reflect && Reflect; + (w || + (w = function freeze(s) { + return s; + }), + x || + (x = function seal(s) { + return s; + }), + j || + (j = function apply(s, o, i) { + return s.apply(o, i); + }), + L || + (L = function construct(s, o) { + return new s(...o); + })); + const B = unapply(Array.prototype.forEach), + $ = unapply(Array.prototype.pop), + V = unapply(Array.prototype.push), + U = unapply(String.prototype.toLowerCase), + z = unapply(String.prototype.toString), + Y = unapply(String.prototype.match), + Z = unapply(String.prototype.replace), + ee = unapply(String.prototype.indexOf), + ie = unapply(String.prototype.trim), + ae = unapply(Object.prototype.hasOwnProperty), + le = unapply(RegExp.prototype.test), + ce = unconstruct(TypeError); + function unapply(s) { + return function (o) { + for (var i = arguments.length, u = new Array(i > 1 ? i - 1 : 0), _ = 1; _ < i; _++) + u[_ - 1] = arguments[_]; + return j(s, o, u); + }; + } + function unconstruct(s) { + return function () { + for (var o = arguments.length, i = new Array(o), u = 0; u < o; u++) + i[u] = arguments[u]; + return L(s, i); + }; + } + function addToSet(s, u) { + let _ = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : U; + o && o(s, null); + let w = u.length; + for (; w--; ) { + let o = u[w]; + if ('string' == typeof o) { + const s = _(o); + s !== o && (i(u) || (u[w] = s), (o = s)); + } + s[o] = !0; + } + return s; + } + function cleanArray(s) { + for (let o = 0; o < s.length; o++) ae(s, o) || (s[o] = null); + return s; + } + function clone(o) { + const i = C(null); + for (const [u, _] of s(o)) + ae(o, u) && + (Array.isArray(_) + ? (i[u] = cleanArray(_)) + : _ && 'object' == typeof _ && _.constructor === Object + ? (i[u] = clone(_)) + : (i[u] = _)); + return i; + } + function lookupGetter(s, o) { + for (; null !== s; ) { + const i = _(s, o); + if (i) { + if (i.get) return unapply(i.get); + if ('function' == typeof i.value) return unapply(i.value); + } + s = u(s); + } + function fallbackValue() { + return null; + } + return fallbackValue; + } + const pe = w([ + 'a', + 'abbr', + 'acronym', + 'address', + 'area', + 'article', + 'aside', + 'audio', + 'b', + 'bdi', + 'bdo', + 'big', + 'blink', + 'blockquote', + 'body', + 'br', + 'button', + 'canvas', + 'caption', + 'center', + 'cite', + 'code', + 'col', + 'colgroup', + 'content', + 'data', + 'datalist', + 'dd', + 'decorator', + 'del', + 'details', + 'dfn', + 'dialog', + 'dir', + 'div', + 'dl', + 'dt', + 'element', + 'em', + 'fieldset', + 'figcaption', + 'figure', + 'font', + 'footer', + 'form', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'head', + 'header', + 'hgroup', + 'hr', + 'html', + 'i', + 'img', + 'input', + 'ins', + 'kbd', + 'label', + 'legend', + 'li', + 'main', + 'map', + 'mark', + 'marquee', + 'menu', + 'menuitem', + 'meter', + 'nav', + 'nobr', + 'ol', + 'optgroup', + 'option', + 'output', + 'p', + 'picture', + 'pre', + 'progress', + 'q', + 'rp', + 'rt', + 'ruby', + 's', + 'samp', + 'section', + 'select', + 'shadow', + 'small', + 'source', + 'spacer', + 'span', + 'strike', + 'strong', + 'style', + 'sub', + 'summary', + 'sup', + 'table', + 'tbody', + 'td', + 'template', + 'textarea', + 'tfoot', + 'th', + 'thead', + 'time', + 'tr', + 'track', + 'tt', + 'u', + 'ul', + 'var', + 'video', + 'wbr' + ]), + de = w([ + 'svg', + 'a', + 'altglyph', + 'altglyphdef', + 'altglyphitem', + 'animatecolor', + 'animatemotion', + 'animatetransform', + 'circle', + 'clippath', + 'defs', + 'desc', + 'ellipse', + 'filter', + 'font', + 'g', + 'glyph', + 'glyphref', + 'hkern', + 'image', + 'line', + 'lineargradient', + 'marker', + 'mask', + 'metadata', + 'mpath', + 'path', + 'pattern', + 'polygon', + 'polyline', + 'radialgradient', + 'rect', + 'stop', + 'style', + 'switch', + 'symbol', + 'text', + 'textpath', + 'title', + 'tref', + 'tspan', + 'view', + 'vkern' + ]), + fe = w([ + 'feBlend', + 'feColorMatrix', + 'feComponentTransfer', + 'feComposite', + 'feConvolveMatrix', + 'feDiffuseLighting', + 'feDisplacementMap', + 'feDistantLight', + 'feDropShadow', + 'feFlood', + 'feFuncA', + 'feFuncB', + 'feFuncG', + 'feFuncR', + 'feGaussianBlur', + 'feImage', + 'feMerge', + 'feMergeNode', + 'feMorphology', + 'feOffset', + 'fePointLight', + 'feSpecularLighting', + 'feSpotLight', + 'feTile', + 'feTurbulence' + ]), + ye = w([ + 'animate', + 'color-profile', + 'cursor', + 'discard', + 'font-face', + 'font-face-format', + 'font-face-name', + 'font-face-src', + 'font-face-uri', + 'foreignobject', + 'hatch', + 'hatchpath', + 'mesh', + 'meshgradient', + 'meshpatch', + 'meshrow', + 'missing-glyph', + 'script', + 'set', + 'solidcolor', + 'unknown', + 'use' + ]), + be = w([ + 'math', + 'menclose', + 'merror', + 'mfenced', + 'mfrac', + 'mglyph', + 'mi', + 'mlabeledtr', + 'mmultiscripts', + 'mn', + 'mo', + 'mover', + 'mpadded', + 'mphantom', + 'mroot', + 'mrow', + 'ms', + 'mspace', + 'msqrt', + 'mstyle', + 'msub', + 'msup', + 'msubsup', + 'mtable', + 'mtd', + 'mtext', + 'mtr', + 'munder', + 'munderover', + 'mprescripts' + ]), + _e = w([ + 'maction', + 'maligngroup', + 'malignmark', + 'mlongdiv', + 'mscarries', + 'mscarry', + 'msgroup', + 'mstack', + 'msline', + 'msrow', + 'semantics', + 'annotation', + 'annotation-xml', + 'mprescripts', + 'none' + ]), + we = w(['#text']), + Se = w([ + 'accept', + 'action', + 'align', + 'alt', + 'autocapitalize', + 'autocomplete', + 'autopictureinpicture', + 'autoplay', + 'background', + 'bgcolor', + 'border', + 'capture', + 'cellpadding', + 'cellspacing', + 'checked', + 'cite', + 'class', + 'clear', + 'color', + 'cols', + 'colspan', + 'controls', + 'controlslist', + 'coords', + 'crossorigin', + 'datetime', + 'decoding', + 'default', + 'dir', + 'disabled', + 'disablepictureinpicture', + 'disableremoteplayback', + 'download', + 'draggable', + 'enctype', + 'enterkeyhint', + 'face', + 'for', + 'headers', + 'height', + 'hidden', + 'high', + 'href', + 'hreflang', + 'id', + 'inputmode', + 'integrity', + 'ismap', + 'kind', + 'label', + 'lang', + 'list', + 'loading', + 'loop', + 'low', + 'max', + 'maxlength', + 'media', + 'method', + 'min', + 'minlength', + 'multiple', + 'muted', + 'name', + 'nonce', + 'noshade', + 'novalidate', + 'nowrap', + 'open', + 'optimum', + 'pattern', + 'placeholder', + 'playsinline', + 'popover', + 'popovertarget', + 'popovertargetaction', + 'poster', + 'preload', + 'pubdate', + 'radiogroup', + 'readonly', + 'rel', + 'required', + 'rev', + 'reversed', + 'role', + 'rows', + 'rowspan', + 'spellcheck', + 'scope', + 'selected', + 'shape', + 'size', + 'sizes', + 'span', + 'srclang', + 'start', + 'src', + 'srcset', + 'step', + 'style', + 'summary', + 'tabindex', + 'title', + 'translate', + 'type', + 'usemap', + 'valign', + 'value', + 'width', + 'wrap', + 'xmlns', + 'slot' + ]), + xe = w([ + 'accent-height', + 'accumulate', + 'additive', + 'alignment-baseline', + 'ascent', + 'attributename', + 'attributetype', + 'azimuth', + 'basefrequency', + 'baseline-shift', + 'begin', + 'bias', + 'by', + 'class', + 'clip', + 'clippathunits', + 'clip-path', + 'clip-rule', + 'color', + 'color-interpolation', + 'color-interpolation-filters', + 'color-profile', + 'color-rendering', + 'cx', + 'cy', + 'd', + 'dx', + 'dy', + 'diffuseconstant', + 'direction', + 'display', + 'divisor', + 'dur', + 'edgemode', + 'elevation', + 'end', + 'fill', + 'fill-opacity', + 'fill-rule', + 'filter', + 'filterunits', + 'flood-color', + 'flood-opacity', + 'font-family', + 'font-size', + 'font-size-adjust', + 'font-stretch', + 'font-style', + 'font-variant', + 'font-weight', + 'fx', + 'fy', + 'g1', + 'g2', + 'glyph-name', + 'glyphref', + 'gradientunits', + 'gradienttransform', + 'height', + 'href', + 'id', + 'image-rendering', + 'in', + 'in2', + 'k', + 'k1', + 'k2', + 'k3', + 'k4', + 'kerning', + 'keypoints', + 'keysplines', + 'keytimes', + 'lang', + 'lengthadjust', + 'letter-spacing', + 'kernelmatrix', + 'kernelunitlength', + 'lighting-color', + 'local', + 'marker-end', + 'marker-mid', + 'marker-start', + 'markerheight', + 'markerunits', + 'markerwidth', + 'maskcontentunits', + 'maskunits', + 'max', + 'mask', + 'media', + 'method', + 'mode', + 'min', + 'name', + 'numoctaves', + 'offset', + 'operator', + 'opacity', + 'order', + 'orient', + 'orientation', + 'origin', + 'overflow', + 'paint-order', + 'path', + 'pathlength', + 'patterncontentunits', + 'patterntransform', + 'patternunits', + 'points', + 'preservealpha', + 'preserveaspectratio', + 'primitiveunits', + 'r', + 'rx', + 'ry', + 'radius', + 'refx', + 'refy', + 'repeatcount', + 'repeatdur', + 'restart', + 'result', + 'rotate', + 'scale', + 'seed', + 'shape-rendering', + 'specularconstant', + 'specularexponent', + 'spreadmethod', + 'startoffset', + 'stddeviation', + 'stitchtiles', + 'stop-color', + 'stop-opacity', + 'stroke-dasharray', + 'stroke-dashoffset', + 'stroke-linecap', + 'stroke-linejoin', + 'stroke-miterlimit', + 'stroke-opacity', + 'stroke', + 'stroke-width', + 'style', + 'surfacescale', + 'systemlanguage', + 'tabindex', + 'targetx', + 'targety', + 'transform', + 'transform-origin', + 'text-anchor', + 'text-decoration', + 'text-rendering', + 'textlength', + 'type', + 'u1', + 'u2', + 'unicode', + 'values', + 'viewbox', + 'visibility', + 'version', + 'vert-adv-y', + 'vert-origin-x', + 'vert-origin-y', + 'width', + 'word-spacing', + 'wrap', + 'writing-mode', + 'xchannelselector', + 'ychannelselector', + 'x', + 'x1', + 'x2', + 'xmlns', + 'y', + 'y1', + 'y2', + 'z', + 'zoomandpan' + ]), + Pe = w([ + 'accent', + 'accentunder', + 'align', + 'bevelled', + 'close', + 'columnsalign', + 'columnlines', + 'columnspan', + 'denomalign', + 'depth', + 'dir', + 'display', + 'displaystyle', + 'encoding', + 'fence', + 'frame', + 'height', + 'href', + 'id', + 'largeop', + 'length', + 'linethickness', + 'lspace', + 'lquote', + 'mathbackground', + 'mathcolor', + 'mathsize', + 'mathvariant', + 'maxsize', + 'minsize', + 'movablelimits', + 'notation', + 'numalign', + 'open', + 'rowalign', + 'rowlines', + 'rowspacing', + 'rowspan', + 'rspace', + 'rquote', + 'scriptlevel', + 'scriptminsize', + 'scriptsizemultiplier', + 'selection', + 'separator', + 'separators', + 'stretchy', + 'subscriptshift', + 'supscriptshift', + 'symmetric', + 'voffset', + 'width', + 'xmlns' + ]), + Te = w(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']), + Re = x(/\{\{[\w\W]*|[\w\W]*\}\}/gm), + qe = x(/<%[\w\W]*|[\w\W]*%>/gm), + $e = x(/\${[\w\W]*}/gm), + ze = x(/^data-[\-\w.\u00B7-\uFFFF]/), + We = x(/^aria-[\-\w]+$/), + He = x( + /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i + ), + Ye = x(/^(?:\w+script|data):/i), + Xe = x(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g), + Qe = x(/^html$/i), + et = x(/^[a-z][.\w]*(-[.\w]+)+$/i); + var tt = Object.freeze({ + __proto__: null, + MUSTACHE_EXPR: Re, + ERB_EXPR: qe, + TMPLIT_EXPR: $e, + DATA_ATTR: ze, + ARIA_ATTR: We, + IS_ALLOWED_URI: He, + IS_SCRIPT_OR_DATA: Ye, + ATTR_WHITESPACE: Xe, + DOCTYPE_NAME: Qe, + CUSTOM_ELEMENT: et + }); + const rt = { + element: 1, + attribute: 2, + text: 3, + cdataSection: 4, + entityReference: 5, + entityNode: 6, + progressingInstruction: 7, + comment: 8, + document: 9, + documentType: 10, + documentFragment: 11, + notation: 12 + }, + nt = function getGlobal() { + return 'undefined' == typeof window ? null : window; + }, + st = function _createTrustedTypesPolicy(s, o) { + if ('object' != typeof s || 'function' != typeof s.createPolicy) return null; + let i = null; + const u = 'data-tt-policy-suffix'; + o && o.hasAttribute(u) && (i = o.getAttribute(u)); + const _ = 'dompurify' + (i ? '#' + i : ''); + try { + return s.createPolicy(_, { createHTML: (s) => s, createScriptURL: (s) => s }); + } catch (s) { + return ( + console.warn('TrustedTypes policy ' + _ + ' could not be created.'), + null + ); + } + }; + function createDOMPurify() { + let o = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : nt(); + const DOMPurify = (s) => createDOMPurify(s); + if ( + ((DOMPurify.version = '3.1.6'), + (DOMPurify.removed = []), + !o || !o.document || o.document.nodeType !== rt.document) + ) + return ((DOMPurify.isSupported = !1), DOMPurify); + let { document: i } = o; + const u = i, + _ = u.currentScript, + { + DocumentFragment: x, + HTMLTemplateElement: j, + Node: L, + Element: Re, + NodeFilter: qe, + NamedNodeMap: $e = o.NamedNodeMap || o.MozNamedAttrMap, + HTMLFormElement: ze, + DOMParser: We, + trustedTypes: Ye + } = o, + Xe = Re.prototype, + et = lookupGetter(Xe, 'cloneNode'), + ot = lookupGetter(Xe, 'remove'), + it = lookupGetter(Xe, 'nextSibling'), + at = lookupGetter(Xe, 'childNodes'), + lt = lookupGetter(Xe, 'parentNode'); + if ('function' == typeof j) { + const s = i.createElement('template'); + s.content && s.content.ownerDocument && (i = s.content.ownerDocument); + } + let ct, + ut = ''; + const { + implementation: pt, + createNodeIterator: ht, + createDocumentFragment: dt, + getElementsByTagName: mt + } = i, + { importNode: gt } = u; + let yt = {}; + DOMPurify.isSupported = + 'function' == typeof s && + 'function' == typeof lt && + pt && + void 0 !== pt.createHTMLDocument; + const { + MUSTACHE_EXPR: vt, + ERB_EXPR: bt, + TMPLIT_EXPR: _t, + DATA_ATTR: Et, + ARIA_ATTR: wt, + IS_SCRIPT_OR_DATA: St, + ATTR_WHITESPACE: xt, + CUSTOM_ELEMENT: kt + } = tt; + let { IS_ALLOWED_URI: Ct } = tt, + Ot = null; + const At = addToSet({}, [...pe, ...de, ...fe, ...be, ...we]); + let jt = null; + const It = addToSet({}, [...Se, ...xe, ...Pe, ...Te]); + let Pt = Object.seal( + C(null, { + tagNameCheck: { writable: !0, configurable: !1, enumerable: !0, value: null }, + attributeNameCheck: { + writable: !0, + configurable: !1, + enumerable: !0, + value: null + }, + allowCustomizedBuiltInElements: { + writable: !0, + configurable: !1, + enumerable: !0, + value: !1 + } + }) + ), + Mt = null, + Tt = null, + Nt = !0, + Rt = !0, + Dt = !1, + Lt = !0, + Bt = !1, + Ft = !0, + qt = !1, + $t = !1, + Vt = !1, + Ut = !1, + zt = !1, + Wt = !1, + Kt = !0, + Ht = !1; + const Jt = 'user-content-'; + let Gt = !0, + Yt = !1, + Xt = {}, + Zt = null; + const Qt = addToSet({}, [ + 'annotation-xml', + 'audio', + 'colgroup', + 'desc', + 'foreignobject', + 'head', + 'iframe', + 'math', + 'mi', + 'mn', + 'mo', + 'ms', + 'mtext', + 'noembed', + 'noframes', + 'noscript', + 'plaintext', + 'script', + 'style', + 'svg', + 'template', + 'thead', + 'title', + 'video', + 'xmp' + ]); + let er = null; + const tr = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']); + let rr = null; + const nr = addToSet({}, [ + 'alt', + 'class', + 'for', + 'id', + 'label', + 'name', + 'pattern', + 'placeholder', + 'role', + 'summary', + 'title', + 'value', + 'style', + 'xmlns' + ]), + sr = 'http://www.w3.org/1998/Math/MathML', + ir = 'http://www.w3.org/2000/svg', + ar = 'http://www.w3.org/1999/xhtml'; + let lr = ar, + cr = !1, + ur = null; + const pr = addToSet({}, [sr, ir, ar], z); + let dr = null; + const fr = ['application/xhtml+xml', 'text/html'], + mr = 'text/html'; + let gr = null, + yr = null; + const vr = i.createElement('form'), + br = function isRegexOrFunction(s) { + return s instanceof RegExp || s instanceof Function; + }, + _r = function _parseConfig() { + let s = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}; + if (!yr || yr !== s) { + if ( + ((s && 'object' == typeof s) || (s = {}), + (s = clone(s)), + (dr = -1 === fr.indexOf(s.PARSER_MEDIA_TYPE) ? mr : s.PARSER_MEDIA_TYPE), + (gr = 'application/xhtml+xml' === dr ? z : U), + (Ot = ae(s, 'ALLOWED_TAGS') ? addToSet({}, s.ALLOWED_TAGS, gr) : At), + (jt = ae(s, 'ALLOWED_ATTR') ? addToSet({}, s.ALLOWED_ATTR, gr) : It), + (ur = ae(s, 'ALLOWED_NAMESPACES') + ? addToSet({}, s.ALLOWED_NAMESPACES, z) + : pr), + (rr = ae(s, 'ADD_URI_SAFE_ATTR') + ? addToSet(clone(nr), s.ADD_URI_SAFE_ATTR, gr) + : nr), + (er = ae(s, 'ADD_DATA_URI_TAGS') + ? addToSet(clone(tr), s.ADD_DATA_URI_TAGS, gr) + : tr), + (Zt = ae(s, 'FORBID_CONTENTS') ? addToSet({}, s.FORBID_CONTENTS, gr) : Qt), + (Mt = ae(s, 'FORBID_TAGS') ? addToSet({}, s.FORBID_TAGS, gr) : {}), + (Tt = ae(s, 'FORBID_ATTR') ? addToSet({}, s.FORBID_ATTR, gr) : {}), + (Xt = !!ae(s, 'USE_PROFILES') && s.USE_PROFILES), + (Nt = !1 !== s.ALLOW_ARIA_ATTR), + (Rt = !1 !== s.ALLOW_DATA_ATTR), + (Dt = s.ALLOW_UNKNOWN_PROTOCOLS || !1), + (Lt = !1 !== s.ALLOW_SELF_CLOSE_IN_ATTR), + (Bt = s.SAFE_FOR_TEMPLATES || !1), + (Ft = !1 !== s.SAFE_FOR_XML), + (qt = s.WHOLE_DOCUMENT || !1), + (Ut = s.RETURN_DOM || !1), + (zt = s.RETURN_DOM_FRAGMENT || !1), + (Wt = s.RETURN_TRUSTED_TYPE || !1), + (Vt = s.FORCE_BODY || !1), + (Kt = !1 !== s.SANITIZE_DOM), + (Ht = s.SANITIZE_NAMED_PROPS || !1), + (Gt = !1 !== s.KEEP_CONTENT), + (Yt = s.IN_PLACE || !1), + (Ct = s.ALLOWED_URI_REGEXP || He), + (lr = s.NAMESPACE || ar), + (Pt = s.CUSTOM_ELEMENT_HANDLING || {}), + s.CUSTOM_ELEMENT_HANDLING && + br(s.CUSTOM_ELEMENT_HANDLING.tagNameCheck) && + (Pt.tagNameCheck = s.CUSTOM_ELEMENT_HANDLING.tagNameCheck), + s.CUSTOM_ELEMENT_HANDLING && + br(s.CUSTOM_ELEMENT_HANDLING.attributeNameCheck) && + (Pt.attributeNameCheck = s.CUSTOM_ELEMENT_HANDLING.attributeNameCheck), + s.CUSTOM_ELEMENT_HANDLING && + 'boolean' == + typeof s.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && + (Pt.allowCustomizedBuiltInElements = + s.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements), + Bt && (Rt = !1), + zt && (Ut = !0), + Xt && + ((Ot = addToSet({}, we)), + (jt = []), + !0 === Xt.html && (addToSet(Ot, pe), addToSet(jt, Se)), + !0 === Xt.svg && (addToSet(Ot, de), addToSet(jt, xe), addToSet(jt, Te)), + !0 === Xt.svgFilters && + (addToSet(Ot, fe), addToSet(jt, xe), addToSet(jt, Te)), + !0 === Xt.mathMl && (addToSet(Ot, be), addToSet(jt, Pe), addToSet(jt, Te))), + s.ADD_TAGS && (Ot === At && (Ot = clone(Ot)), addToSet(Ot, s.ADD_TAGS, gr)), + s.ADD_ATTR && (jt === It && (jt = clone(jt)), addToSet(jt, s.ADD_ATTR, gr)), + s.ADD_URI_SAFE_ATTR && addToSet(rr, s.ADD_URI_SAFE_ATTR, gr), + s.FORBID_CONTENTS && + (Zt === Qt && (Zt = clone(Zt)), addToSet(Zt, s.FORBID_CONTENTS, gr)), + Gt && (Ot['#text'] = !0), + qt && addToSet(Ot, ['html', 'head', 'body']), + Ot.table && (addToSet(Ot, ['tbody']), delete Mt.tbody), + s.TRUSTED_TYPES_POLICY) + ) { + if ('function' != typeof s.TRUSTED_TYPES_POLICY.createHTML) + throw ce( + 'TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.' + ); + if ('function' != typeof s.TRUSTED_TYPES_POLICY.createScriptURL) + throw ce( + 'TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.' + ); + ((ct = s.TRUSTED_TYPES_POLICY), (ut = ct.createHTML(''))); + } else + (void 0 === ct && (ct = st(Ye, _)), + null !== ct && 'string' == typeof ut && (ut = ct.createHTML(''))); + (w && w(s), (yr = s)); + } + }, + Er = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']), + wr = addToSet({}, ['foreignobject', 'annotation-xml']), + Sr = addToSet({}, ['title', 'style', 'font', 'a', 'script']), + xr = addToSet({}, [...de, ...fe, ...ye]), + kr = addToSet({}, [...be, ..._e]), + Cr = function _checkValidNamespace(s) { + let o = lt(s); + (o && o.tagName) || (o = { namespaceURI: lr, tagName: 'template' }); + const i = U(s.tagName), + u = U(o.tagName); + return ( + !!ur[s.namespaceURI] && + (s.namespaceURI === ir + ? o.namespaceURI === ar + ? 'svg' === i + : o.namespaceURI === sr + ? 'svg' === i && ('annotation-xml' === u || Er[u]) + : Boolean(xr[i]) + : s.namespaceURI === sr + ? o.namespaceURI === ar + ? 'math' === i + : o.namespaceURI === ir + ? 'math' === i && wr[u] + : Boolean(kr[i]) + : s.namespaceURI === ar + ? !(o.namespaceURI === ir && !wr[u]) && + !(o.namespaceURI === sr && !Er[u]) && + !kr[i] && + (Sr[i] || !xr[i]) + : !('application/xhtml+xml' !== dr || !ur[s.namespaceURI])) + ); + }, + Or = function _forceRemove(s) { + V(DOMPurify.removed, { element: s }); + try { + lt(s).removeChild(s); + } catch (o) { + ot(s); + } + }, + Ar = function _removeAttribute(s, o) { + try { + V(DOMPurify.removed, { attribute: o.getAttributeNode(s), from: o }); + } catch (s) { + V(DOMPurify.removed, { attribute: null, from: o }); + } + if ((o.removeAttribute(s), 'is' === s && !jt[s])) + if (Ut || zt) + try { + Or(o); + } catch (s) {} + else + try { + o.setAttribute(s, ''); + } catch (s) {} + }, + jr = function _initDocument(s) { + let o = null, + u = null; + if (Vt) s = '' + s; + else { + const o = Y(s, /^[\r\n\t ]+/); + u = o && o[0]; + } + 'application/xhtml+xml' === dr && + lr === ar && + (s = + '' + + s + + ''); + const _ = ct ? ct.createHTML(s) : s; + if (lr === ar) + try { + o = new We().parseFromString(_, dr); + } catch (s) {} + if (!o || !o.documentElement) { + o = pt.createDocument(lr, 'template', null); + try { + o.documentElement.innerHTML = cr ? ut : _; + } catch (s) {} + } + const w = o.body || o.documentElement; + return ( + s && u && w.insertBefore(i.createTextNode(u), w.childNodes[0] || null), + lr === ar ? mt.call(o, qt ? 'html' : 'body')[0] : qt ? o.documentElement : w + ); + }, + Ir = function _createNodeIterator(s) { + return ht.call( + s.ownerDocument || s, + s, + qe.SHOW_ELEMENT | + qe.SHOW_COMMENT | + qe.SHOW_TEXT | + qe.SHOW_PROCESSING_INSTRUCTION | + qe.SHOW_CDATA_SECTION, + null + ); + }, + Pr = function _isClobbered(s) { + return ( + s instanceof ze && + ('string' != typeof s.nodeName || + 'string' != typeof s.textContent || + 'function' != typeof s.removeChild || + !(s.attributes instanceof $e) || + 'function' != typeof s.removeAttribute || + 'function' != typeof s.setAttribute || + 'string' != typeof s.namespaceURI || + 'function' != typeof s.insertBefore || + 'function' != typeof s.hasChildNodes) + ); + }, + Mr = function _isNode(s) { + return 'function' == typeof L && s instanceof L; + }, + Tr = function _executeHook(s, o, i) { + yt[s] && + B(yt[s], (s) => { + s.call(DOMPurify, o, i, yr); + }); + }, + Nr = function _sanitizeElements(s) { + let o = null; + if ((Tr('beforeSanitizeElements', s, null), Pr(s))) return (Or(s), !0); + const i = gr(s.nodeName); + if ( + (Tr('uponSanitizeElement', s, { tagName: i, allowedTags: Ot }), + s.hasChildNodes() && + !Mr(s.firstElementChild) && + le(/<[/\w]/g, s.innerHTML) && + le(/<[/\w]/g, s.textContent)) + ) + return (Or(s), !0); + if (s.nodeType === rt.progressingInstruction) return (Or(s), !0); + if (Ft && s.nodeType === rt.comment && le(/<[/\w]/g, s.data)) return (Or(s), !0); + if (!Ot[i] || Mt[i]) { + if (!Mt[i] && Dr(i)) { + if (Pt.tagNameCheck instanceof RegExp && le(Pt.tagNameCheck, i)) return !1; + if (Pt.tagNameCheck instanceof Function && Pt.tagNameCheck(i)) return !1; + } + if (Gt && !Zt[i]) { + const o = lt(s) || s.parentNode, + i = at(s) || s.childNodes; + if (i && o) + for (let u = i.length - 1; u >= 0; --u) { + const _ = et(i[u], !0); + ((_.__removalCount = (s.__removalCount || 0) + 1), + o.insertBefore(_, it(s))); + } + } + return (Or(s), !0); + } + return s instanceof Re && !Cr(s) + ? (Or(s), !0) + : ('noscript' !== i && 'noembed' !== i && 'noframes' !== i) || + !le(/<\/no(script|embed|frames)/i, s.innerHTML) + ? (Bt && + s.nodeType === rt.text && + ((o = s.textContent), + B([vt, bt, _t], (s) => { + o = Z(o, s, ' '); + }), + s.textContent !== o && + (V(DOMPurify.removed, { element: s.cloneNode() }), + (s.textContent = o))), + Tr('afterSanitizeElements', s, null), + !1) + : (Or(s), !0); + }, + Rr = function _isValidAttribute(s, o, u) { + if (Kt && ('id' === o || 'name' === o) && (u in i || u in vr)) return !1; + if (Rt && !Tt[o] && le(Et, o)); + else if (Nt && le(wt, o)); + else if (!jt[o] || Tt[o]) { + if ( + !( + (Dr(s) && + ((Pt.tagNameCheck instanceof RegExp && le(Pt.tagNameCheck, s)) || + (Pt.tagNameCheck instanceof Function && Pt.tagNameCheck(s))) && + ((Pt.attributeNameCheck instanceof RegExp && + le(Pt.attributeNameCheck, o)) || + (Pt.attributeNameCheck instanceof Function && + Pt.attributeNameCheck(o)))) || + ('is' === o && + Pt.allowCustomizedBuiltInElements && + ((Pt.tagNameCheck instanceof RegExp && le(Pt.tagNameCheck, u)) || + (Pt.tagNameCheck instanceof Function && Pt.tagNameCheck(u)))) + ) + ) + return !1; + } else if (rr[o]); + else if (le(Ct, Z(u, xt, ''))); + else if ( + ('src' !== o && 'xlink:href' !== o && 'href' !== o) || + 'script' === s || + 0 !== ee(u, 'data:') || + !er[s] + ) + if (Dt && !le(St, Z(u, xt, ''))); + else if (u) return !1; + return !0; + }, + Dr = function _isBasicCustomElement(s) { + return 'annotation-xml' !== s && Y(s, kt); + }, + Lr = function _sanitizeAttributes(s) { + Tr('beforeSanitizeAttributes', s, null); + const { attributes: o } = s; + if (!o) return; + const i = { attrName: '', attrValue: '', keepAttr: !0, allowedAttributes: jt }; + let u = o.length; + for (; u--; ) { + const _ = o[u], + { name: w, namespaceURI: x, value: C } = _, + j = gr(w); + let L = 'value' === w ? C : ie(C); + if ( + ((i.attrName = j), + (i.attrValue = L), + (i.keepAttr = !0), + (i.forceKeepAttr = void 0), + Tr('uponSanitizeAttribute', s, i), + (L = i.attrValue), + Ft && le(/((--!?|])>)|<\/(style|title)/i, L)) + ) { + Ar(w, s); + continue; + } + if (i.forceKeepAttr) continue; + if ((Ar(w, s), !i.keepAttr)) continue; + if (!Lt && le(/\/>/i, L)) { + Ar(w, s); + continue; + } + Bt && + B([vt, bt, _t], (s) => { + L = Z(L, s, ' '); + }); + const V = gr(s.nodeName); + if (Rr(V, j, L)) { + if ( + (!Ht || ('id' !== j && 'name' !== j) || (Ar(w, s), (L = Jt + L)), + ct && 'object' == typeof Ye && 'function' == typeof Ye.getAttributeType) + ) + if (x); + else + switch (Ye.getAttributeType(V, j)) { + case 'TrustedHTML': + L = ct.createHTML(L); + break; + case 'TrustedScriptURL': + L = ct.createScriptURL(L); + } + try { + (x ? s.setAttributeNS(x, w, L) : s.setAttribute(w, L), + Pr(s) ? Or(s) : $(DOMPurify.removed)); + } catch (s) {} + } + } + Tr('afterSanitizeAttributes', s, null); + }, + Br = function _sanitizeShadowDOM(s) { + let o = null; + const i = Ir(s); + for (Tr('beforeSanitizeShadowDOM', s, null); (o = i.nextNode()); ) + (Tr('uponSanitizeShadowNode', o, null), + Nr(o) || (o.content instanceof x && _sanitizeShadowDOM(o.content), Lr(o))); + Tr('afterSanitizeShadowDOM', s, null); + }; + return ( + (DOMPurify.sanitize = function (s) { + let o = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}, + i = null, + _ = null, + w = null, + C = null; + if (((cr = !s), cr && (s = '\x3c!--\x3e'), 'string' != typeof s && !Mr(s))) { + if ('function' != typeof s.toString) throw ce('toString is not a function'); + if ('string' != typeof (s = s.toString())) + throw ce('dirty is not a string, aborting'); + } + if (!DOMPurify.isSupported) return s; + if ( + ($t || _r(o), (DOMPurify.removed = []), 'string' == typeof s && (Yt = !1), Yt) + ) { + if (s.nodeName) { + const o = gr(s.nodeName); + if (!Ot[o] || Mt[o]) + throw ce('root node is forbidden and cannot be sanitized in-place'); + } + } else if (s instanceof L) + ((i = jr('\x3c!----\x3e')), + (_ = i.ownerDocument.importNode(s, !0)), + (_.nodeType === rt.element && 'BODY' === _.nodeName) || 'HTML' === _.nodeName + ? (i = _) + : i.appendChild(_)); + else { + if (!Ut && !Bt && !qt && -1 === s.indexOf('<')) + return ct && Wt ? ct.createHTML(s) : s; + if (((i = jr(s)), !i)) return Ut ? null : Wt ? ut : ''; + } + i && Vt && Or(i.firstChild); + const j = Ir(Yt ? s : i); + for (; (w = j.nextNode()); ) + Nr(w) || (w.content instanceof x && Br(w.content), Lr(w)); + if (Yt) return s; + if (Ut) { + if (zt) + for (C = dt.call(i.ownerDocument); i.firstChild; ) + C.appendChild(i.firstChild); + else C = i; + return ((jt.shadowroot || jt.shadowrootmode) && (C = gt.call(u, C, !0)), C); + } + let $ = qt ? i.outerHTML : i.innerHTML; + return ( + qt && + Ot['!doctype'] && + i.ownerDocument && + i.ownerDocument.doctype && + i.ownerDocument.doctype.name && + le(Qe, i.ownerDocument.doctype.name) && + ($ = '\n' + $), + Bt && + B([vt, bt, _t], (s) => { + $ = Z($, s, ' '); + }), + ct && Wt ? ct.createHTML($) : $ + ); + }), + (DOMPurify.setConfig = function () { + (_r(arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}), + ($t = !0)); + }), + (DOMPurify.clearConfig = function () { + ((yr = null), ($t = !1)); + }), + (DOMPurify.isValidAttribute = function (s, o, i) { + yr || _r({}); + const u = gr(s), + _ = gr(o); + return Rr(u, _, i); + }), + (DOMPurify.addHook = function (s, o) { + 'function' == typeof o && ((yt[s] = yt[s] || []), V(yt[s], o)); + }), + (DOMPurify.removeHook = function (s) { + if (yt[s]) return $(yt[s]); + }), + (DOMPurify.removeHooks = function (s) { + yt[s] && (yt[s] = []); + }), + (DOMPurify.removeAllHooks = function () { + yt = {}; + }), + DOMPurify + ); + } + return createDOMPurify(); + })(); + }, + 78004: (s) => { + 'use strict'; + class SubRange { + constructor(s, o) { + ((this.low = s), (this.high = o), (this.length = 1 + o - s)); + } + overlaps(s) { + return !(this.high < s.low || this.low > s.high); + } + touches(s) { + return !(this.high + 1 < s.low || this.low - 1 > s.high); + } + add(s) { + return new SubRange(Math.min(this.low, s.low), Math.max(this.high, s.high)); + } + subtract(s) { + return s.low <= this.low && s.high >= this.high + ? [] + : s.low > this.low && s.high < this.high + ? [new SubRange(this.low, s.low - 1), new SubRange(s.high + 1, this.high)] + : s.low <= this.low + ? [new SubRange(s.high + 1, this.high)] + : [new SubRange(this.low, s.low - 1)]; + } + toString() { + return this.low == this.high ? this.low.toString() : this.low + '-' + this.high; + } + } + class DRange { + constructor(s, o) { + ((this.ranges = []), (this.length = 0), null != s && this.add(s, o)); + } + _update_length() { + this.length = this.ranges.reduce((s, o) => s + o.length, 0); + } + add(s, o) { + var _add = (s) => { + for (var o = 0; o < this.ranges.length && !s.touches(this.ranges[o]); ) o++; + for ( + var i = this.ranges.slice(0, o); + o < this.ranges.length && s.touches(this.ranges[o]); + ) + ((s = s.add(this.ranges[o])), o++); + (i.push(s), (this.ranges = i.concat(this.ranges.slice(o))), this._update_length()); + }; + return ( + s instanceof DRange + ? s.ranges.forEach(_add) + : (null == o && (o = s), _add(new SubRange(s, o))), + this + ); + } + subtract(s, o) { + var _subtract = (s) => { + for (var o = 0; o < this.ranges.length && !s.overlaps(this.ranges[o]); ) o++; + for ( + var i = this.ranges.slice(0, o); + o < this.ranges.length && s.overlaps(this.ranges[o]); + ) + ((i = i.concat(this.ranges[o].subtract(s))), o++); + ((this.ranges = i.concat(this.ranges.slice(o))), this._update_length()); + }; + return ( + s instanceof DRange + ? s.ranges.forEach(_subtract) + : (null == o && (o = s), _subtract(new SubRange(s, o))), + this + ); + } + intersect(s, o) { + var i = [], + _intersect = (s) => { + for (var o = 0; o < this.ranges.length && !s.overlaps(this.ranges[o]); ) o++; + for (; o < this.ranges.length && s.overlaps(this.ranges[o]); ) { + var u = Math.max(this.ranges[o].low, s.low), + _ = Math.min(this.ranges[o].high, s.high); + (i.push(new SubRange(u, _)), o++); + } + }; + return ( + s instanceof DRange + ? s.ranges.forEach(_intersect) + : (null == o && (o = s), _intersect(new SubRange(s, o))), + (this.ranges = i), + this._update_length(), + this + ); + } + index(s) { + for (var o = 0; o < this.ranges.length && this.ranges[o].length <= s; ) + ((s -= this.ranges[o].length), o++); + return this.ranges[o].low + s; + } + toString() { + return '[ ' + this.ranges.join(', ') + ' ]'; + } + clone() { + return new DRange(this); + } + numbers() { + return this.ranges.reduce((s, o) => { + for (var i = o.low; i <= o.high; ) (s.push(i), i++); + return s; + }, []); + } + subranges() { + return this.ranges.map((s) => ({ + low: s.low, + high: s.high, + length: 1 + s.high - s.low + })); + } + } + s.exports = DRange; + }, + 37007: (s) => { + 'use strict'; + var o, + i = 'object' == typeof Reflect ? Reflect : null, + u = + i && 'function' == typeof i.apply + ? i.apply + : function ReflectApply(s, o, i) { + return Function.prototype.apply.call(s, o, i); + }; + o = + i && 'function' == typeof i.ownKeys + ? i.ownKeys + : Object.getOwnPropertySymbols + ? function ReflectOwnKeys(s) { + return Object.getOwnPropertyNames(s).concat(Object.getOwnPropertySymbols(s)); + } + : function ReflectOwnKeys(s) { + return Object.getOwnPropertyNames(s); + }; + var _ = + Number.isNaN || + function NumberIsNaN(s) { + return s != s; + }; + function EventEmitter() { + EventEmitter.init.call(this); + } + ((s.exports = EventEmitter), + (s.exports.once = function once(s, o) { + return new Promise(function (i, u) { + function errorListener(i) { + (s.removeListener(o, resolver), u(i)); + } + function resolver() { + ('function' == typeof s.removeListener && + s.removeListener('error', errorListener), + i([].slice.call(arguments))); + } + (eventTargetAgnosticAddListener(s, o, resolver, { once: !0 }), + 'error' !== o && + (function addErrorHandlerIfEventEmitter(s, o, i) { + 'function' == typeof s.on && eventTargetAgnosticAddListener(s, 'error', o, i); + })(s, errorListener, { once: !0 })); + }); + }), + (EventEmitter.EventEmitter = EventEmitter), + (EventEmitter.prototype._events = void 0), + (EventEmitter.prototype._eventsCount = 0), + (EventEmitter.prototype._maxListeners = void 0)); + var w = 10; + function checkListener(s) { + if ('function' != typeof s) + throw new TypeError( + 'The "listener" argument must be of type Function. Received type ' + typeof s + ); + } + function _getMaxListeners(s) { + return void 0 === s._maxListeners ? EventEmitter.defaultMaxListeners : s._maxListeners; + } + function _addListener(s, o, i, u) { + var _, w, x; + if ( + (checkListener(i), + void 0 === (w = s._events) + ? ((w = s._events = Object.create(null)), (s._eventsCount = 0)) + : (void 0 !== w.newListener && + (s.emit('newListener', o, i.listener ? i.listener : i), (w = s._events)), + (x = w[o])), + void 0 === x) + ) + ((x = w[o] = i), ++s._eventsCount); + else if ( + ('function' == typeof x + ? (x = w[o] = u ? [i, x] : [x, i]) + : u + ? x.unshift(i) + : x.push(i), + (_ = _getMaxListeners(s)) > 0 && x.length > _ && !x.warned) + ) { + x.warned = !0; + var C = new Error( + 'Possible EventEmitter memory leak detected. ' + + x.length + + ' ' + + String(o) + + ' listeners added. Use emitter.setMaxListeners() to increase limit' + ); + ((C.name = 'MaxListenersExceededWarning'), + (C.emitter = s), + (C.type = o), + (C.count = x.length), + (function ProcessEmitWarning(s) { + console && console.warn && console.warn(s); + })(C)); + } + return s; + } + function onceWrapper() { + if (!this.fired) + return ( + this.target.removeListener(this.type, this.wrapFn), + (this.fired = !0), + 0 === arguments.length + ? this.listener.call(this.target) + : this.listener.apply(this.target, arguments) + ); + } + function _onceWrap(s, o, i) { + var u = { fired: !1, wrapFn: void 0, target: s, type: o, listener: i }, + _ = onceWrapper.bind(u); + return ((_.listener = i), (u.wrapFn = _), _); + } + function _listeners(s, o, i) { + var u = s._events; + if (void 0 === u) return []; + var _ = u[o]; + return void 0 === _ + ? [] + : 'function' == typeof _ + ? i + ? [_.listener || _] + : [_] + : i + ? (function unwrapListeners(s) { + for (var o = new Array(s.length), i = 0; i < o.length; ++i) + o[i] = s[i].listener || s[i]; + return o; + })(_) + : arrayClone(_, _.length); + } + function listenerCount(s) { + var o = this._events; + if (void 0 !== o) { + var i = o[s]; + if ('function' == typeof i) return 1; + if (void 0 !== i) return i.length; + } + return 0; + } + function arrayClone(s, o) { + for (var i = new Array(o), u = 0; u < o; ++u) i[u] = s[u]; + return i; + } + function eventTargetAgnosticAddListener(s, o, i, u) { + if ('function' == typeof s.on) u.once ? s.once(o, i) : s.on(o, i); + else { + if ('function' != typeof s.addEventListener) + throw new TypeError( + 'The "emitter" argument must be of type EventEmitter. Received type ' + typeof s + ); + s.addEventListener(o, function wrapListener(_) { + (u.once && s.removeEventListener(o, wrapListener), i(_)); + }); + } + } + (Object.defineProperty(EventEmitter, 'defaultMaxListeners', { + enumerable: !0, + get: function () { + return w; + }, + set: function (s) { + if ('number' != typeof s || s < 0 || _(s)) + throw new RangeError( + 'The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + + s + + '.' + ); + w = s; + } + }), + (EventEmitter.init = function () { + ((void 0 !== this._events && this._events !== Object.getPrototypeOf(this)._events) || + ((this._events = Object.create(null)), (this._eventsCount = 0)), + (this._maxListeners = this._maxListeners || void 0)); + }), + (EventEmitter.prototype.setMaxListeners = function setMaxListeners(s) { + if ('number' != typeof s || s < 0 || _(s)) + throw new RangeError( + 'The value of "n" is out of range. It must be a non-negative number. Received ' + + s + + '.' + ); + return ((this._maxListeners = s), this); + }), + (EventEmitter.prototype.getMaxListeners = function getMaxListeners() { + return _getMaxListeners(this); + }), + (EventEmitter.prototype.emit = function emit(s) { + for (var o = [], i = 1; i < arguments.length; i++) o.push(arguments[i]); + var _ = 'error' === s, + w = this._events; + if (void 0 !== w) _ = _ && void 0 === w.error; + else if (!_) return !1; + if (_) { + var x; + if ((o.length > 0 && (x = o[0]), x instanceof Error)) throw x; + var C = new Error('Unhandled error.' + (x ? ' (' + x.message + ')' : '')); + throw ((C.context = x), C); + } + var j = w[s]; + if (void 0 === j) return !1; + if ('function' == typeof j) u(j, this, o); + else { + var L = j.length, + B = arrayClone(j, L); + for (i = 0; i < L; ++i) u(B[i], this, o); + } + return !0; + }), + (EventEmitter.prototype.addListener = function addListener(s, o) { + return _addListener(this, s, o, !1); + }), + (EventEmitter.prototype.on = EventEmitter.prototype.addListener), + (EventEmitter.prototype.prependListener = function prependListener(s, o) { + return _addListener(this, s, o, !0); + }), + (EventEmitter.prototype.once = function once(s, o) { + return (checkListener(o), this.on(s, _onceWrap(this, s, o)), this); + }), + (EventEmitter.prototype.prependOnceListener = function prependOnceListener(s, o) { + return (checkListener(o), this.prependListener(s, _onceWrap(this, s, o)), this); + }), + (EventEmitter.prototype.removeListener = function removeListener(s, o) { + var i, u, _, w, x; + if ((checkListener(o), void 0 === (u = this._events))) return this; + if (void 0 === (i = u[s])) return this; + if (i === o || i.listener === o) + 0 == --this._eventsCount + ? (this._events = Object.create(null)) + : (delete u[s], + u.removeListener && this.emit('removeListener', s, i.listener || o)); + else if ('function' != typeof i) { + for (_ = -1, w = i.length - 1; w >= 0; w--) + if (i[w] === o || i[w].listener === o) { + ((x = i[w].listener), (_ = w)); + break; + } + if (_ < 0) return this; + (0 === _ + ? i.shift() + : (function spliceOne(s, o) { + for (; o + 1 < s.length; o++) s[o] = s[o + 1]; + s.pop(); + })(i, _), + 1 === i.length && (u[s] = i[0]), + void 0 !== u.removeListener && this.emit('removeListener', s, x || o)); + } + return this; + }), + (EventEmitter.prototype.off = EventEmitter.prototype.removeListener), + (EventEmitter.prototype.removeAllListeners = function removeAllListeners(s) { + var o, i, u; + if (void 0 === (i = this._events)) return this; + if (void 0 === i.removeListener) + return ( + 0 === arguments.length + ? ((this._events = Object.create(null)), (this._eventsCount = 0)) + : void 0 !== i[s] && + (0 == --this._eventsCount + ? (this._events = Object.create(null)) + : delete i[s]), + this + ); + if (0 === arguments.length) { + var _, + w = Object.keys(i); + for (u = 0; u < w.length; ++u) + 'removeListener' !== (_ = w[u]) && this.removeAllListeners(_); + return ( + this.removeAllListeners('removeListener'), + (this._events = Object.create(null)), + (this._eventsCount = 0), + this + ); + } + if ('function' == typeof (o = i[s])) this.removeListener(s, o); + else if (void 0 !== o) + for (u = o.length - 1; u >= 0; u--) this.removeListener(s, o[u]); + return this; + }), + (EventEmitter.prototype.listeners = function listeners(s) { + return _listeners(this, s, !0); + }), + (EventEmitter.prototype.rawListeners = function rawListeners(s) { + return _listeners(this, s, !1); + }), + (EventEmitter.listenerCount = function (s, o) { + return 'function' == typeof s.listenerCount + ? s.listenerCount(o) + : listenerCount.call(s, o); + }), + (EventEmitter.prototype.listenerCount = listenerCount), + (EventEmitter.prototype.eventNames = function eventNames() { + return this._eventsCount > 0 ? o(this._events) : []; + })); + }, + 85587: (s, o, i) => { + 'use strict'; + var u = i(26311), + _ = create(Error); + function create(s) { + return ((FormattedError.displayName = s.displayName || s.name), FormattedError); + function FormattedError(o) { + return (o && (o = u.apply(null, arguments)), new s(o)); + } + } + ((s.exports = _), + (_.eval = create(EvalError)), + (_.range = create(RangeError)), + (_.reference = create(ReferenceError)), + (_.syntax = create(SyntaxError)), + (_.type = create(TypeError)), + (_.uri = create(URIError)), + (_.create = create)); + }, + 26311: (s) => { + !(function () { + var o; + function format(s) { + for ( + var o, + i, + u, + _, + w = 1, + x = [].slice.call(arguments), + C = 0, + j = s.length, + L = '', + B = !1, + $ = !1, + nextArg = function () { + return x[w++]; + }, + slurpNumber = function () { + for (var i = ''; /\d/.test(s[C]); ) ((i += s[C++]), (o = s[C])); + return i.length > 0 ? parseInt(i) : null; + }; + C < j; + ++C + ) + if (((o = s[C]), B)) + switch ( + ((B = !1), + '.' == o + ? (($ = !1), (o = s[++C])) + : '0' == o && '.' == s[C + 1] + ? (($ = !0), (o = s[(C += 2)])) + : ($ = !0), + (_ = slurpNumber()), + o) + ) { + case 'b': + L += parseInt(nextArg(), 10).toString(2); + break; + case 'c': + L += + 'string' == typeof (i = nextArg()) || i instanceof String + ? i + : String.fromCharCode(parseInt(i, 10)); + break; + case 'd': + L += parseInt(nextArg(), 10); + break; + case 'f': + ((u = String(parseFloat(nextArg()).toFixed(_ || 6))), + (L += $ ? u : u.replace(/^0/, ''))); + break; + case 'j': + L += JSON.stringify(nextArg()); + break; + case 'o': + L += '0' + parseInt(nextArg(), 10).toString(8); + break; + case 's': + L += nextArg(); + break; + case 'x': + L += '0x' + parseInt(nextArg(), 10).toString(16); + break; + case 'X': + L += '0x' + parseInt(nextArg(), 10).toString(16).toUpperCase(); + break; + default: + L += o; + } + else '%' === o ? (B = !0) : (L += o); + return L; + } + (((o = s.exports = format).format = format), + (o.vsprintf = function vsprintf(s, o) { + return format.apply(null, [s].concat(o)); + }), + 'undefined' != typeof console && + 'function' == typeof console.log && + (o.printf = function printf() { + console.log(format.apply(null, arguments)); + })); + })(); + }, + 45981: (s) => { + function deepFreeze(s) { + return ( + s instanceof Map + ? (s.clear = + s.delete = + s.set = + function () { + throw new Error('map is read-only'); + }) + : s instanceof Set && + (s.add = + s.clear = + s.delete = + function () { + throw new Error('set is read-only'); + }), + Object.freeze(s), + Object.getOwnPropertyNames(s).forEach(function (o) { + var i = s[o]; + 'object' != typeof i || Object.isFrozen(i) || deepFreeze(i); + }), + s + ); + } + var o = deepFreeze, + i = deepFreeze; + o.default = i; + class Response { + constructor(s) { + (void 0 === s.data && (s.data = {}), + (this.data = s.data), + (this.isMatchIgnored = !1)); + } + ignoreMatch() { + this.isMatchIgnored = !0; + } + } + function escapeHTML(s) { + return s + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + function inherit(s, ...o) { + const i = Object.create(null); + for (const o in s) i[o] = s[o]; + return ( + o.forEach(function (s) { + for (const o in s) i[o] = s[o]; + }), + i + ); + } + const emitsWrappingTags = (s) => !!s.kind; + class HTMLRenderer { + constructor(s, o) { + ((this.buffer = ''), (this.classPrefix = o.classPrefix), s.walk(this)); + } + addText(s) { + this.buffer += escapeHTML(s); + } + openNode(s) { + if (!emitsWrappingTags(s)) return; + let o = s.kind; + (s.sublanguage || (o = `${this.classPrefix}${o}`), this.span(o)); + } + closeNode(s) { + emitsWrappingTags(s) && (this.buffer += ''); + } + value() { + return this.buffer; + } + span(s) { + this.buffer += ``; + } + } + class TokenTree { + constructor() { + ((this.rootNode = { children: [] }), (this.stack = [this.rootNode])); + } + get top() { + return this.stack[this.stack.length - 1]; + } + get root() { + return this.rootNode; + } + add(s) { + this.top.children.push(s); + } + openNode(s) { + const o = { kind: s, children: [] }; + (this.add(o), this.stack.push(o)); + } + closeNode() { + if (this.stack.length > 1) return this.stack.pop(); + } + closeAllNodes() { + for (; this.closeNode(); ); + } + toJSON() { + return JSON.stringify(this.rootNode, null, 4); + } + walk(s) { + return this.constructor._walk(s, this.rootNode); + } + static _walk(s, o) { + return ( + 'string' == typeof o + ? s.addText(o) + : o.children && + (s.openNode(o), o.children.forEach((o) => this._walk(s, o)), s.closeNode(o)), + s + ); + } + static _collapse(s) { + 'string' != typeof s && + s.children && + (s.children.every((s) => 'string' == typeof s) + ? (s.children = [s.children.join('')]) + : s.children.forEach((s) => { + TokenTree._collapse(s); + })); + } + } + class TokenTreeEmitter extends TokenTree { + constructor(s) { + (super(), (this.options = s)); + } + addKeyword(s, o) { + '' !== s && (this.openNode(o), this.addText(s), this.closeNode()); + } + addText(s) { + '' !== s && this.add(s); + } + addSublanguage(s, o) { + const i = s.root; + ((i.kind = o), (i.sublanguage = !0), this.add(i)); + } + toHTML() { + return new HTMLRenderer(this, this.options).value(); + } + finalize() { + return !0; + } + } + function source(s) { + return s ? ('string' == typeof s ? s : s.source) : null; + } + const u = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./; + const _ = '[a-zA-Z]\\w*', + w = '[a-zA-Z_]\\w*', + x = '\\b\\d+(\\.\\d+)?', + C = '(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)', + j = '\\b(0b[01]+)', + L = { begin: '\\\\[\\s\\S]', relevance: 0 }, + B = { className: 'string', begin: "'", end: "'", illegal: '\\n', contains: [L] }, + $ = { className: 'string', begin: '"', end: '"', illegal: '\\n', contains: [L] }, + V = { + begin: + /\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ + }, + COMMENT = function (s, o, i = {}) { + const u = inherit({ className: 'comment', begin: s, end: o, contains: [] }, i); + return ( + u.contains.push(V), + u.contains.push({ + className: 'doctag', + begin: '(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):', + relevance: 0 + }), + u + ); + }, + U = COMMENT('//', '$'), + z = COMMENT('/\\*', '\\*/'), + Y = COMMENT('#', '$'), + Z = { className: 'number', begin: x, relevance: 0 }, + ee = { className: 'number', begin: C, relevance: 0 }, + ie = { className: 'number', begin: j, relevance: 0 }, + ae = { + className: 'number', + begin: + x + + '(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?', + relevance: 0 + }, + le = { + begin: /(?=\/[^/\n]*\/)/, + contains: [ + { + className: 'regexp', + begin: /\//, + end: /\/[gimuy]*/, + illegal: /\n/, + contains: [L, { begin: /\[/, end: /\]/, relevance: 0, contains: [L] }] + } + ] + }, + ce = { className: 'title', begin: _, relevance: 0 }, + pe = { className: 'title', begin: w, relevance: 0 }, + de = { begin: '\\.\\s*' + w, relevance: 0 }; + var fe = Object.freeze({ + __proto__: null, + MATCH_NOTHING_RE: /\b\B/, + IDENT_RE: _, + UNDERSCORE_IDENT_RE: w, + NUMBER_RE: x, + C_NUMBER_RE: C, + BINARY_NUMBER_RE: j, + RE_STARTERS_RE: + '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~', + SHEBANG: (s = {}) => { + const o = /^#![ ]*\//; + return ( + s.binary && + (s.begin = (function concat(...s) { + return s.map((s) => source(s)).join(''); + })(o, /.*\b/, s.binary, /\b.*/)), + inherit( + { + className: 'meta', + begin: o, + end: /$/, + relevance: 0, + 'on:begin': (s, o) => { + 0 !== s.index && o.ignoreMatch(); + } + }, + s + ) + ); + }, + BACKSLASH_ESCAPE: L, + APOS_STRING_MODE: B, + QUOTE_STRING_MODE: $, + PHRASAL_WORDS_MODE: V, + COMMENT, + C_LINE_COMMENT_MODE: U, + C_BLOCK_COMMENT_MODE: z, + HASH_COMMENT_MODE: Y, + NUMBER_MODE: Z, + C_NUMBER_MODE: ee, + BINARY_NUMBER_MODE: ie, + CSS_NUMBER_MODE: ae, + REGEXP_MODE: le, + TITLE_MODE: ce, + UNDERSCORE_TITLE_MODE: pe, + METHOD_GUARD: de, + END_SAME_AS_BEGIN: function (s) { + return Object.assign(s, { + 'on:begin': (s, o) => { + o.data._beginMatch = s[1]; + }, + 'on:end': (s, o) => { + o.data._beginMatch !== s[1] && o.ignoreMatch(); + } + }); + } + }); + function skipIfhasPrecedingDot(s, o) { + '.' === s.input[s.index - 1] && o.ignoreMatch(); + } + function beginKeywords(s, o) { + o && + s.beginKeywords && + ((s.begin = '\\b(' + s.beginKeywords.split(' ').join('|') + ')(?!\\.)(?=\\b|\\s)'), + (s.__beforeBegin = skipIfhasPrecedingDot), + (s.keywords = s.keywords || s.beginKeywords), + delete s.beginKeywords, + void 0 === s.relevance && (s.relevance = 0)); + } + function compileIllegal(s, o) { + Array.isArray(s.illegal) && + (s.illegal = (function either(...s) { + return '(' + s.map((s) => source(s)).join('|') + ')'; + })(...s.illegal)); + } + function compileMatch(s, o) { + if (s.match) { + if (s.begin || s.end) throw new Error('begin & end are not supported with match'); + ((s.begin = s.match), delete s.match); + } + } + function compileRelevance(s, o) { + void 0 === s.relevance && (s.relevance = 1); + } + const ye = [ + 'of', + 'and', + 'for', + 'in', + 'not', + 'or', + 'if', + 'then', + 'parent', + 'list', + 'value' + ]; + function compileKeywords(s, o, i = 'keyword') { + const u = {}; + return ( + 'string' == typeof s + ? compileList(i, s.split(' ')) + : Array.isArray(s) + ? compileList(i, s) + : Object.keys(s).forEach(function (i) { + Object.assign(u, compileKeywords(s[i], o, i)); + }), + u + ); + function compileList(s, i) { + (o && (i = i.map((s) => s.toLowerCase())), + i.forEach(function (o) { + const i = o.split('|'); + u[i[0]] = [s, scoreForKeyword(i[0], i[1])]; + })); + } + } + function scoreForKeyword(s, o) { + return o + ? Number(o) + : (function commonKeyword(s) { + return ye.includes(s.toLowerCase()); + })(s) + ? 0 + : 1; + } + function compileLanguage(s, { plugins: o }) { + function langRe(o, i) { + return new RegExp(source(o), 'm' + (s.case_insensitive ? 'i' : '') + (i ? 'g' : '')); + } + class MultiRegex { + constructor() { + ((this.matchIndexes = {}), + (this.regexes = []), + (this.matchAt = 1), + (this.position = 0)); + } + addRule(s, o) { + ((o.position = this.position++), + (this.matchIndexes[this.matchAt] = o), + this.regexes.push([o, s]), + (this.matchAt += + (function countMatchGroups(s) { + return new RegExp(s.toString() + '|').exec('').length - 1; + })(s) + 1)); + } + compile() { + 0 === this.regexes.length && (this.exec = () => null); + const s = this.regexes.map((s) => s[1]); + ((this.matcherRe = langRe( + (function join(s, o = '|') { + let i = 0; + return s + .map((s) => { + i += 1; + const o = i; + let _ = source(s), + w = ''; + for (; _.length > 0; ) { + const s = u.exec(_); + if (!s) { + w += _; + break; + } + ((w += _.substring(0, s.index)), + (_ = _.substring(s.index + s[0].length)), + '\\' === s[0][0] && s[1] + ? (w += '\\' + String(Number(s[1]) + o)) + : ((w += s[0]), '(' === s[0] && i++)); + } + return w; + }) + .map((s) => `(${s})`) + .join(o); + })(s), + !0 + )), + (this.lastIndex = 0)); + } + exec(s) { + this.matcherRe.lastIndex = this.lastIndex; + const o = this.matcherRe.exec(s); + if (!o) return null; + const i = o.findIndex((s, o) => o > 0 && void 0 !== s), + u = this.matchIndexes[i]; + return (o.splice(0, i), Object.assign(o, u)); + } + } + class ResumableMultiRegex { + constructor() { + ((this.rules = []), + (this.multiRegexes = []), + (this.count = 0), + (this.lastIndex = 0), + (this.regexIndex = 0)); + } + getMatcher(s) { + if (this.multiRegexes[s]) return this.multiRegexes[s]; + const o = new MultiRegex(); + return ( + this.rules.slice(s).forEach(([s, i]) => o.addRule(s, i)), + o.compile(), + (this.multiRegexes[s] = o), + o + ); + } + resumingScanAtSamePosition() { + return 0 !== this.regexIndex; + } + considerAll() { + this.regexIndex = 0; + } + addRule(s, o) { + (this.rules.push([s, o]), 'begin' === o.type && this.count++); + } + exec(s) { + const o = this.getMatcher(this.regexIndex); + o.lastIndex = this.lastIndex; + let i = o.exec(s); + if (this.resumingScanAtSamePosition()) + if (i && i.index === this.lastIndex); + else { + const o = this.getMatcher(0); + ((o.lastIndex = this.lastIndex + 1), (i = o.exec(s))); + } + return ( + i && + ((this.regexIndex += i.position + 1), + this.regexIndex === this.count && this.considerAll()), + i + ); + } + } + if ( + (s.compilerExtensions || (s.compilerExtensions = []), + s.contains && s.contains.includes('self')) + ) + throw new Error( + 'ERR: contains `self` is not supported at the top-level of a language. See documentation.' + ); + return ( + (s.classNameAliases = inherit(s.classNameAliases || {})), + (function compileMode(o, i) { + const u = o; + if (o.isCompiled) return u; + ([compileMatch].forEach((s) => s(o, i)), + s.compilerExtensions.forEach((s) => s(o, i)), + (o.__beforeBegin = null), + [beginKeywords, compileIllegal, compileRelevance].forEach((s) => s(o, i)), + (o.isCompiled = !0)); + let _ = null; + if ( + ('object' == typeof o.keywords && + ((_ = o.keywords.$pattern), delete o.keywords.$pattern), + o.keywords && (o.keywords = compileKeywords(o.keywords, s.case_insensitive)), + o.lexemes && _) + ) + throw new Error( + 'ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ' + ); + return ( + (_ = _ || o.lexemes || /\w+/), + (u.keywordPatternRe = langRe(_, !0)), + i && + (o.begin || (o.begin = /\B|\b/), + (u.beginRe = langRe(o.begin)), + o.endSameAsBegin && (o.end = o.begin), + o.end || o.endsWithParent || (o.end = /\B|\b/), + o.end && (u.endRe = langRe(o.end)), + (u.terminatorEnd = source(o.end) || ''), + o.endsWithParent && + i.terminatorEnd && + (u.terminatorEnd += (o.end ? '|' : '') + i.terminatorEnd)), + o.illegal && (u.illegalRe = langRe(o.illegal)), + o.contains || (o.contains = []), + (o.contains = [].concat( + ...o.contains.map(function (s) { + return (function expandOrCloneMode(s) { + s.variants && + !s.cachedVariants && + (s.cachedVariants = s.variants.map(function (o) { + return inherit(s, { variants: null }, o); + })); + if (s.cachedVariants) return s.cachedVariants; + if (dependencyOnParent(s)) + return inherit(s, { starts: s.starts ? inherit(s.starts) : null }); + if (Object.isFrozen(s)) return inherit(s); + return s; + })('self' === s ? o : s); + }) + )), + o.contains.forEach(function (s) { + compileMode(s, u); + }), + o.starts && compileMode(o.starts, i), + (u.matcher = (function buildModeRegex(s) { + const o = new ResumableMultiRegex(); + return ( + s.contains.forEach((s) => o.addRule(s.begin, { rule: s, type: 'begin' })), + s.terminatorEnd && o.addRule(s.terminatorEnd, { type: 'end' }), + s.illegal && o.addRule(s.illegal, { type: 'illegal' }), + o + ); + })(u)), + u + ); + })(s) + ); + } + function dependencyOnParent(s) { + return !!s && (s.endsWithParent || dependencyOnParent(s.starts)); + } + function BuildVuePlugin(s) { + const o = { + props: ['language', 'code', 'autodetect'], + data: function () { + return { detectedLanguage: '', unknownLanguage: !1 }; + }, + computed: { + className() { + return this.unknownLanguage ? '' : 'hljs ' + this.detectedLanguage; + }, + highlighted() { + if (!this.autoDetect && !s.getLanguage(this.language)) + return ( + console.warn( + `The language "${this.language}" you specified could not be found.` + ), + (this.unknownLanguage = !0), + escapeHTML(this.code) + ); + let o = {}; + return ( + this.autoDetect + ? ((o = s.highlightAuto(this.code)), (this.detectedLanguage = o.language)) + : ((o = s.highlight(this.language, this.code, this.ignoreIllegals)), + (this.detectedLanguage = this.language)), + o.value + ); + }, + autoDetect() { + return ( + !this.language || + (function hasValueOrEmptyAttribute(s) { + return Boolean(s || '' === s); + })(this.autodetect) + ); + }, + ignoreIllegals: () => !0 + }, + render(s) { + return s('pre', {}, [ + s('code', { class: this.className, domProps: { innerHTML: this.highlighted } }) + ]); + } + }; + return { + Component: o, + VuePlugin: { + install(s) { + s.component('highlightjs', o); + } + } + }; + } + const be = { + 'after:highlightElement': ({ el: s, result: o, text: i }) => { + const u = nodeStream(s); + if (!u.length) return; + const _ = document.createElement('div'); + ((_.innerHTML = o.value), + (o.value = (function mergeStreams(s, o, i) { + let u = 0, + _ = ''; + const w = []; + function selectStream() { + return s.length && o.length + ? s[0].offset !== o[0].offset + ? s[0].offset < o[0].offset + ? s + : o + : 'start' === o[0].event + ? s + : o + : s.length + ? s + : o; + } + function open(s) { + function attributeString(s) { + return ' ' + s.nodeName + '="' + escapeHTML(s.value) + '"'; + } + _ += '<' + tag(s) + [].map.call(s.attributes, attributeString).join('') + '>'; + } + function close(s) { + _ += ''; + } + function render(s) { + ('start' === s.event ? open : close)(s.node); + } + for (; s.length || o.length; ) { + let o = selectStream(); + if ( + ((_ += escapeHTML(i.substring(u, o[0].offset))), (u = o[0].offset), o === s) + ) { + w.reverse().forEach(close); + do { + (render(o.splice(0, 1)[0]), (o = selectStream())); + } while (o === s && o.length && o[0].offset === u); + w.reverse().forEach(open); + } else + ('start' === o[0].event ? w.push(o[0].node) : w.pop(), + render(o.splice(0, 1)[0])); + } + return _ + escapeHTML(i.substr(u)); + })(u, nodeStream(_), i))); + } + }; + function tag(s) { + return s.nodeName.toLowerCase(); + } + function nodeStream(s) { + const o = []; + return ( + (function _nodeStream(s, i) { + for (let u = s.firstChild; u; u = u.nextSibling) + 3 === u.nodeType + ? (i += u.nodeValue.length) + : 1 === u.nodeType && + (o.push({ event: 'start', offset: i, node: u }), + (i = _nodeStream(u, i)), + tag(u).match(/br|hr|img|input/) || + o.push({ event: 'stop', offset: i, node: u })); + return i; + })(s, 0), + o + ); + } + const _e = {}, + error = (s) => { + console.error(s); + }, + warn = (s, ...o) => { + console.log(`WARN: ${s}`, ...o); + }, + deprecated = (s, o) => { + _e[`${s}/${o}`] || + (console.log(`Deprecated as of ${s}. ${o}`), (_e[`${s}/${o}`] = !0)); + }, + we = escapeHTML, + Se = inherit, + xe = Symbol('nomatch'); + var Pe = (function (s) { + const i = Object.create(null), + u = Object.create(null), + _ = []; + let w = !0; + const x = /(^(<[^>]+>|\t|)+|\n)/gm, + C = + "Could not find the language '{}', did you forget to load/include a language module?", + j = { disableAutodetect: !0, name: 'Plain text', contains: [] }; + let L = { + noHighlightRe: /^(no-?highlight)$/i, + languageDetectRe: /\blang(?:uage)?-([\w-]+)\b/i, + classPrefix: 'hljs-', + tabReplace: null, + useBR: !1, + languages: null, + __emitter: TokenTreeEmitter + }; + function shouldNotHighlight(s) { + return L.noHighlightRe.test(s); + } + function highlight(s, o, i, u) { + let _ = '', + w = ''; + 'object' == typeof o + ? ((_ = s), (i = o.ignoreIllegals), (w = o.language), (u = void 0)) + : (deprecated('10.7.0', 'highlight(lang, code, ...args) has been deprecated.'), + deprecated( + '10.7.0', + 'Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277' + ), + (w = s), + (_ = o)); + const x = { code: _, language: w }; + fire('before:highlight', x); + const C = x.result ? x.result : _highlight(x.language, x.code, i, u); + return ((C.code = x.code), fire('after:highlight', C), C); + } + function _highlight(s, o, u, x) { + function keywordData(s, o) { + const i = B.case_insensitive ? o[0].toLowerCase() : o[0]; + return Object.prototype.hasOwnProperty.call(s.keywords, i) && s.keywords[i]; + } + function processBuffer() { + (null != U.subLanguage + ? (function processSubLanguage() { + if ('' === Z) return; + let s = null; + if ('string' == typeof U.subLanguage) { + if (!i[U.subLanguage]) return void Y.addText(Z); + ((s = _highlight(U.subLanguage, Z, !0, z[U.subLanguage])), + (z[U.subLanguage] = s.top)); + } else s = highlightAuto(Z, U.subLanguage.length ? U.subLanguage : null); + (U.relevance > 0 && (ee += s.relevance), + Y.addSublanguage(s.emitter, s.language)); + })() + : (function processKeywords() { + if (!U.keywords) return void Y.addText(Z); + let s = 0; + U.keywordPatternRe.lastIndex = 0; + let o = U.keywordPatternRe.exec(Z), + i = ''; + for (; o; ) { + i += Z.substring(s, o.index); + const u = keywordData(U, o); + if (u) { + const [s, _] = u; + if ((Y.addText(i), (i = ''), (ee += _), s.startsWith('_'))) i += o[0]; + else { + const i = B.classNameAliases[s] || s; + Y.addKeyword(o[0], i); + } + } else i += o[0]; + ((s = U.keywordPatternRe.lastIndex), (o = U.keywordPatternRe.exec(Z))); + } + ((i += Z.substr(s)), Y.addText(i)); + })(), + (Z = '')); + } + function startNewMode(s) { + return ( + s.className && Y.openNode(B.classNameAliases[s.className] || s.className), + (U = Object.create(s, { parent: { value: U } })), + U + ); + } + function endOfMode(s, o, i) { + let u = (function startsWith(s, o) { + const i = s && s.exec(o); + return i && 0 === i.index; + })(s.endRe, i); + if (u) { + if (s['on:end']) { + const i = new Response(s); + (s['on:end'](o, i), i.isMatchIgnored && (u = !1)); + } + if (u) { + for (; s.endsParent && s.parent; ) s = s.parent; + return s; + } + } + if (s.endsWithParent) return endOfMode(s.parent, o, i); + } + function doIgnore(s) { + return 0 === U.matcher.regexIndex ? ((Z += s[0]), 1) : ((le = !0), 0); + } + function doBeginMatch(s) { + const o = s[0], + i = s.rule, + u = new Response(i), + _ = [i.__beforeBegin, i['on:begin']]; + for (const i of _) if (i && (i(s, u), u.isMatchIgnored)) return doIgnore(o); + return ( + i && + i.endSameAsBegin && + (i.endRe = (function escape(s) { + return new RegExp(s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 'm'); + })(o)), + i.skip + ? (Z += o) + : (i.excludeBegin && (Z += o), + processBuffer(), + i.returnBegin || i.excludeBegin || (Z = o)), + startNewMode(i), + i.returnBegin ? 0 : o.length + ); + } + function doEndMatch(s) { + const i = s[0], + u = o.substr(s.index), + _ = endOfMode(U, s, u); + if (!_) return xe; + const w = U; + w.skip + ? (Z += i) + : (w.returnEnd || w.excludeEnd || (Z += i), + processBuffer(), + w.excludeEnd && (Z = i)); + do { + (U.className && Y.closeNode(), + U.skip || U.subLanguage || (ee += U.relevance), + (U = U.parent)); + } while (U !== _.parent); + return ( + _.starts && + (_.endSameAsBegin && (_.starts.endRe = _.endRe), startNewMode(_.starts)), + w.returnEnd ? 0 : i.length + ); + } + let j = {}; + function processLexeme(i, _) { + const x = _ && _[0]; + if (((Z += i), null == x)) return (processBuffer(), 0); + if ('begin' === j.type && 'end' === _.type && j.index === _.index && '' === x) { + if (((Z += o.slice(_.index, _.index + 1)), !w)) { + const o = new Error('0 width match regex'); + throw ((o.languageName = s), (o.badRule = j.rule), o); + } + return 1; + } + if (((j = _), 'begin' === _.type)) return doBeginMatch(_); + if ('illegal' === _.type && !u) { + const s = new Error( + 'Illegal lexeme "' + x + '" for mode "' + (U.className || '') + '"' + ); + throw ((s.mode = U), s); + } + if ('end' === _.type) { + const s = doEndMatch(_); + if (s !== xe) return s; + } + if ('illegal' === _.type && '' === x) return 1; + if (ae > 1e5 && ae > 3 * _.index) { + throw new Error('potential infinite loop, way more iterations than matches'); + } + return ((Z += x), x.length); + } + const B = getLanguage(s); + if (!B) throw (error(C.replace('{}', s)), new Error('Unknown language: "' + s + '"')); + const $ = compileLanguage(B, { plugins: _ }); + let V = '', + U = x || $; + const z = {}, + Y = new L.__emitter(L); + !(function processContinuations() { + const s = []; + for (let o = U; o !== B; o = o.parent) o.className && s.unshift(o.className); + s.forEach((s) => Y.openNode(s)); + })(); + let Z = '', + ee = 0, + ie = 0, + ae = 0, + le = !1; + try { + for (U.matcher.considerAll(); ; ) { + (ae++, le ? (le = !1) : U.matcher.considerAll(), (U.matcher.lastIndex = ie)); + const s = U.matcher.exec(o); + if (!s) break; + const i = processLexeme(o.substring(ie, s.index), s); + ie = s.index + i; + } + return ( + processLexeme(o.substr(ie)), + Y.closeAllNodes(), + Y.finalize(), + (V = Y.toHTML()), + { + relevance: Math.floor(ee), + value: V, + language: s, + illegal: !1, + emitter: Y, + top: U + } + ); + } catch (i) { + if (i.message && i.message.includes('Illegal')) + return { + illegal: !0, + illegalBy: { + msg: i.message, + context: o.slice(ie - 100, ie + 100), + mode: i.mode + }, + sofar: V, + relevance: 0, + value: we(o), + emitter: Y + }; + if (w) + return { + illegal: !1, + relevance: 0, + value: we(o), + emitter: Y, + language: s, + top: U, + errorRaised: i + }; + throw i; + } + } + function highlightAuto(s, o) { + o = o || L.languages || Object.keys(i); + const u = (function justTextHighlightResult(s) { + const o = { + relevance: 0, + emitter: new L.__emitter(L), + value: we(s), + illegal: !1, + top: j + }; + return (o.emitter.addText(s), o); + })(s), + _ = o + .filter(getLanguage) + .filter(autoDetection) + .map((o) => _highlight(o, s, !1)); + _.unshift(u); + const w = _.sort((s, o) => { + if (s.relevance !== o.relevance) return o.relevance - s.relevance; + if (s.language && o.language) { + if (getLanguage(s.language).supersetOf === o.language) return 1; + if (getLanguage(o.language).supersetOf === s.language) return -1; + } + return 0; + }), + [x, C] = w, + B = x; + return ((B.second_best = C), B); + } + const B = { + 'before:highlightElement': ({ el: s }) => { + L.useBR && + (s.innerHTML = s.innerHTML.replace(/\n/g, '').replace(//g, '\n')); + }, + 'after:highlightElement': ({ result: s }) => { + L.useBR && (s.value = s.value.replace(/\n/g, '
      ')); + } + }, + $ = /^(<[^>]+>|\t)+/gm, + V = { + 'after:highlightElement': ({ result: s }) => { + L.tabReplace && + (s.value = s.value.replace($, (s) => s.replace(/\t/g, L.tabReplace))); + } + }; + function highlightElement(s) { + let o = null; + const i = (function blockLanguage(s) { + let o = s.className + ' '; + o += s.parentNode ? s.parentNode.className : ''; + const i = L.languageDetectRe.exec(o); + if (i) { + const o = getLanguage(i[1]); + return ( + o || + (warn(C.replace('{}', i[1])), + warn('Falling back to no-highlight mode for this block.', s)), + o ? i[1] : 'no-highlight' + ); + } + return o.split(/\s+/).find((s) => shouldNotHighlight(s) || getLanguage(s)); + })(s); + if (shouldNotHighlight(i)) return; + (fire('before:highlightElement', { el: s, language: i }), (o = s)); + const _ = o.textContent, + w = i ? highlight(_, { language: i, ignoreIllegals: !0 }) : highlightAuto(_); + (fire('after:highlightElement', { el: s, result: w, text: _ }), + (s.innerHTML = w.value), + (function updateClassName(s, o, i) { + const _ = o ? u[o] : i; + (s.classList.add('hljs'), _ && s.classList.add(_)); + })(s, i, w.language), + (s.result = { language: w.language, re: w.relevance, relavance: w.relevance }), + w.second_best && + (s.second_best = { + language: w.second_best.language, + re: w.second_best.relevance, + relavance: w.second_best.relevance + })); + } + const initHighlighting = () => { + if (initHighlighting.called) return; + ((initHighlighting.called = !0), + deprecated( + '10.6.0', + 'initHighlighting() is deprecated. Use highlightAll() instead.' + )); + document.querySelectorAll('pre code').forEach(highlightElement); + }; + let U = !1; + function highlightAll() { + if ('loading' === document.readyState) return void (U = !0); + document.querySelectorAll('pre code').forEach(highlightElement); + } + function getLanguage(s) { + return ((s = (s || '').toLowerCase()), i[s] || i[u[s]]); + } + function registerAliases(s, { languageName: o }) { + ('string' == typeof s && (s = [s]), + s.forEach((s) => { + u[s.toLowerCase()] = o; + })); + } + function autoDetection(s) { + const o = getLanguage(s); + return o && !o.disableAutodetect; + } + function fire(s, o) { + const i = s; + _.forEach(function (s) { + s[i] && s[i](o); + }); + } + ('undefined' != typeof window && + window.addEventListener && + window.addEventListener( + 'DOMContentLoaded', + function boot() { + U && highlightAll(); + }, + !1 + ), + Object.assign(s, { + highlight, + highlightAuto, + highlightAll, + fixMarkup: function deprecateFixMarkup(s) { + return ( + deprecated('10.2.0', 'fixMarkup will be removed entirely in v11.0'), + deprecated( + '10.2.0', + 'Please see https://github.com/highlightjs/highlight.js/issues/2534' + ), + (function fixMarkup(s) { + return L.tabReplace || L.useBR + ? s.replace(x, (s) => + '\n' === s + ? L.useBR + ? '
      ' + : s + : L.tabReplace + ? s.replace(/\t/g, L.tabReplace) + : s + ) + : s; + })(s) + ); + }, + highlightElement, + highlightBlock: function deprecateHighlightBlock(s) { + return ( + deprecated('10.7.0', 'highlightBlock will be removed entirely in v12.0'), + deprecated('10.7.0', 'Please use highlightElement now.'), + highlightElement(s) + ); + }, + configure: function configure(s) { + (s.useBR && + (deprecated('10.3.0', "'useBR' will be removed entirely in v11.0"), + deprecated( + '10.3.0', + 'Please see https://github.com/highlightjs/highlight.js/issues/2559' + )), + (L = Se(L, s))); + }, + initHighlighting, + initHighlightingOnLoad: function initHighlightingOnLoad() { + (deprecated( + '10.6.0', + 'initHighlightingOnLoad() is deprecated. Use highlightAll() instead.' + ), + (U = !0)); + }, + registerLanguage: function registerLanguage(o, u) { + let _ = null; + try { + _ = u(s); + } catch (s) { + if ( + (error( + "Language definition for '{}' could not be registered.".replace('{}', o) + ), + !w) + ) + throw s; + (error(s), (_ = j)); + } + (_.name || (_.name = o), + (i[o] = _), + (_.rawDefinition = u.bind(null, s)), + _.aliases && registerAliases(_.aliases, { languageName: o })); + }, + unregisterLanguage: function unregisterLanguage(s) { + delete i[s]; + for (const o of Object.keys(u)) u[o] === s && delete u[o]; + }, + listLanguages: function listLanguages() { + return Object.keys(i); + }, + getLanguage, + registerAliases, + requireLanguage: function requireLanguage(s) { + (deprecated('10.4.0', 'requireLanguage will be removed entirely in v11.'), + deprecated( + '10.4.0', + 'Please see https://github.com/highlightjs/highlight.js/pull/2844' + )); + const o = getLanguage(s); + if (o) return o; + throw new Error( + "The '{}' language is required, but not loaded.".replace('{}', s) + ); + }, + autoDetection, + inherit: Se, + addPlugin: function addPlugin(s) { + (!(function upgradePluginAPI(s) { + (s['before:highlightBlock'] && + !s['before:highlightElement'] && + (s['before:highlightElement'] = (o) => { + s['before:highlightBlock'](Object.assign({ block: o.el }, o)); + }), + s['after:highlightBlock'] && + !s['after:highlightElement'] && + (s['after:highlightElement'] = (o) => { + s['after:highlightBlock'](Object.assign({ block: o.el }, o)); + })); + })(s), + _.push(s)); + }, + vuePlugin: BuildVuePlugin(s).VuePlugin + }), + (s.debugMode = function () { + w = !1; + }), + (s.safeMode = function () { + w = !0; + }), + (s.versionString = '10.7.3')); + for (const s in fe) 'object' == typeof fe[s] && o(fe[s]); + return (Object.assign(s, fe), s.addPlugin(B), s.addPlugin(be), s.addPlugin(V), s); + })({}); + s.exports = Pe; + }, + 35344: (s) => { + function concat(...s) { + return s + .map((s) => + (function source(s) { + return s ? ('string' == typeof s ? s : s.source) : null; + })(s) + ) + .join(''); + } + s.exports = function bash(s) { + const o = {}, + i = { begin: /\$\{/, end: /\}/, contains: ['self', { begin: /:-/, contains: [o] }] }; + Object.assign(o, { + className: 'variable', + variants: [{ begin: concat(/\$[\w\d#@][\w\d_]*/, '(?![\\w\\d])(?![$])') }, i] + }); + const u = { + className: 'subst', + begin: /\$\(/, + end: /\)/, + contains: [s.BACKSLASH_ESCAPE] + }, + _ = { + begin: /<<-?\s*(?=\w+)/, + starts: { + contains: [ + s.END_SAME_AS_BEGIN({ begin: /(\w+)/, end: /(\w+)/, className: 'string' }) + ] + } + }, + w = { + className: 'string', + begin: /"/, + end: /"/, + contains: [s.BACKSLASH_ESCAPE, o, u] + }; + u.contains.push(w); + const x = { + begin: /\$\(\(/, + end: /\)\)/, + contains: [{ begin: /\d+#[0-9a-f]+/, className: 'number' }, s.NUMBER_MODE, o] + }, + C = s.SHEBANG({ + binary: `(${['fish', 'bash', 'zsh', 'sh', 'csh', 'ksh', 'tcsh', 'dash', 'scsh'].join('|')})`, + relevance: 10 + }), + j = { + className: 'function', + begin: /\w[\w\d_]*\s*\(\s*\)\s*\{/, + returnBegin: !0, + contains: [s.inherit(s.TITLE_MODE, { begin: /\w[\w\d_]*/ })], + relevance: 0 + }; + return { + name: 'Bash', + aliases: ['sh', 'zsh'], + keywords: { + $pattern: /\b[a-z._-]+\b/, + keyword: 'if then else elif fi for while in do done case esac function', + literal: 'true false', + built_in: + 'break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp' + }, + contains: [ + C, + s.SHEBANG(), + j, + x, + s.HASH_COMMENT_MODE, + _, + w, + { className: '', begin: /\\"/ }, + { className: 'string', begin: /'/, end: /'/ }, + o + ] + }; + }; + }, + 73402: (s) => { + function concat(...s) { + return s + .map((s) => + (function source(s) { + return s ? ('string' == typeof s ? s : s.source) : null; + })(s) + ) + .join(''); + } + s.exports = function http(s) { + const o = 'HTTP/(2|1\\.[01])', + i = { + className: 'attribute', + begin: concat('^', /[A-Za-z][A-Za-z0-9-]*/, '(?=\\:\\s)'), + starts: { + contains: [ + { + className: 'punctuation', + begin: /: /, + relevance: 0, + starts: { end: '$', relevance: 0 } + } + ] + } + }, + u = [i, { begin: '\\n\\n', starts: { subLanguage: [], endsWithParent: !0 } }]; + return { + name: 'HTTP', + aliases: ['https'], + illegal: /\S/, + contains: [ + { + begin: '^(?=' + o + ' \\d{3})', + end: /$/, + contains: [ + { className: 'meta', begin: o }, + { className: 'number', begin: '\\b\\d{3}\\b' } + ], + starts: { end: /\b\B/, illegal: /\S/, contains: u } + }, + { + begin: '(?=^[A-Z]+ (.*?) ' + o + '$)', + end: /$/, + contains: [ + { className: 'string', begin: ' ', end: ' ', excludeBegin: !0, excludeEnd: !0 }, + { className: 'meta', begin: o }, + { className: 'keyword', begin: '[A-Z]+' } + ], + starts: { end: /\b\B/, illegal: /\S/, contains: u } + }, + s.inherit(i, { relevance: 0 }) + ] + }; + }; + }, + 95089: (s) => { + const o = '[A-Za-z$_][0-9A-Za-z$_]*', + i = [ + 'as', + 'in', + 'of', + 'if', + 'for', + 'while', + 'finally', + 'var', + 'new', + 'function', + 'do', + 'return', + 'void', + 'else', + 'break', + 'catch', + 'instanceof', + 'with', + 'throw', + 'case', + 'default', + 'try', + 'switch', + 'continue', + 'typeof', + 'delete', + 'let', + 'yield', + 'const', + 'class', + 'debugger', + 'async', + 'await', + 'static', + 'import', + 'from', + 'export', + 'extends' + ], + u = ['true', 'false', 'null', 'undefined', 'NaN', 'Infinity'], + _ = [].concat( + [ + 'setInterval', + 'setTimeout', + 'clearInterval', + 'clearTimeout', + 'require', + 'exports', + 'eval', + 'isFinite', + 'isNaN', + 'parseFloat', + 'parseInt', + 'decodeURI', + 'decodeURIComponent', + 'encodeURI', + 'encodeURIComponent', + 'escape', + 'unescape' + ], + [ + 'arguments', + 'this', + 'super', + 'console', + 'window', + 'document', + 'localStorage', + 'module', + 'global' + ], + [ + 'Intl', + 'DataView', + 'Number', + 'Math', + 'Date', + 'String', + 'RegExp', + 'Object', + 'Function', + 'Boolean', + 'Error', + 'Symbol', + 'Set', + 'Map', + 'WeakSet', + 'WeakMap', + 'Proxy', + 'Reflect', + 'JSON', + 'Promise', + 'Float64Array', + 'Int16Array', + 'Int32Array', + 'Int8Array', + 'Uint16Array', + 'Uint32Array', + 'Float32Array', + 'Array', + 'Uint8Array', + 'Uint8ClampedArray', + 'ArrayBuffer', + 'BigInt64Array', + 'BigUint64Array', + 'BigInt' + ], + [ + 'EvalError', + 'InternalError', + 'RangeError', + 'ReferenceError', + 'SyntaxError', + 'TypeError', + 'URIError' + ] + ); + function lookahead(s) { + return concat('(?=', s, ')'); + } + function concat(...s) { + return s + .map((s) => + (function source(s) { + return s ? ('string' == typeof s ? s : s.source) : null; + })(s) + ) + .join(''); + } + s.exports = function javascript(s) { + const w = o, + x = '<>', + C = '', + j = { + begin: /<[A-Za-z0-9\\._:-]+/, + end: /\/[A-Za-z0-9\\._:-]+>|\/>/, + isTrulyOpeningTag: (s, o) => { + const i = s[0].length + s.index, + u = s.input[i]; + '<' !== u + ? '>' === u && + (((s, { after: o }) => { + const i = '', + returnBegin: !0, + end: '\\s*=>', + contains: [ + { + className: 'params', + variants: [ + { begin: s.UNDERSCORE_IDENT_RE, relevance: 0 }, + { className: null, begin: /\(\s*\)/, skip: !0 }, + { + begin: /\(/, + end: /\)/, + excludeBegin: !0, + excludeEnd: !0, + keywords: L, + contains: ce + } + ] + } + ] + }, + { begin: /,/, relevance: 0 }, + { className: '', begin: /\s/, end: /\s*/, skip: !0 }, + { + variants: [ + { begin: x, end: C }, + { begin: j.begin, 'on:begin': j.isTrulyOpeningTag, end: j.end } + ], + subLanguage: 'xml', + contains: [{ begin: j.begin, end: j.end, skip: !0, contains: ['self'] }] + } + ], + relevance: 0 + }, + { + className: 'function', + beginKeywords: 'function', + end: /[{;]/, + excludeEnd: !0, + keywords: L, + contains: ['self', s.inherit(s.TITLE_MODE, { begin: w }), pe], + illegal: /%/ + }, + { beginKeywords: 'while if switch catch for' }, + { + className: 'function', + begin: + s.UNDERSCORE_IDENT_RE + + '\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{', + returnBegin: !0, + contains: [pe, s.inherit(s.TITLE_MODE, { begin: w })] + }, + { variants: [{ begin: '\\.' + w }, { begin: '\\$' + w }], relevance: 0 }, + { + className: 'class', + beginKeywords: 'class', + end: /[{;=]/, + excludeEnd: !0, + illegal: /[:"[\]]/, + contains: [{ beginKeywords: 'extends' }, s.UNDERSCORE_TITLE_MODE] + }, + { + begin: /\b(?=constructor)/, + end: /[{;]/, + excludeEnd: !0, + contains: [s.inherit(s.TITLE_MODE, { begin: w }), 'self', pe] + }, + { + begin: '(get|set)\\s+(?=' + w + '\\()', + end: /\{/, + keywords: 'get set', + contains: [s.inherit(s.TITLE_MODE, { begin: w }), { begin: /\(\)/ }, pe] + }, + { begin: /\$[(.]/ } + ] + }; + }; + }, + 65772: (s) => { + s.exports = function json(s) { + const o = { literal: 'true false null' }, + i = [s.C_LINE_COMMENT_MODE, s.C_BLOCK_COMMENT_MODE], + u = [s.QUOTE_STRING_MODE, s.C_NUMBER_MODE], + _ = { end: ',', endsWithParent: !0, excludeEnd: !0, contains: u, keywords: o }, + w = { + begin: /\{/, + end: /\}/, + contains: [ + { + className: 'attr', + begin: /"/, + end: /"/, + contains: [s.BACKSLASH_ESCAPE], + illegal: '\\n' + }, + s.inherit(_, { begin: /:/ }) + ].concat(i), + illegal: '\\S' + }, + x = { begin: '\\[', end: '\\]', contains: [s.inherit(_)], illegal: '\\S' }; + return ( + u.push(w, x), + i.forEach(function (s) { + u.push(s); + }), + { name: 'JSON', contains: u, keywords: o, illegal: '\\S' } + ); + }; + }, + 26571: (s) => { + s.exports = function powershell(s) { + const o = { + $pattern: /-?[A-z\.\-]+\b/, + keyword: + 'if else foreach return do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch hidden static parameter', + built_in: + 'ac asnp cat cd CFS chdir clc clear clhy cli clp cls clv cnsn compare copy cp cpi cpp curl cvpa dbp del diff dir dnsn ebp echo|0 epal epcsv epsn erase etsn exsn fc fhx fl ft fw gal gbp gc gcb gci gcm gcs gdr gerr ghy gi gin gjb gl gm gmo gp gps gpv group gsn gsnp gsv gtz gu gv gwmi h history icm iex ihy ii ipal ipcsv ipmo ipsn irm ise iwmi iwr kill lp ls man md measure mi mount move mp mv nal ndr ni nmo npssc nsn nv ogv oh popd ps pushd pwd r rbp rcjb rcsn rd rdr ren ri rjb rm rmdir rmo rni rnp rp rsn rsnp rujb rv rvpa rwmi sajb sal saps sasv sbp sc scb select set shcm si sl sleep sls sort sp spjb spps spsv start stz sujb sv swmi tee trcm type wget where wjb write' + }, + i = { begin: '`[\\s\\S]', relevance: 0 }, + u = { + className: 'variable', + variants: [ + { begin: /\$\B/ }, + { className: 'keyword', begin: /\$this/ }, + { begin: /\$[\w\d][\w\d_:]*/ } + ] + }, + _ = { + className: 'string', + variants: [ + { begin: /"/, end: /"/ }, + { begin: /@"/, end: /^"@/ } + ], + contains: [i, u, { className: 'variable', begin: /\$[A-z]/, end: /[^A-z]/ }] + }, + w = { + className: 'string', + variants: [ + { begin: /'/, end: /'/ }, + { begin: /@'/, end: /^'@/ } + ] + }, + x = s.inherit(s.COMMENT(null, null), { + variants: [ + { begin: /#/, end: /$/ }, + { begin: /<#/, end: /#>/ } + ], + contains: [ + { + className: 'doctag', + variants: [ + { + begin: + /\.(synopsis|description|example|inputs|outputs|notes|link|component|role|functionality)/ + }, + { + begin: + /\.(parameter|forwardhelptargetname|forwardhelpcategory|remotehelprunspace|externalhelp)\s+\S+/ + } + ] + } + ] + }), + C = { + className: 'built_in', + variants: [ + { + begin: '('.concat( + 'Add|Clear|Close|Copy|Enter|Exit|Find|Format|Get|Hide|Join|Lock|Move|New|Open|Optimize|Pop|Push|Redo|Remove|Rename|Reset|Resize|Search|Select|Set|Show|Skip|Split|Step|Switch|Undo|Unlock|Watch|Backup|Checkpoint|Compare|Compress|Convert|ConvertFrom|ConvertTo|Dismount|Edit|Expand|Export|Group|Import|Initialize|Limit|Merge|Mount|Out|Publish|Restore|Save|Sync|Unpublish|Update|Approve|Assert|Build|Complete|Confirm|Deny|Deploy|Disable|Enable|Install|Invoke|Register|Request|Restart|Resume|Start|Stop|Submit|Suspend|Uninstall|Unregister|Wait|Debug|Measure|Ping|Repair|Resolve|Test|Trace|Connect|Disconnect|Read|Receive|Send|Write|Block|Grant|Protect|Revoke|Unblock|Unprotect|Use|ForEach|Sort|Tee|Where', + ')+(-)[\\w\\d]+' + ) + } + ] + }, + j = { + className: 'class', + beginKeywords: 'class enum', + end: /\s*[{]/, + excludeEnd: !0, + relevance: 0, + contains: [s.TITLE_MODE] + }, + L = { + className: 'function', + begin: /function\s+/, + end: /\s*\{|$/, + excludeEnd: !0, + returnBegin: !0, + relevance: 0, + contains: [ + { begin: 'function', relevance: 0, className: 'keyword' }, + { className: 'title', begin: /\w[\w\d]*((-)[\w\d]+)*/, relevance: 0 }, + { begin: /\(/, end: /\)/, className: 'params', relevance: 0, contains: [u] } + ] + }, + B = { + begin: /using\s/, + end: /$/, + returnBegin: !0, + contains: [ + _, + w, + { className: 'keyword', begin: /(using|assembly|command|module|namespace|type)/ } + ] + }, + $ = { + variants: [ + { + className: 'operator', + begin: '('.concat( + '-and|-as|-band|-bnot|-bor|-bxor|-casesensitive|-ccontains|-ceq|-cge|-cgt|-cle|-clike|-clt|-cmatch|-cne|-cnotcontains|-cnotlike|-cnotmatch|-contains|-creplace|-csplit|-eq|-exact|-f|-file|-ge|-gt|-icontains|-ieq|-ige|-igt|-ile|-ilike|-ilt|-imatch|-in|-ine|-inotcontains|-inotlike|-inotmatch|-ireplace|-is|-isnot|-isplit|-join|-le|-like|-lt|-match|-ne|-not|-notcontains|-notin|-notlike|-notmatch|-or|-regex|-replace|-shl|-shr|-split|-wildcard|-xor', + ')\\b' + ) + }, + { className: 'literal', begin: /(-)[\w\d]+/, relevance: 0 } + ] + }, + V = { + className: 'function', + begin: /\[.*\]\s*[\w]+[ ]??\(/, + end: /$/, + returnBegin: !0, + relevance: 0, + contains: [ + { + className: 'keyword', + begin: '('.concat(o.keyword.toString().replace(/\s/g, '|'), ')\\b'), + endsParent: !0, + relevance: 0 + }, + s.inherit(s.TITLE_MODE, { endsParent: !0 }) + ] + }, + U = [ + V, + x, + i, + s.NUMBER_MODE, + _, + w, + C, + u, + { className: 'literal', begin: /\$(null|true|false)\b/ }, + { className: 'selector-tag', begin: /@\B/, relevance: 0 } + ], + z = { + begin: /\[/, + end: /\]/, + excludeBegin: !0, + excludeEnd: !0, + relevance: 0, + contains: [].concat( + 'self', + U, + { + begin: + '(' + + [ + 'string', + 'char', + 'byte', + 'int', + 'long', + 'bool', + 'decimal', + 'single', + 'double', + 'DateTime', + 'xml', + 'array', + 'hashtable', + 'void' + ].join('|') + + ')', + className: 'built_in', + relevance: 0 + }, + { className: 'type', begin: /[\.\w\d]+/, relevance: 0 } + ) + }; + return ( + V.contains.unshift(z), + { + name: 'PowerShell', + aliases: ['ps', 'ps1'], + case_insensitive: !0, + keywords: o, + contains: U.concat(j, L, B, $, z) + } + ); + }; + }, + 17285: (s) => { + function source(s) { + return s ? ('string' == typeof s ? s : s.source) : null; + } + function lookahead(s) { + return concat('(?=', s, ')'); + } + function concat(...s) { + return s.map((s) => source(s)).join(''); + } + function either(...s) { + return '(' + s.map((s) => source(s)).join('|') + ')'; + } + s.exports = function xml(s) { + const o = concat( + /[A-Z_]/, + (function optional(s) { + return concat('(', s, ')?'); + })(/[A-Z0-9_.-]*:/), + /[A-Z0-9_.-]*/ + ), + i = { className: 'symbol', begin: /&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/ }, + u = { + begin: /\s/, + contains: [ + { className: 'meta-keyword', begin: /#?[a-z_][a-z1-9_-]+/, illegal: /\n/ } + ] + }, + _ = s.inherit(u, { begin: /\(/, end: /\)/ }), + w = s.inherit(s.APOS_STRING_MODE, { className: 'meta-string' }), + x = s.inherit(s.QUOTE_STRING_MODE, { className: 'meta-string' }), + C = { + endsWithParent: !0, + illegal: /`]+/ } + ] + } + ] + } + ] + }; + return { + name: 'HTML, XML', + aliases: ['html', 'xhtml', 'rss', 'atom', 'xjb', 'xsd', 'xsl', 'plist', 'wsf', 'svg'], + case_insensitive: !0, + contains: [ + { + className: 'meta', + begin: //, + relevance: 10, + contains: [ + u, + x, + w, + _, + { + begin: /\[/, + end: /\]/, + contains: [ + { className: 'meta', begin: //, contains: [u, _, x, w] } + ] + } + ] + }, + s.COMMENT(//, { relevance: 10 }), + { begin: //, relevance: 10 }, + i, + { className: 'meta', begin: /<\?xml/, end: /\?>/, relevance: 10 }, + { + className: 'tag', + begin: /)/, + end: />/, + keywords: { name: 'style' }, + contains: [C], + starts: { end: /<\/style>/, returnEnd: !0, subLanguage: ['css', 'xml'] } + }, + { + className: 'tag', + begin: /)/, + end: />/, + keywords: { name: 'script' }, + contains: [C], + starts: { + end: /<\/script>/, + returnEnd: !0, + subLanguage: ['javascript', 'handlebars', 'xml'] + } + }, + { className: 'tag', begin: /<>|<\/>/ }, + { + className: 'tag', + begin: concat(//, />/, /\s/)))), + end: /\/?>/, + contains: [{ className: 'name', begin: o, relevance: 0, starts: C }] + }, + { + className: 'tag', + begin: concat(/<\//, lookahead(concat(o, />/))), + contains: [ + { className: 'name', begin: o, relevance: 0 }, + { begin: />/, relevance: 0, endsParent: !0 } + ] + } + ] + }; + }; + }, + 17533: (s) => { + s.exports = function yaml(s) { + var o = 'true false yes no null', + i = "[\\w#;/?:@&=+$,.~*'()[\\]]+", + u = { + className: 'string', + relevance: 0, + variants: [{ begin: /'/, end: /'/ }, { begin: /"/, end: /"/ }, { begin: /\S+/ }], + contains: [ + s.BACKSLASH_ESCAPE, + { + className: 'template-variable', + variants: [ + { begin: /\{\{/, end: /\}\}/ }, + { begin: /%\{/, end: /\}/ } + ] + } + ] + }, + _ = s.inherit(u, { + variants: [ + { begin: /'/, end: /'/ }, + { begin: /"/, end: /"/ }, + { begin: /[^\s,{}[\]]+/ } + ] + }), + w = { + className: 'number', + begin: + '\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b' + }, + x = { end: ',', endsWithParent: !0, excludeEnd: !0, keywords: o, relevance: 0 }, + C = { begin: /\{/, end: /\}/, contains: [x], illegal: '\\n', relevance: 0 }, + j = { begin: '\\[', end: '\\]', contains: [x], illegal: '\\n', relevance: 0 }, + L = [ + { + className: 'attr', + variants: [ + { begin: '\\w[\\w :\\/.-]*:(?=[ \t]|$)' }, + { begin: '"\\w[\\w :\\/.-]*":(?=[ \t]|$)' }, + { begin: "'\\w[\\w :\\/.-]*':(?=[ \t]|$)" } + ] + }, + { className: 'meta', begin: '^---\\s*$', relevance: 10 }, + { + className: 'string', + begin: '[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*' + }, + { + begin: '<%[%=-]?', + end: '[%-]?%>', + subLanguage: 'ruby', + excludeBegin: !0, + excludeEnd: !0, + relevance: 0 + }, + { className: 'type', begin: '!\\w+!' + i }, + { className: 'type', begin: '!<' + i + '>' }, + { className: 'type', begin: '!' + i }, + { className: 'type', begin: '!!' + i }, + { className: 'meta', begin: '&' + s.UNDERSCORE_IDENT_RE + '$' }, + { className: 'meta', begin: '\\*' + s.UNDERSCORE_IDENT_RE + '$' }, + { className: 'bullet', begin: '-(?=[ ]|$)', relevance: 0 }, + s.HASH_COMMENT_MODE, + { beginKeywords: o, keywords: { literal: o } }, + w, + { className: 'number', begin: s.C_NUMBER_RE + '\\b', relevance: 0 }, + C, + j, + u + ], + B = [...L]; + return ( + B.pop(), + B.push(_), + (x.contains = B), + { name: 'YAML', case_insensitive: !0, aliases: ['yml'], contains: L } + ); + }; + }, + 251: (s, o) => { + ((o.read = function (s, o, i, u, _) { + var w, + x, + C = 8 * _ - u - 1, + j = (1 << C) - 1, + L = j >> 1, + B = -7, + $ = i ? _ - 1 : 0, + V = i ? -1 : 1, + U = s[o + $]; + for ( + $ += V, w = U & ((1 << -B) - 1), U >>= -B, B += C; + B > 0; + w = 256 * w + s[o + $], $ += V, B -= 8 + ); + for ( + x = w & ((1 << -B) - 1), w >>= -B, B += u; + B > 0; + x = 256 * x + s[o + $], $ += V, B -= 8 + ); + if (0 === w) w = 1 - L; + else { + if (w === j) return x ? NaN : (1 / 0) * (U ? -1 : 1); + ((x += Math.pow(2, u)), (w -= L)); + } + return (U ? -1 : 1) * x * Math.pow(2, w - u); + }), + (o.write = function (s, o, i, u, _, w) { + var x, + C, + j, + L = 8 * w - _ - 1, + B = (1 << L) - 1, + $ = B >> 1, + V = 23 === _ ? Math.pow(2, -24) - Math.pow(2, -77) : 0, + U = u ? 0 : w - 1, + z = u ? 1 : -1, + Y = o < 0 || (0 === o && 1 / o < 0) ? 1 : 0; + for ( + o = Math.abs(o), + isNaN(o) || o === 1 / 0 + ? ((C = isNaN(o) ? 1 : 0), (x = B)) + : ((x = Math.floor(Math.log(o) / Math.LN2)), + o * (j = Math.pow(2, -x)) < 1 && (x--, (j *= 2)), + (o += x + $ >= 1 ? V / j : V * Math.pow(2, 1 - $)) * j >= 2 && + (x++, (j /= 2)), + x + $ >= B + ? ((C = 0), (x = B)) + : x + $ >= 1 + ? ((C = (o * j - 1) * Math.pow(2, _)), (x += $)) + : ((C = o * Math.pow(2, $ - 1) * Math.pow(2, _)), (x = 0))); + _ >= 8; + s[i + U] = 255 & C, U += z, C /= 256, _ -= 8 + ); + for (x = (x << _) | C, L += _; L > 0; s[i + U] = 255 & x, U += z, x /= 256, L -= 8); + s[i + U - z] |= 128 * Y; + })); + }, + 9404: function (s) { + s.exports = (function () { + 'use strict'; + var s = Array.prototype.slice; + function createClass(s, o) { + (o && (s.prototype = Object.create(o.prototype)), (s.prototype.constructor = s)); + } + function Iterable(s) { + return isIterable(s) ? s : Seq(s); + } + function KeyedIterable(s) { + return isKeyed(s) ? s : KeyedSeq(s); + } + function IndexedIterable(s) { + return isIndexed(s) ? s : IndexedSeq(s); + } + function SetIterable(s) { + return isIterable(s) && !isAssociative(s) ? s : SetSeq(s); + } + function isIterable(s) { + return !(!s || !s[o]); + } + function isKeyed(s) { + return !(!s || !s[i]); + } + function isIndexed(s) { + return !(!s || !s[u]); + } + function isAssociative(s) { + return isKeyed(s) || isIndexed(s); + } + function isOrdered(s) { + return !(!s || !s[_]); + } + (createClass(KeyedIterable, Iterable), + createClass(IndexedIterable, Iterable), + createClass(SetIterable, Iterable), + (Iterable.isIterable = isIterable), + (Iterable.isKeyed = isKeyed), + (Iterable.isIndexed = isIndexed), + (Iterable.isAssociative = isAssociative), + (Iterable.isOrdered = isOrdered), + (Iterable.Keyed = KeyedIterable), + (Iterable.Indexed = IndexedIterable), + (Iterable.Set = SetIterable)); + var o = '@@__IMMUTABLE_ITERABLE__@@', + i = '@@__IMMUTABLE_KEYED__@@', + u = '@@__IMMUTABLE_INDEXED__@@', + _ = '@@__IMMUTABLE_ORDERED__@@', + w = 'delete', + x = 5, + C = 1 << x, + j = C - 1, + L = {}, + B = { value: !1 }, + $ = { value: !1 }; + function MakeRef(s) { + return ((s.value = !1), s); + } + function SetRef(s) { + s && (s.value = !0); + } + function OwnerID() {} + function arrCopy(s, o) { + o = o || 0; + for (var i = Math.max(0, s.length - o), u = new Array(i), _ = 0; _ < i; _++) + u[_] = s[_ + o]; + return u; + } + function ensureSize(s) { + return (void 0 === s.size && (s.size = s.__iterate(returnTrue)), s.size); + } + function wrapIndex(s, o) { + if ('number' != typeof o) { + var i = o >>> 0; + if ('' + i !== o || 4294967295 === i) return NaN; + o = i; + } + return o < 0 ? ensureSize(s) + o : o; + } + function returnTrue() { + return !0; + } + function wholeSlice(s, o, i) { + return ( + (0 === s || (void 0 !== i && s <= -i)) && (void 0 === o || (void 0 !== i && o >= i)) + ); + } + function resolveBegin(s, o) { + return resolveIndex(s, o, 0); + } + function resolveEnd(s, o) { + return resolveIndex(s, o, o); + } + function resolveIndex(s, o, i) { + return void 0 === s + ? i + : s < 0 + ? Math.max(0, o + s) + : void 0 === o + ? s + : Math.min(o, s); + } + var V = 0, + U = 1, + z = 2, + Y = 'function' == typeof Symbol && Symbol.iterator, + Z = '@@iterator', + ee = Y || Z; + function Iterator(s) { + this.next = s; + } + function iteratorValue(s, o, i, u) { + var _ = 0 === s ? o : 1 === s ? i : [o, i]; + return (u ? (u.value = _) : (u = { value: _, done: !1 }), u); + } + function iteratorDone() { + return { value: void 0, done: !0 }; + } + function hasIterator(s) { + return !!getIteratorFn(s); + } + function isIterator(s) { + return s && 'function' == typeof s.next; + } + function getIterator(s) { + var o = getIteratorFn(s); + return o && o.call(s); + } + function getIteratorFn(s) { + var o = s && ((Y && s[Y]) || s[Z]); + if ('function' == typeof o) return o; + } + function isArrayLike(s) { + return s && 'number' == typeof s.length; + } + function Seq(s) { + return null == s ? emptySequence() : isIterable(s) ? s.toSeq() : seqFromValue(s); + } + function KeyedSeq(s) { + return null == s + ? emptySequence().toKeyedSeq() + : isIterable(s) + ? isKeyed(s) + ? s.toSeq() + : s.fromEntrySeq() + : keyedSeqFromValue(s); + } + function IndexedSeq(s) { + return null == s + ? emptySequence() + : isIterable(s) + ? isKeyed(s) + ? s.entrySeq() + : s.toIndexedSeq() + : indexedSeqFromValue(s); + } + function SetSeq(s) { + return ( + null == s + ? emptySequence() + : isIterable(s) + ? isKeyed(s) + ? s.entrySeq() + : s + : indexedSeqFromValue(s) + ).toSetSeq(); + } + ((Iterator.prototype.toString = function () { + return '[Iterator]'; + }), + (Iterator.KEYS = V), + (Iterator.VALUES = U), + (Iterator.ENTRIES = z), + (Iterator.prototype.inspect = Iterator.prototype.toSource = + function () { + return this.toString(); + }), + (Iterator.prototype[ee] = function () { + return this; + }), + createClass(Seq, Iterable), + (Seq.of = function () { + return Seq(arguments); + }), + (Seq.prototype.toSeq = function () { + return this; + }), + (Seq.prototype.toString = function () { + return this.__toString('Seq {', '}'); + }), + (Seq.prototype.cacheResult = function () { + return ( + !this._cache && + this.__iterateUncached && + ((this._cache = this.entrySeq().toArray()), (this.size = this._cache.length)), + this + ); + }), + (Seq.prototype.__iterate = function (s, o) { + return seqIterate(this, s, o, !0); + }), + (Seq.prototype.__iterator = function (s, o) { + return seqIterator(this, s, o, !0); + }), + createClass(KeyedSeq, Seq), + (KeyedSeq.prototype.toKeyedSeq = function () { + return this; + }), + createClass(IndexedSeq, Seq), + (IndexedSeq.of = function () { + return IndexedSeq(arguments); + }), + (IndexedSeq.prototype.toIndexedSeq = function () { + return this; + }), + (IndexedSeq.prototype.toString = function () { + return this.__toString('Seq [', ']'); + }), + (IndexedSeq.prototype.__iterate = function (s, o) { + return seqIterate(this, s, o, !1); + }), + (IndexedSeq.prototype.__iterator = function (s, o) { + return seqIterator(this, s, o, !1); + }), + createClass(SetSeq, Seq), + (SetSeq.of = function () { + return SetSeq(arguments); + }), + (SetSeq.prototype.toSetSeq = function () { + return this; + }), + (Seq.isSeq = isSeq), + (Seq.Keyed = KeyedSeq), + (Seq.Set = SetSeq), + (Seq.Indexed = IndexedSeq)); + var ie, + ae, + le, + ce = '@@__IMMUTABLE_SEQ__@@'; + function ArraySeq(s) { + ((this._array = s), (this.size = s.length)); + } + function ObjectSeq(s) { + var o = Object.keys(s); + ((this._object = s), (this._keys = o), (this.size = o.length)); + } + function IterableSeq(s) { + ((this._iterable = s), (this.size = s.length || s.size)); + } + function IteratorSeq(s) { + ((this._iterator = s), (this._iteratorCache = [])); + } + function isSeq(s) { + return !(!s || !s[ce]); + } + function emptySequence() { + return ie || (ie = new ArraySeq([])); + } + function keyedSeqFromValue(s) { + var o = Array.isArray(s) + ? new ArraySeq(s).fromEntrySeq() + : isIterator(s) + ? new IteratorSeq(s).fromEntrySeq() + : hasIterator(s) + ? new IterableSeq(s).fromEntrySeq() + : 'object' == typeof s + ? new ObjectSeq(s) + : void 0; + if (!o) + throw new TypeError( + 'Expected Array or iterable object of [k, v] entries, or keyed object: ' + s + ); + return o; + } + function indexedSeqFromValue(s) { + var o = maybeIndexedSeqFromValue(s); + if (!o) throw new TypeError('Expected Array or iterable object of values: ' + s); + return o; + } + function seqFromValue(s) { + var o = maybeIndexedSeqFromValue(s) || ('object' == typeof s && new ObjectSeq(s)); + if (!o) + throw new TypeError( + 'Expected Array or iterable object of values, or keyed object: ' + s + ); + return o; + } + function maybeIndexedSeqFromValue(s) { + return isArrayLike(s) + ? new ArraySeq(s) + : isIterator(s) + ? new IteratorSeq(s) + : hasIterator(s) + ? new IterableSeq(s) + : void 0; + } + function seqIterate(s, o, i, u) { + var _ = s._cache; + if (_) { + for (var w = _.length - 1, x = 0; x <= w; x++) { + var C = _[i ? w - x : x]; + if (!1 === o(C[1], u ? C[0] : x, s)) return x + 1; + } + return x; + } + return s.__iterateUncached(o, i); + } + function seqIterator(s, o, i, u) { + var _ = s._cache; + if (_) { + var w = _.length - 1, + x = 0; + return new Iterator(function () { + var s = _[i ? w - x : x]; + return x++ > w ? iteratorDone() : iteratorValue(o, u ? s[0] : x - 1, s[1]); + }); + } + return s.__iteratorUncached(o, i); + } + function fromJS(s, o) { + return o ? fromJSWith(o, s, '', { '': s }) : fromJSDefault(s); + } + function fromJSWith(s, o, i, u) { + return Array.isArray(o) + ? s.call( + u, + i, + IndexedSeq(o).map(function (i, u) { + return fromJSWith(s, i, u, o); + }) + ) + : isPlainObj(o) + ? s.call( + u, + i, + KeyedSeq(o).map(function (i, u) { + return fromJSWith(s, i, u, o); + }) + ) + : o; + } + function fromJSDefault(s) { + return Array.isArray(s) + ? IndexedSeq(s).map(fromJSDefault).toList() + : isPlainObj(s) + ? KeyedSeq(s).map(fromJSDefault).toMap() + : s; + } + function isPlainObj(s) { + return s && (s.constructor === Object || void 0 === s.constructor); + } + function is(s, o) { + if (s === o || (s != s && o != o)) return !0; + if (!s || !o) return !1; + if ('function' == typeof s.valueOf && 'function' == typeof o.valueOf) { + if ((s = s.valueOf()) === (o = o.valueOf()) || (s != s && o != o)) return !0; + if (!s || !o) return !1; + } + return !( + 'function' != typeof s.equals || + 'function' != typeof o.equals || + !s.equals(o) + ); + } + function deepEqual(s, o) { + if (s === o) return !0; + if ( + !isIterable(o) || + (void 0 !== s.size && void 0 !== o.size && s.size !== o.size) || + (void 0 !== s.__hash && void 0 !== o.__hash && s.__hash !== o.__hash) || + isKeyed(s) !== isKeyed(o) || + isIndexed(s) !== isIndexed(o) || + isOrdered(s) !== isOrdered(o) + ) + return !1; + if (0 === s.size && 0 === o.size) return !0; + var i = !isAssociative(s); + if (isOrdered(s)) { + var u = s.entries(); + return ( + o.every(function (s, o) { + var _ = u.next().value; + return _ && is(_[1], s) && (i || is(_[0], o)); + }) && u.next().done + ); + } + var _ = !1; + if (void 0 === s.size) + if (void 0 === o.size) 'function' == typeof s.cacheResult && s.cacheResult(); + else { + _ = !0; + var w = s; + ((s = o), (o = w)); + } + var x = !0, + C = o.__iterate(function (o, u) { + if (i ? !s.has(o) : _ ? !is(o, s.get(u, L)) : !is(s.get(u, L), o)) + return ((x = !1), !1); + }); + return x && s.size === C; + } + function Repeat(s, o) { + if (!(this instanceof Repeat)) return new Repeat(s, o); + if ( + ((this._value = s), + (this.size = void 0 === o ? 1 / 0 : Math.max(0, o)), + 0 === this.size) + ) { + if (ae) return ae; + ae = this; + } + } + function invariant(s, o) { + if (!s) throw new Error(o); + } + function Range(s, o, i) { + if (!(this instanceof Range)) return new Range(s, o, i); + if ( + (invariant(0 !== i, 'Cannot step a Range by 0'), + (s = s || 0), + void 0 === o && (o = 1 / 0), + (i = void 0 === i ? 1 : Math.abs(i)), + o < s && (i = -i), + (this._start = s), + (this._end = o), + (this._step = i), + (this.size = Math.max(0, Math.ceil((o - s) / i - 1) + 1)), + 0 === this.size) + ) { + if (le) return le; + le = this; + } + } + function Collection() { + throw TypeError('Abstract'); + } + function KeyedCollection() {} + function IndexedCollection() {} + function SetCollection() {} + ((Seq.prototype[ce] = !0), + createClass(ArraySeq, IndexedSeq), + (ArraySeq.prototype.get = function (s, o) { + return this.has(s) ? this._array[wrapIndex(this, s)] : o; + }), + (ArraySeq.prototype.__iterate = function (s, o) { + for (var i = this._array, u = i.length - 1, _ = 0; _ <= u; _++) + if (!1 === s(i[o ? u - _ : _], _, this)) return _ + 1; + return _; + }), + (ArraySeq.prototype.__iterator = function (s, o) { + var i = this._array, + u = i.length - 1, + _ = 0; + return new Iterator(function () { + return _ > u ? iteratorDone() : iteratorValue(s, _, i[o ? u - _++ : _++]); + }); + }), + createClass(ObjectSeq, KeyedSeq), + (ObjectSeq.prototype.get = function (s, o) { + return void 0 === o || this.has(s) ? this._object[s] : o; + }), + (ObjectSeq.prototype.has = function (s) { + return this._object.hasOwnProperty(s); + }), + (ObjectSeq.prototype.__iterate = function (s, o) { + for (var i = this._object, u = this._keys, _ = u.length - 1, w = 0; w <= _; w++) { + var x = u[o ? _ - w : w]; + if (!1 === s(i[x], x, this)) return w + 1; + } + return w; + }), + (ObjectSeq.prototype.__iterator = function (s, o) { + var i = this._object, + u = this._keys, + _ = u.length - 1, + w = 0; + return new Iterator(function () { + var x = u[o ? _ - w : w]; + return w++ > _ ? iteratorDone() : iteratorValue(s, x, i[x]); + }); + }), + (ObjectSeq.prototype[_] = !0), + createClass(IterableSeq, IndexedSeq), + (IterableSeq.prototype.__iterateUncached = function (s, o) { + if (o) return this.cacheResult().__iterate(s, o); + var i = getIterator(this._iterable), + u = 0; + if (isIterator(i)) + for (var _; !(_ = i.next()).done && !1 !== s(_.value, u++, this); ); + return u; + }), + (IterableSeq.prototype.__iteratorUncached = function (s, o) { + if (o) return this.cacheResult().__iterator(s, o); + var i = getIterator(this._iterable); + if (!isIterator(i)) return new Iterator(iteratorDone); + var u = 0; + return new Iterator(function () { + var o = i.next(); + return o.done ? o : iteratorValue(s, u++, o.value); + }); + }), + createClass(IteratorSeq, IndexedSeq), + (IteratorSeq.prototype.__iterateUncached = function (s, o) { + if (o) return this.cacheResult().__iterate(s, o); + for (var i, u = this._iterator, _ = this._iteratorCache, w = 0; w < _.length; ) + if (!1 === s(_[w], w++, this)) return w; + for (; !(i = u.next()).done; ) { + var x = i.value; + if (((_[w] = x), !1 === s(x, w++, this))) break; + } + return w; + }), + (IteratorSeq.prototype.__iteratorUncached = function (s, o) { + if (o) return this.cacheResult().__iterator(s, o); + var i = this._iterator, + u = this._iteratorCache, + _ = 0; + return new Iterator(function () { + if (_ >= u.length) { + var o = i.next(); + if (o.done) return o; + u[_] = o.value; + } + return iteratorValue(s, _, u[_++]); + }); + }), + createClass(Repeat, IndexedSeq), + (Repeat.prototype.toString = function () { + return 0 === this.size + ? 'Repeat []' + : 'Repeat [ ' + this._value + ' ' + this.size + ' times ]'; + }), + (Repeat.prototype.get = function (s, o) { + return this.has(s) ? this._value : o; + }), + (Repeat.prototype.includes = function (s) { + return is(this._value, s); + }), + (Repeat.prototype.slice = function (s, o) { + var i = this.size; + return wholeSlice(s, o, i) + ? this + : new Repeat(this._value, resolveEnd(o, i) - resolveBegin(s, i)); + }), + (Repeat.prototype.reverse = function () { + return this; + }), + (Repeat.prototype.indexOf = function (s) { + return is(this._value, s) ? 0 : -1; + }), + (Repeat.prototype.lastIndexOf = function (s) { + return is(this._value, s) ? this.size : -1; + }), + (Repeat.prototype.__iterate = function (s, o) { + for (var i = 0; i < this.size; i++) + if (!1 === s(this._value, i, this)) return i + 1; + return i; + }), + (Repeat.prototype.__iterator = function (s, o) { + var i = this, + u = 0; + return new Iterator(function () { + return u < i.size ? iteratorValue(s, u++, i._value) : iteratorDone(); + }); + }), + (Repeat.prototype.equals = function (s) { + return s instanceof Repeat ? is(this._value, s._value) : deepEqual(s); + }), + createClass(Range, IndexedSeq), + (Range.prototype.toString = function () { + return 0 === this.size + ? 'Range []' + : 'Range [ ' + + this._start + + '...' + + this._end + + (1 !== this._step ? ' by ' + this._step : '') + + ' ]'; + }), + (Range.prototype.get = function (s, o) { + return this.has(s) ? this._start + wrapIndex(this, s) * this._step : o; + }), + (Range.prototype.includes = function (s) { + var o = (s - this._start) / this._step; + return o >= 0 && o < this.size && o === Math.floor(o); + }), + (Range.prototype.slice = function (s, o) { + return wholeSlice(s, o, this.size) + ? this + : ((s = resolveBegin(s, this.size)), + (o = resolveEnd(o, this.size)) <= s + ? new Range(0, 0) + : new Range(this.get(s, this._end), this.get(o, this._end), this._step)); + }), + (Range.prototype.indexOf = function (s) { + var o = s - this._start; + if (o % this._step == 0) { + var i = o / this._step; + if (i >= 0 && i < this.size) return i; + } + return -1; + }), + (Range.prototype.lastIndexOf = function (s) { + return this.indexOf(s); + }), + (Range.prototype.__iterate = function (s, o) { + for ( + var i = this.size - 1, + u = this._step, + _ = o ? this._start + i * u : this._start, + w = 0; + w <= i; + w++ + ) { + if (!1 === s(_, w, this)) return w + 1; + _ += o ? -u : u; + } + return w; + }), + (Range.prototype.__iterator = function (s, o) { + var i = this.size - 1, + u = this._step, + _ = o ? this._start + i * u : this._start, + w = 0; + return new Iterator(function () { + var x = _; + return ((_ += o ? -u : u), w > i ? iteratorDone() : iteratorValue(s, w++, x)); + }); + }), + (Range.prototype.equals = function (s) { + return s instanceof Range + ? this._start === s._start && this._end === s._end && this._step === s._step + : deepEqual(this, s); + }), + createClass(Collection, Iterable), + createClass(KeyedCollection, Collection), + createClass(IndexedCollection, Collection), + createClass(SetCollection, Collection), + (Collection.Keyed = KeyedCollection), + (Collection.Indexed = IndexedCollection), + (Collection.Set = SetCollection)); + var pe = + 'function' == typeof Math.imul && -2 === Math.imul(4294967295, 2) + ? Math.imul + : function imul(s, o) { + var i = 65535 & (s |= 0), + u = 65535 & (o |= 0); + return (i * u + ((((s >>> 16) * u + i * (o >>> 16)) << 16) >>> 0)) | 0; + }; + function smi(s) { + return ((s >>> 1) & 1073741824) | (3221225471 & s); + } + function hash(s) { + if (!1 === s || null == s) return 0; + if ('function' == typeof s.valueOf && (!1 === (s = s.valueOf()) || null == s)) + return 0; + if (!0 === s) return 1; + var o = typeof s; + if ('number' === o) { + if (s != s || s === 1 / 0) return 0; + var i = 0 | s; + for (i !== s && (i ^= 4294967295 * s); s > 4294967295; ) i ^= s /= 4294967295; + return smi(i); + } + if ('string' === o) return s.length > Se ? cachedHashString(s) : hashString(s); + if ('function' == typeof s.hashCode) return s.hashCode(); + if ('object' === o) return hashJSObj(s); + if ('function' == typeof s.toString) return hashString(s.toString()); + throw new Error('Value type ' + o + ' cannot be hashed.'); + } + function cachedHashString(s) { + var o = Te[s]; + return ( + void 0 === o && + ((o = hashString(s)), Pe === xe && ((Pe = 0), (Te = {})), Pe++, (Te[s] = o)), + o + ); + } + function hashString(s) { + for (var o = 0, i = 0; i < s.length; i++) o = (31 * o + s.charCodeAt(i)) | 0; + return smi(o); + } + function hashJSObj(s) { + var o; + if (be && void 0 !== (o = ye.get(s))) return o; + if (void 0 !== (o = s[we])) return o; + if (!fe) { + if (void 0 !== (o = s.propertyIsEnumerable && s.propertyIsEnumerable[we])) return o; + if (void 0 !== (o = getIENodeHash(s))) return o; + } + if (((o = ++_e), 1073741824 & _e && (_e = 0), be)) ye.set(s, o); + else { + if (void 0 !== de && !1 === de(s)) + throw new Error('Non-extensible objects are not allowed as keys.'); + if (fe) + Object.defineProperty(s, we, { + enumerable: !1, + configurable: !1, + writable: !1, + value: o + }); + else if ( + void 0 !== s.propertyIsEnumerable && + s.propertyIsEnumerable === s.constructor.prototype.propertyIsEnumerable + ) + ((s.propertyIsEnumerable = function () { + return this.constructor.prototype.propertyIsEnumerable.apply(this, arguments); + }), + (s.propertyIsEnumerable[we] = o)); + else { + if (void 0 === s.nodeType) + throw new Error('Unable to set a non-enumerable property on object.'); + s[we] = o; + } + } + return o; + } + var de = Object.isExtensible, + fe = (function () { + try { + return (Object.defineProperty({}, '@', {}), !0); + } catch (s) { + return !1; + } + })(); + function getIENodeHash(s) { + if (s && s.nodeType > 0) + switch (s.nodeType) { + case 1: + return s.uniqueID; + case 9: + return s.documentElement && s.documentElement.uniqueID; + } + } + var ye, + be = 'function' == typeof WeakMap; + be && (ye = new WeakMap()); + var _e = 0, + we = '__immutablehash__'; + 'function' == typeof Symbol && (we = Symbol(we)); + var Se = 16, + xe = 255, + Pe = 0, + Te = {}; + function assertNotInfinite(s) { + invariant(s !== 1 / 0, 'Cannot perform this action with an infinite size.'); + } + function Map(s) { + return null == s + ? emptyMap() + : isMap(s) && !isOrdered(s) + ? s + : emptyMap().withMutations(function (o) { + var i = KeyedIterable(s); + (assertNotInfinite(i.size), + i.forEach(function (s, i) { + return o.set(i, s); + })); + }); + } + function isMap(s) { + return !(!s || !s[qe]); + } + (createClass(Map, KeyedCollection), + (Map.of = function () { + var o = s.call(arguments, 0); + return emptyMap().withMutations(function (s) { + for (var i = 0; i < o.length; i += 2) { + if (i + 1 >= o.length) throw new Error('Missing value for key: ' + o[i]); + s.set(o[i], o[i + 1]); + } + }); + }), + (Map.prototype.toString = function () { + return this.__toString('Map {', '}'); + }), + (Map.prototype.get = function (s, o) { + return this._root ? this._root.get(0, void 0, s, o) : o; + }), + (Map.prototype.set = function (s, o) { + return updateMap(this, s, o); + }), + (Map.prototype.setIn = function (s, o) { + return this.updateIn(s, L, function () { + return o; + }); + }), + (Map.prototype.remove = function (s) { + return updateMap(this, s, L); + }), + (Map.prototype.deleteIn = function (s) { + return this.updateIn(s, function () { + return L; + }); + }), + (Map.prototype.update = function (s, o, i) { + return 1 === arguments.length ? s(this) : this.updateIn([s], o, i); + }), + (Map.prototype.updateIn = function (s, o, i) { + i || ((i = o), (o = void 0)); + var u = updateInDeepMap(this, forceIterator(s), o, i); + return u === L ? void 0 : u; + }), + (Map.prototype.clear = function () { + return 0 === this.size + ? this + : this.__ownerID + ? ((this.size = 0), + (this._root = null), + (this.__hash = void 0), + (this.__altered = !0), + this) + : emptyMap(); + }), + (Map.prototype.merge = function () { + return mergeIntoMapWith(this, void 0, arguments); + }), + (Map.prototype.mergeWith = function (o) { + return mergeIntoMapWith(this, o, s.call(arguments, 1)); + }), + (Map.prototype.mergeIn = function (o) { + var i = s.call(arguments, 1); + return this.updateIn(o, emptyMap(), function (s) { + return 'function' == typeof s.merge ? s.merge.apply(s, i) : i[i.length - 1]; + }); + }), + (Map.prototype.mergeDeep = function () { + return mergeIntoMapWith(this, deepMerger, arguments); + }), + (Map.prototype.mergeDeepWith = function (o) { + var i = s.call(arguments, 1); + return mergeIntoMapWith(this, deepMergerWith(o), i); + }), + (Map.prototype.mergeDeepIn = function (o) { + var i = s.call(arguments, 1); + return this.updateIn(o, emptyMap(), function (s) { + return 'function' == typeof s.mergeDeep + ? s.mergeDeep.apply(s, i) + : i[i.length - 1]; + }); + }), + (Map.prototype.sort = function (s) { + return OrderedMap(sortFactory(this, s)); + }), + (Map.prototype.sortBy = function (s, o) { + return OrderedMap(sortFactory(this, o, s)); + }), + (Map.prototype.withMutations = function (s) { + var o = this.asMutable(); + return (s(o), o.wasAltered() ? o.__ensureOwner(this.__ownerID) : this); + }), + (Map.prototype.asMutable = function () { + return this.__ownerID ? this : this.__ensureOwner(new OwnerID()); + }), + (Map.prototype.asImmutable = function () { + return this.__ensureOwner(); + }), + (Map.prototype.wasAltered = function () { + return this.__altered; + }), + (Map.prototype.__iterator = function (s, o) { + return new MapIterator(this, s, o); + }), + (Map.prototype.__iterate = function (s, o) { + var i = this, + u = 0; + return ( + this._root && + this._root.iterate(function (o) { + return (u++, s(o[1], o[0], i)); + }, o), + u + ); + }), + (Map.prototype.__ensureOwner = function (s) { + return s === this.__ownerID + ? this + : s + ? makeMap(this.size, this._root, s, this.__hash) + : ((this.__ownerID = s), (this.__altered = !1), this); + }), + (Map.isMap = isMap)); + var Re, + qe = '@@__IMMUTABLE_MAP__@@', + $e = Map.prototype; + function ArrayMapNode(s, o) { + ((this.ownerID = s), (this.entries = o)); + } + function BitmapIndexedNode(s, o, i) { + ((this.ownerID = s), (this.bitmap = o), (this.nodes = i)); + } + function HashArrayMapNode(s, o, i) { + ((this.ownerID = s), (this.count = o), (this.nodes = i)); + } + function HashCollisionNode(s, o, i) { + ((this.ownerID = s), (this.keyHash = o), (this.entries = i)); + } + function ValueNode(s, o, i) { + ((this.ownerID = s), (this.keyHash = o), (this.entry = i)); + } + function MapIterator(s, o, i) { + ((this._type = o), + (this._reverse = i), + (this._stack = s._root && mapIteratorFrame(s._root))); + } + function mapIteratorValue(s, o) { + return iteratorValue(s, o[0], o[1]); + } + function mapIteratorFrame(s, o) { + return { node: s, index: 0, __prev: o }; + } + function makeMap(s, o, i, u) { + var _ = Object.create($e); + return ( + (_.size = s), + (_._root = o), + (_.__ownerID = i), + (_.__hash = u), + (_.__altered = !1), + _ + ); + } + function emptyMap() { + return Re || (Re = makeMap(0)); + } + function updateMap(s, o, i) { + var u, _; + if (s._root) { + var w = MakeRef(B), + x = MakeRef($); + if (((u = updateNode(s._root, s.__ownerID, 0, void 0, o, i, w, x)), !x.value)) + return s; + _ = s.size + (w.value ? (i === L ? -1 : 1) : 0); + } else { + if (i === L) return s; + ((_ = 1), (u = new ArrayMapNode(s.__ownerID, [[o, i]]))); + } + return s.__ownerID + ? ((s.size = _), (s._root = u), (s.__hash = void 0), (s.__altered = !0), s) + : u + ? makeMap(_, u) + : emptyMap(); + } + function updateNode(s, o, i, u, _, w, x, C) { + return s + ? s.update(o, i, u, _, w, x, C) + : w === L + ? s + : (SetRef(C), SetRef(x), new ValueNode(o, u, [_, w])); + } + function isLeafNode(s) { + return s.constructor === ValueNode || s.constructor === HashCollisionNode; + } + function mergeIntoNode(s, o, i, u, _) { + if (s.keyHash === u) return new HashCollisionNode(o, u, [s.entry, _]); + var w, + C = (0 === i ? s.keyHash : s.keyHash >>> i) & j, + L = (0 === i ? u : u >>> i) & j; + return new BitmapIndexedNode( + o, + (1 << C) | (1 << L), + C === L + ? [mergeIntoNode(s, o, i + x, u, _)] + : ((w = new ValueNode(o, u, _)), C < L ? [s, w] : [w, s]) + ); + } + function createNodes(s, o, i, u) { + s || (s = new OwnerID()); + for (var _ = new ValueNode(s, hash(i), [i, u]), w = 0; w < o.length; w++) { + var x = o[w]; + _ = _.update(s, 0, void 0, x[0], x[1]); + } + return _; + } + function packNodes(s, o, i, u) { + for ( + var _ = 0, w = 0, x = new Array(i), C = 0, j = 1, L = o.length; + C < L; + C++, j <<= 1 + ) { + var B = o[C]; + void 0 !== B && C !== u && ((_ |= j), (x[w++] = B)); + } + return new BitmapIndexedNode(s, _, x); + } + function expandNodes(s, o, i, u, _) { + for (var w = 0, x = new Array(C), j = 0; 0 !== i; j++, i >>>= 1) + x[j] = 1 & i ? o[w++] : void 0; + return ((x[u] = _), new HashArrayMapNode(s, w + 1, x)); + } + function mergeIntoMapWith(s, o, i) { + for (var u = [], _ = 0; _ < i.length; _++) { + var w = i[_], + x = KeyedIterable(w); + (isIterable(w) || + (x = x.map(function (s) { + return fromJS(s); + })), + u.push(x)); + } + return mergeIntoCollectionWith(s, o, u); + } + function deepMerger(s, o, i) { + return s && s.mergeDeep && isIterable(o) ? s.mergeDeep(o) : is(s, o) ? s : o; + } + function deepMergerWith(s) { + return function (o, i, u) { + if (o && o.mergeDeepWith && isIterable(i)) return o.mergeDeepWith(s, i); + var _ = s(o, i, u); + return is(o, _) ? o : _; + }; + } + function mergeIntoCollectionWith(s, o, i) { + return 0 === + (i = i.filter(function (s) { + return 0 !== s.size; + })).length + ? s + : 0 !== s.size || s.__ownerID || 1 !== i.length + ? s.withMutations(function (s) { + for ( + var u = o + ? function (i, u) { + s.update(u, L, function (s) { + return s === L ? i : o(s, i, u); + }); + } + : function (o, i) { + s.set(i, o); + }, + _ = 0; + _ < i.length; + _++ + ) + i[_].forEach(u); + }) + : s.constructor(i[0]); + } + function updateInDeepMap(s, o, i, u) { + var _ = s === L, + w = o.next(); + if (w.done) { + var x = _ ? i : s, + C = u(x); + return C === x ? s : C; + } + invariant(_ || (s && s.set), 'invalid keyPath'); + var j = w.value, + B = _ ? L : s.get(j, L), + $ = updateInDeepMap(B, o, i, u); + return $ === B ? s : $ === L ? s.remove(j) : (_ ? emptyMap() : s).set(j, $); + } + function popCount(s) { + return ( + (s = + ((s = (858993459 & (s -= (s >> 1) & 1431655765)) + ((s >> 2) & 858993459)) + + (s >> 4)) & + 252645135), + (s += s >> 8), + 127 & (s += s >> 16) + ); + } + function setIn(s, o, i, u) { + var _ = u ? s : arrCopy(s); + return ((_[o] = i), _); + } + function spliceIn(s, o, i, u) { + var _ = s.length + 1; + if (u && o + 1 === _) return ((s[o] = i), s); + for (var w = new Array(_), x = 0, C = 0; C < _; C++) + C === o ? ((w[C] = i), (x = -1)) : (w[C] = s[C + x]); + return w; + } + function spliceOut(s, o, i) { + var u = s.length - 1; + if (i && o === u) return (s.pop(), s); + for (var _ = new Array(u), w = 0, x = 0; x < u; x++) + (x === o && (w = 1), (_[x] = s[x + w])); + return _; + } + (($e[qe] = !0), + ($e[w] = $e.remove), + ($e.removeIn = $e.deleteIn), + (ArrayMapNode.prototype.get = function (s, o, i, u) { + for (var _ = this.entries, w = 0, x = _.length; w < x; w++) + if (is(i, _[w][0])) return _[w][1]; + return u; + }), + (ArrayMapNode.prototype.update = function (s, o, i, u, _, w, x) { + for ( + var C = _ === L, j = this.entries, B = 0, $ = j.length; + B < $ && !is(u, j[B][0]); + B++ + ); + var V = B < $; + if (V ? j[B][1] === _ : C) return this; + if ((SetRef(x), (C || !V) && SetRef(w), !C || 1 !== j.length)) { + if (!V && !C && j.length >= ze) return createNodes(s, j, u, _); + var U = s && s === this.ownerID, + z = U ? j : arrCopy(j); + return ( + V + ? C + ? B === $ - 1 + ? z.pop() + : (z[B] = z.pop()) + : (z[B] = [u, _]) + : z.push([u, _]), + U ? ((this.entries = z), this) : new ArrayMapNode(s, z) + ); + } + }), + (BitmapIndexedNode.prototype.get = function (s, o, i, u) { + void 0 === o && (o = hash(i)); + var _ = 1 << ((0 === s ? o : o >>> s) & j), + w = this.bitmap; + return w & _ ? this.nodes[popCount(w & (_ - 1))].get(s + x, o, i, u) : u; + }), + (BitmapIndexedNode.prototype.update = function (s, o, i, u, _, w, C) { + void 0 === i && (i = hash(u)); + var B = (0 === o ? i : i >>> o) & j, + $ = 1 << B, + V = this.bitmap, + U = !!(V & $); + if (!U && _ === L) return this; + var z = popCount(V & ($ - 1)), + Y = this.nodes, + Z = U ? Y[z] : void 0, + ee = updateNode(Z, s, o + x, i, u, _, w, C); + if (ee === Z) return this; + if (!U && ee && Y.length >= We) return expandNodes(s, Y, V, B, ee); + if (U && !ee && 2 === Y.length && isLeafNode(Y[1 ^ z])) return Y[1 ^ z]; + if (U && ee && 1 === Y.length && isLeafNode(ee)) return ee; + var ie = s && s === this.ownerID, + ae = U ? (ee ? V : V ^ $) : V | $, + le = U + ? ee + ? setIn(Y, z, ee, ie) + : spliceOut(Y, z, ie) + : spliceIn(Y, z, ee, ie); + return ie + ? ((this.bitmap = ae), (this.nodes = le), this) + : new BitmapIndexedNode(s, ae, le); + }), + (HashArrayMapNode.prototype.get = function (s, o, i, u) { + void 0 === o && (o = hash(i)); + var _ = (0 === s ? o : o >>> s) & j, + w = this.nodes[_]; + return w ? w.get(s + x, o, i, u) : u; + }), + (HashArrayMapNode.prototype.update = function (s, o, i, u, _, w, C) { + void 0 === i && (i = hash(u)); + var B = (0 === o ? i : i >>> o) & j, + $ = _ === L, + V = this.nodes, + U = V[B]; + if ($ && !U) return this; + var z = updateNode(U, s, o + x, i, u, _, w, C); + if (z === U) return this; + var Y = this.count; + if (U) { + if (!z && --Y < He) return packNodes(s, V, Y, B); + } else Y++; + var Z = s && s === this.ownerID, + ee = setIn(V, B, z, Z); + return Z + ? ((this.count = Y), (this.nodes = ee), this) + : new HashArrayMapNode(s, Y, ee); + }), + (HashCollisionNode.prototype.get = function (s, o, i, u) { + for (var _ = this.entries, w = 0, x = _.length; w < x; w++) + if (is(i, _[w][0])) return _[w][1]; + return u; + }), + (HashCollisionNode.prototype.update = function (s, o, i, u, _, w, x) { + void 0 === i && (i = hash(u)); + var C = _ === L; + if (i !== this.keyHash) + return C ? this : (SetRef(x), SetRef(w), mergeIntoNode(this, s, o, i, [u, _])); + for (var j = this.entries, B = 0, $ = j.length; B < $ && !is(u, j[B][0]); B++); + var V = B < $; + if (V ? j[B][1] === _ : C) return this; + if ((SetRef(x), (C || !V) && SetRef(w), C && 2 === $)) + return new ValueNode(s, this.keyHash, j[1 ^ B]); + var U = s && s === this.ownerID, + z = U ? j : arrCopy(j); + return ( + V + ? C + ? B === $ - 1 + ? z.pop() + : (z[B] = z.pop()) + : (z[B] = [u, _]) + : z.push([u, _]), + U ? ((this.entries = z), this) : new HashCollisionNode(s, this.keyHash, z) + ); + }), + (ValueNode.prototype.get = function (s, o, i, u) { + return is(i, this.entry[0]) ? this.entry[1] : u; + }), + (ValueNode.prototype.update = function (s, o, i, u, _, w, x) { + var C = _ === L, + j = is(u, this.entry[0]); + return (j ? _ === this.entry[1] : C) + ? this + : (SetRef(x), + C + ? void SetRef(w) + : j + ? s && s === this.ownerID + ? ((this.entry[1] = _), this) + : new ValueNode(s, this.keyHash, [u, _]) + : (SetRef(w), mergeIntoNode(this, s, o, hash(u), [u, _]))); + }), + (ArrayMapNode.prototype.iterate = HashCollisionNode.prototype.iterate = + function (s, o) { + for (var i = this.entries, u = 0, _ = i.length - 1; u <= _; u++) + if (!1 === s(i[o ? _ - u : u])) return !1; + }), + (BitmapIndexedNode.prototype.iterate = HashArrayMapNode.prototype.iterate = + function (s, o) { + for (var i = this.nodes, u = 0, _ = i.length - 1; u <= _; u++) { + var w = i[o ? _ - u : u]; + if (w && !1 === w.iterate(s, o)) return !1; + } + }), + (ValueNode.prototype.iterate = function (s, o) { + return s(this.entry); + }), + createClass(MapIterator, Iterator), + (MapIterator.prototype.next = function () { + for (var s = this._type, o = this._stack; o; ) { + var i, + u = o.node, + _ = o.index++; + if (u.entry) { + if (0 === _) return mapIteratorValue(s, u.entry); + } else if (u.entries) { + if (_ <= (i = u.entries.length - 1)) + return mapIteratorValue(s, u.entries[this._reverse ? i - _ : _]); + } else if (_ <= (i = u.nodes.length - 1)) { + var w = u.nodes[this._reverse ? i - _ : _]; + if (w) { + if (w.entry) return mapIteratorValue(s, w.entry); + o = this._stack = mapIteratorFrame(w, o); + } + continue; + } + o = this._stack = this._stack.__prev; + } + return iteratorDone(); + })); + var ze = C / 4, + We = C / 2, + He = C / 4; + function List(s) { + var o = emptyList(); + if (null == s) return o; + if (isList(s)) return s; + var i = IndexedIterable(s), + u = i.size; + return 0 === u + ? o + : (assertNotInfinite(u), + u > 0 && u < C + ? makeList(0, u, x, null, new VNode(i.toArray())) + : o.withMutations(function (s) { + (s.setSize(u), + i.forEach(function (o, i) { + return s.set(i, o); + })); + })); + } + function isList(s) { + return !(!s || !s[Ye]); + } + (createClass(List, IndexedCollection), + (List.of = function () { + return this(arguments); + }), + (List.prototype.toString = function () { + return this.__toString('List [', ']'); + }), + (List.prototype.get = function (s, o) { + if ((s = wrapIndex(this, s)) >= 0 && s < this.size) { + var i = listNodeFor(this, (s += this._origin)); + return i && i.array[s & j]; + } + return o; + }), + (List.prototype.set = function (s, o) { + return updateList(this, s, o); + }), + (List.prototype.remove = function (s) { + return this.has(s) + ? 0 === s + ? this.shift() + : s === this.size - 1 + ? this.pop() + : this.splice(s, 1) + : this; + }), + (List.prototype.insert = function (s, o) { + return this.splice(s, 0, o); + }), + (List.prototype.clear = function () { + return 0 === this.size + ? this + : this.__ownerID + ? ((this.size = this._origin = this._capacity = 0), + (this._level = x), + (this._root = this._tail = null), + (this.__hash = void 0), + (this.__altered = !0), + this) + : emptyList(); + }), + (List.prototype.push = function () { + var s = arguments, + o = this.size; + return this.withMutations(function (i) { + setListBounds(i, 0, o + s.length); + for (var u = 0; u < s.length; u++) i.set(o + u, s[u]); + }); + }), + (List.prototype.pop = function () { + return setListBounds(this, 0, -1); + }), + (List.prototype.unshift = function () { + var s = arguments; + return this.withMutations(function (o) { + setListBounds(o, -s.length); + for (var i = 0; i < s.length; i++) o.set(i, s[i]); + }); + }), + (List.prototype.shift = function () { + return setListBounds(this, 1); + }), + (List.prototype.merge = function () { + return mergeIntoListWith(this, void 0, arguments); + }), + (List.prototype.mergeWith = function (o) { + return mergeIntoListWith(this, o, s.call(arguments, 1)); + }), + (List.prototype.mergeDeep = function () { + return mergeIntoListWith(this, deepMerger, arguments); + }), + (List.prototype.mergeDeepWith = function (o) { + var i = s.call(arguments, 1); + return mergeIntoListWith(this, deepMergerWith(o), i); + }), + (List.prototype.setSize = function (s) { + return setListBounds(this, 0, s); + }), + (List.prototype.slice = function (s, o) { + var i = this.size; + return wholeSlice(s, o, i) + ? this + : setListBounds(this, resolveBegin(s, i), resolveEnd(o, i)); + }), + (List.prototype.__iterator = function (s, o) { + var i = 0, + u = iterateList(this, o); + return new Iterator(function () { + var o = u(); + return o === tt ? iteratorDone() : iteratorValue(s, i++, o); + }); + }), + (List.prototype.__iterate = function (s, o) { + for ( + var i, u = 0, _ = iterateList(this, o); + (i = _()) !== tt && !1 !== s(i, u++, this); + ); + return u; + }), + (List.prototype.__ensureOwner = function (s) { + return s === this.__ownerID + ? this + : s + ? makeList( + this._origin, + this._capacity, + this._level, + this._root, + this._tail, + s, + this.__hash + ) + : ((this.__ownerID = s), this); + }), + (List.isList = isList)); + var Ye = '@@__IMMUTABLE_LIST__@@', + Xe = List.prototype; + function VNode(s, o) { + ((this.array = s), (this.ownerID = o)); + } + ((Xe[Ye] = !0), + (Xe[w] = Xe.remove), + (Xe.setIn = $e.setIn), + (Xe.deleteIn = Xe.removeIn = $e.removeIn), + (Xe.update = $e.update), + (Xe.updateIn = $e.updateIn), + (Xe.mergeIn = $e.mergeIn), + (Xe.mergeDeepIn = $e.mergeDeepIn), + (Xe.withMutations = $e.withMutations), + (Xe.asMutable = $e.asMutable), + (Xe.asImmutable = $e.asImmutable), + (Xe.wasAltered = $e.wasAltered), + (VNode.prototype.removeBefore = function (s, o, i) { + if (i === o ? 1 << o : 0 === this.array.length) return this; + var u = (i >>> o) & j; + if (u >= this.array.length) return new VNode([], s); + var _, + w = 0 === u; + if (o > 0) { + var C = this.array[u]; + if ((_ = C && C.removeBefore(s, o - x, i)) === C && w) return this; + } + if (w && !_) return this; + var L = editableVNode(this, s); + if (!w) for (var B = 0; B < u; B++) L.array[B] = void 0; + return (_ && (L.array[u] = _), L); + }), + (VNode.prototype.removeAfter = function (s, o, i) { + if (i === (o ? 1 << o : 0) || 0 === this.array.length) return this; + var u, + _ = ((i - 1) >>> o) & j; + if (_ >= this.array.length) return this; + if (o > 0) { + var w = this.array[_]; + if ((u = w && w.removeAfter(s, o - x, i)) === w && _ === this.array.length - 1) + return this; + } + var C = editableVNode(this, s); + return (C.array.splice(_ + 1), u && (C.array[_] = u), C); + })); + var Qe, + et, + tt = {}; + function iterateList(s, o) { + var i = s._origin, + u = s._capacity, + _ = getTailOffset(u), + w = s._tail; + return iterateNodeOrLeaf(s._root, s._level, 0); + function iterateNodeOrLeaf(s, o, i) { + return 0 === o ? iterateLeaf(s, i) : iterateNode(s, o, i); + } + function iterateLeaf(s, x) { + var j = x === _ ? w && w.array : s && s.array, + L = x > i ? 0 : i - x, + B = u - x; + return ( + B > C && (B = C), + function () { + if (L === B) return tt; + var s = o ? --B : L++; + return j && j[s]; + } + ); + } + function iterateNode(s, _, w) { + var j, + L = s && s.array, + B = w > i ? 0 : (i - w) >> _, + $ = 1 + ((u - w) >> _); + return ( + $ > C && ($ = C), + function () { + for (;;) { + if (j) { + var s = j(); + if (s !== tt) return s; + j = null; + } + if (B === $) return tt; + var i = o ? --$ : B++; + j = iterateNodeOrLeaf(L && L[i], _ - x, w + (i << _)); + } + } + ); + } + } + function makeList(s, o, i, u, _, w, x) { + var C = Object.create(Xe); + return ( + (C.size = o - s), + (C._origin = s), + (C._capacity = o), + (C._level = i), + (C._root = u), + (C._tail = _), + (C.__ownerID = w), + (C.__hash = x), + (C.__altered = !1), + C + ); + } + function emptyList() { + return Qe || (Qe = makeList(0, 0, x)); + } + function updateList(s, o, i) { + if ((o = wrapIndex(s, o)) != o) return s; + if (o >= s.size || o < 0) + return s.withMutations(function (s) { + o < 0 ? setListBounds(s, o).set(0, i) : setListBounds(s, 0, o + 1).set(o, i); + }); + o += s._origin; + var u = s._tail, + _ = s._root, + w = MakeRef($); + return ( + o >= getTailOffset(s._capacity) + ? (u = updateVNode(u, s.__ownerID, 0, o, i, w)) + : (_ = updateVNode(_, s.__ownerID, s._level, o, i, w)), + w.value + ? s.__ownerID + ? ((s._root = _), (s._tail = u), (s.__hash = void 0), (s.__altered = !0), s) + : makeList(s._origin, s._capacity, s._level, _, u) + : s + ); + } + function updateVNode(s, o, i, u, _, w) { + var C, + L = (u >>> i) & j, + B = s && L < s.array.length; + if (!B && void 0 === _) return s; + if (i > 0) { + var $ = s && s.array[L], + V = updateVNode($, o, i - x, u, _, w); + return V === $ ? s : (((C = editableVNode(s, o)).array[L] = V), C); + } + return B && s.array[L] === _ + ? s + : (SetRef(w), + (C = editableVNode(s, o)), + void 0 === _ && L === C.array.length - 1 ? C.array.pop() : (C.array[L] = _), + C); + } + function editableVNode(s, o) { + return o && s && o === s.ownerID ? s : new VNode(s ? s.array.slice() : [], o); + } + function listNodeFor(s, o) { + if (o >= getTailOffset(s._capacity)) return s._tail; + if (o < 1 << (s._level + x)) { + for (var i = s._root, u = s._level; i && u > 0; ) + ((i = i.array[(o >>> u) & j]), (u -= x)); + return i; + } + } + function setListBounds(s, o, i) { + (void 0 !== o && (o |= 0), void 0 !== i && (i |= 0)); + var u = s.__ownerID || new OwnerID(), + _ = s._origin, + w = s._capacity, + C = _ + o, + L = void 0 === i ? w : i < 0 ? w + i : _ + i; + if (C === _ && L === w) return s; + if (C >= L) return s.clear(); + for (var B = s._level, $ = s._root, V = 0; C + V < 0; ) + (($ = new VNode($ && $.array.length ? [void 0, $] : [], u)), (V += 1 << (B += x))); + V && ((C += V), (_ += V), (L += V), (w += V)); + for (var U = getTailOffset(w), z = getTailOffset(L); z >= 1 << (B + x); ) + (($ = new VNode($ && $.array.length ? [$] : [], u)), (B += x)); + var Y = s._tail, + Z = z < U ? listNodeFor(s, L - 1) : z > U ? new VNode([], u) : Y; + if (Y && z > U && C < w && Y.array.length) { + for (var ee = ($ = editableVNode($, u)), ie = B; ie > x; ie -= x) { + var ae = (U >>> ie) & j; + ee = ee.array[ae] = editableVNode(ee.array[ae], u); + } + ee.array[(U >>> x) & j] = Y; + } + if ((L < w && (Z = Z && Z.removeAfter(u, 0, L)), C >= z)) + ((C -= z), (L -= z), (B = x), ($ = null), (Z = Z && Z.removeBefore(u, 0, C))); + else if (C > _ || z < U) { + for (V = 0; $; ) { + var le = (C >>> B) & j; + if ((le !== z >>> B) & j) break; + (le && (V += (1 << B) * le), (B -= x), ($ = $.array[le])); + } + ($ && C > _ && ($ = $.removeBefore(u, B, C - V)), + $ && z < U && ($ = $.removeAfter(u, B, z - V)), + V && ((C -= V), (L -= V))); + } + return s.__ownerID + ? ((s.size = L - C), + (s._origin = C), + (s._capacity = L), + (s._level = B), + (s._root = $), + (s._tail = Z), + (s.__hash = void 0), + (s.__altered = !0), + s) + : makeList(C, L, B, $, Z); + } + function mergeIntoListWith(s, o, i) { + for (var u = [], _ = 0, w = 0; w < i.length; w++) { + var x = i[w], + C = IndexedIterable(x); + (C.size > _ && (_ = C.size), + isIterable(x) || + (C = C.map(function (s) { + return fromJS(s); + })), + u.push(C)); + } + return (_ > s.size && (s = s.setSize(_)), mergeIntoCollectionWith(s, o, u)); + } + function getTailOffset(s) { + return s < C ? 0 : ((s - 1) >>> x) << x; + } + function OrderedMap(s) { + return null == s + ? emptyOrderedMap() + : isOrderedMap(s) + ? s + : emptyOrderedMap().withMutations(function (o) { + var i = KeyedIterable(s); + (assertNotInfinite(i.size), + i.forEach(function (s, i) { + return o.set(i, s); + })); + }); + } + function isOrderedMap(s) { + return isMap(s) && isOrdered(s); + } + function makeOrderedMap(s, o, i, u) { + var _ = Object.create(OrderedMap.prototype); + return ( + (_.size = s ? s.size : 0), + (_._map = s), + (_._list = o), + (_.__ownerID = i), + (_.__hash = u), + _ + ); + } + function emptyOrderedMap() { + return et || (et = makeOrderedMap(emptyMap(), emptyList())); + } + function updateOrderedMap(s, o, i) { + var u, + _, + w = s._map, + x = s._list, + j = w.get(o), + B = void 0 !== j; + if (i === L) { + if (!B) return s; + x.size >= C && x.size >= 2 * w.size + ? ((u = (_ = x.filter(function (s, o) { + return void 0 !== s && j !== o; + })) + .toKeyedSeq() + .map(function (s) { + return s[0]; + }) + .flip() + .toMap()), + s.__ownerID && (u.__ownerID = _.__ownerID = s.__ownerID)) + : ((u = w.remove(o)), (_ = j === x.size - 1 ? x.pop() : x.set(j, void 0))); + } else if (B) { + if (i === x.get(j)[1]) return s; + ((u = w), (_ = x.set(j, [o, i]))); + } else ((u = w.set(o, x.size)), (_ = x.set(x.size, [o, i]))); + return s.__ownerID + ? ((s.size = u.size), (s._map = u), (s._list = _), (s.__hash = void 0), s) + : makeOrderedMap(u, _); + } + function ToKeyedSequence(s, o) { + ((this._iter = s), (this._useKeys = o), (this.size = s.size)); + } + function ToIndexedSequence(s) { + ((this._iter = s), (this.size = s.size)); + } + function ToSetSequence(s) { + ((this._iter = s), (this.size = s.size)); + } + function FromEntriesSequence(s) { + ((this._iter = s), (this.size = s.size)); + } + function flipFactory(s) { + var o = makeSequence(s); + return ( + (o._iter = s), + (o.size = s.size), + (o.flip = function () { + return s; + }), + (o.reverse = function () { + var o = s.reverse.apply(this); + return ( + (o.flip = function () { + return s.reverse(); + }), + o + ); + }), + (o.has = function (o) { + return s.includes(o); + }), + (o.includes = function (o) { + return s.has(o); + }), + (o.cacheResult = cacheResultThrough), + (o.__iterateUncached = function (o, i) { + var u = this; + return s.__iterate(function (s, i) { + return !1 !== o(i, s, u); + }, i); + }), + (o.__iteratorUncached = function (o, i) { + if (o === z) { + var u = s.__iterator(o, i); + return new Iterator(function () { + var s = u.next(); + if (!s.done) { + var o = s.value[0]; + ((s.value[0] = s.value[1]), (s.value[1] = o)); + } + return s; + }); + } + return s.__iterator(o === U ? V : U, i); + }), + o + ); + } + function mapFactory(s, o, i) { + var u = makeSequence(s); + return ( + (u.size = s.size), + (u.has = function (o) { + return s.has(o); + }), + (u.get = function (u, _) { + var w = s.get(u, L); + return w === L ? _ : o.call(i, w, u, s); + }), + (u.__iterateUncached = function (u, _) { + var w = this; + return s.__iterate(function (s, _, x) { + return !1 !== u(o.call(i, s, _, x), _, w); + }, _); + }), + (u.__iteratorUncached = function (u, _) { + var w = s.__iterator(z, _); + return new Iterator(function () { + var _ = w.next(); + if (_.done) return _; + var x = _.value, + C = x[0]; + return iteratorValue(u, C, o.call(i, x[1], C, s), _); + }); + }), + u + ); + } + function reverseFactory(s, o) { + var i = makeSequence(s); + return ( + (i._iter = s), + (i.size = s.size), + (i.reverse = function () { + return s; + }), + s.flip && + (i.flip = function () { + var o = flipFactory(s); + return ( + (o.reverse = function () { + return s.flip(); + }), + o + ); + }), + (i.get = function (i, u) { + return s.get(o ? i : -1 - i, u); + }), + (i.has = function (i) { + return s.has(o ? i : -1 - i); + }), + (i.includes = function (o) { + return s.includes(o); + }), + (i.cacheResult = cacheResultThrough), + (i.__iterate = function (o, i) { + var u = this; + return s.__iterate(function (s, i) { + return o(s, i, u); + }, !i); + }), + (i.__iterator = function (o, i) { + return s.__iterator(o, !i); + }), + i + ); + } + function filterFactory(s, o, i, u) { + var _ = makeSequence(s); + return ( + u && + ((_.has = function (u) { + var _ = s.get(u, L); + return _ !== L && !!o.call(i, _, u, s); + }), + (_.get = function (u, _) { + var w = s.get(u, L); + return w !== L && o.call(i, w, u, s) ? w : _; + })), + (_.__iterateUncached = function (_, w) { + var x = this, + C = 0; + return ( + s.__iterate(function (s, w, j) { + if (o.call(i, s, w, j)) return (C++, _(s, u ? w : C - 1, x)); + }, w), + C + ); + }), + (_.__iteratorUncached = function (_, w) { + var x = s.__iterator(z, w), + C = 0; + return new Iterator(function () { + for (;;) { + var w = x.next(); + if (w.done) return w; + var j = w.value, + L = j[0], + B = j[1]; + if (o.call(i, B, L, s)) return iteratorValue(_, u ? L : C++, B, w); + } + }); + }), + _ + ); + } + function countByFactory(s, o, i) { + var u = Map().asMutable(); + return ( + s.__iterate(function (_, w) { + u.update(o.call(i, _, w, s), 0, function (s) { + return s + 1; + }); + }), + u.asImmutable() + ); + } + function groupByFactory(s, o, i) { + var u = isKeyed(s), + _ = (isOrdered(s) ? OrderedMap() : Map()).asMutable(); + s.__iterate(function (w, x) { + _.update(o.call(i, w, x, s), function (s) { + return ((s = s || []).push(u ? [x, w] : w), s); + }); + }); + var w = iterableClass(s); + return _.map(function (o) { + return reify(s, w(o)); + }); + } + function sliceFactory(s, o, i, u) { + var _ = s.size; + if ( + (void 0 !== o && (o |= 0), + void 0 !== i && (i === 1 / 0 ? (i = _) : (i |= 0)), + wholeSlice(o, i, _)) + ) + return s; + var w = resolveBegin(o, _), + x = resolveEnd(i, _); + if (w != w || x != x) return sliceFactory(s.toSeq().cacheResult(), o, i, u); + var C, + j = x - w; + j == j && (C = j < 0 ? 0 : j); + var L = makeSequence(s); + return ( + (L.size = 0 === C ? C : (s.size && C) || void 0), + !u && + isSeq(s) && + C >= 0 && + (L.get = function (o, i) { + return (o = wrapIndex(this, o)) >= 0 && o < C ? s.get(o + w, i) : i; + }), + (L.__iterateUncached = function (o, i) { + var _ = this; + if (0 === C) return 0; + if (i) return this.cacheResult().__iterate(o, i); + var x = 0, + j = !0, + L = 0; + return ( + s.__iterate(function (s, i) { + if (!j || !(j = x++ < w)) + return (L++, !1 !== o(s, u ? i : L - 1, _) && L !== C); + }), + L + ); + }), + (L.__iteratorUncached = function (o, i) { + if (0 !== C && i) return this.cacheResult().__iterator(o, i); + var _ = 0 !== C && s.__iterator(o, i), + x = 0, + j = 0; + return new Iterator(function () { + for (; x++ < w; ) _.next(); + if (++j > C) return iteratorDone(); + var s = _.next(); + return u || o === U + ? s + : iteratorValue(o, j - 1, o === V ? void 0 : s.value[1], s); + }); + }), + L + ); + } + function takeWhileFactory(s, o, i) { + var u = makeSequence(s); + return ( + (u.__iterateUncached = function (u, _) { + var w = this; + if (_) return this.cacheResult().__iterate(u, _); + var x = 0; + return ( + s.__iterate(function (s, _, C) { + return o.call(i, s, _, C) && ++x && u(s, _, w); + }), + x + ); + }), + (u.__iteratorUncached = function (u, _) { + var w = this; + if (_) return this.cacheResult().__iterator(u, _); + var x = s.__iterator(z, _), + C = !0; + return new Iterator(function () { + if (!C) return iteratorDone(); + var s = x.next(); + if (s.done) return s; + var _ = s.value, + j = _[0], + L = _[1]; + return o.call(i, L, j, w) + ? u === z + ? s + : iteratorValue(u, j, L, s) + : ((C = !1), iteratorDone()); + }); + }), + u + ); + } + function skipWhileFactory(s, o, i, u) { + var _ = makeSequence(s); + return ( + (_.__iterateUncached = function (_, w) { + var x = this; + if (w) return this.cacheResult().__iterate(_, w); + var C = !0, + j = 0; + return ( + s.__iterate(function (s, w, L) { + if (!C || !(C = o.call(i, s, w, L))) return (j++, _(s, u ? w : j - 1, x)); + }), + j + ); + }), + (_.__iteratorUncached = function (_, w) { + var x = this; + if (w) return this.cacheResult().__iterator(_, w); + var C = s.__iterator(z, w), + j = !0, + L = 0; + return new Iterator(function () { + var s, w, B; + do { + if ((s = C.next()).done) + return u || _ === U + ? s + : iteratorValue(_, L++, _ === V ? void 0 : s.value[1], s); + var $ = s.value; + ((w = $[0]), (B = $[1]), j && (j = o.call(i, B, w, x))); + } while (j); + return _ === z ? s : iteratorValue(_, w, B, s); + }); + }), + _ + ); + } + function concatFactory(s, o) { + var i = isKeyed(s), + u = [s] + .concat(o) + .map(function (s) { + return ( + isIterable(s) + ? i && (s = KeyedIterable(s)) + : (s = i + ? keyedSeqFromValue(s) + : indexedSeqFromValue(Array.isArray(s) ? s : [s])), + s + ); + }) + .filter(function (s) { + return 0 !== s.size; + }); + if (0 === u.length) return s; + if (1 === u.length) { + var _ = u[0]; + if (_ === s || (i && isKeyed(_)) || (isIndexed(s) && isIndexed(_))) return _; + } + var w = new ArraySeq(u); + return ( + i ? (w = w.toKeyedSeq()) : isIndexed(s) || (w = w.toSetSeq()), + ((w = w.flatten(!0)).size = u.reduce(function (s, o) { + if (void 0 !== s) { + var i = o.size; + if (void 0 !== i) return s + i; + } + }, 0)), + w + ); + } + function flattenFactory(s, o, i) { + var u = makeSequence(s); + return ( + (u.__iterateUncached = function (u, _) { + var w = 0, + x = !1; + function flatDeep(s, C) { + var j = this; + s.__iterate(function (s, _) { + return ( + (!o || C < o) && isIterable(s) + ? flatDeep(s, C + 1) + : !1 === u(s, i ? _ : w++, j) && (x = !0), + !x + ); + }, _); + } + return (flatDeep(s, 0), w); + }), + (u.__iteratorUncached = function (u, _) { + var w = s.__iterator(u, _), + x = [], + C = 0; + return new Iterator(function () { + for (; w; ) { + var s = w.next(); + if (!1 === s.done) { + var j = s.value; + if ((u === z && (j = j[1]), (o && !(x.length < o)) || !isIterable(j))) + return i ? s : iteratorValue(u, C++, j, s); + (x.push(w), (w = j.__iterator(u, _))); + } else w = x.pop(); + } + return iteratorDone(); + }); + }), + u + ); + } + function flatMapFactory(s, o, i) { + var u = iterableClass(s); + return s + .toSeq() + .map(function (_, w) { + return u(o.call(i, _, w, s)); + }) + .flatten(!0); + } + function interposeFactory(s, o) { + var i = makeSequence(s); + return ( + (i.size = s.size && 2 * s.size - 1), + (i.__iterateUncached = function (i, u) { + var _ = this, + w = 0; + return ( + s.__iterate(function (s, u) { + return (!w || !1 !== i(o, w++, _)) && !1 !== i(s, w++, _); + }, u), + w + ); + }), + (i.__iteratorUncached = function (i, u) { + var _, + w = s.__iterator(U, u), + x = 0; + return new Iterator(function () { + return (!_ || x % 2) && (_ = w.next()).done + ? _ + : x % 2 + ? iteratorValue(i, x++, o) + : iteratorValue(i, x++, _.value, _); + }); + }), + i + ); + } + function sortFactory(s, o, i) { + o || (o = defaultComparator); + var u = isKeyed(s), + _ = 0, + w = s + .toSeq() + .map(function (o, u) { + return [u, o, _++, i ? i(o, u, s) : o]; + }) + .toArray(); + return ( + w + .sort(function (s, i) { + return o(s[3], i[3]) || s[2] - i[2]; + }) + .forEach( + u + ? function (s, o) { + w[o].length = 2; + } + : function (s, o) { + w[o] = s[1]; + } + ), + u ? KeyedSeq(w) : isIndexed(s) ? IndexedSeq(w) : SetSeq(w) + ); + } + function maxFactory(s, o, i) { + if ((o || (o = defaultComparator), i)) { + var u = s + .toSeq() + .map(function (o, u) { + return [o, i(o, u, s)]; + }) + .reduce(function (s, i) { + return maxCompare(o, s[1], i[1]) ? i : s; + }); + return u && u[0]; + } + return s.reduce(function (s, i) { + return maxCompare(o, s, i) ? i : s; + }); + } + function maxCompare(s, o, i) { + var u = s(i, o); + return (0 === u && i !== o && (null == i || i != i)) || u > 0; + } + function zipWithFactory(s, o, i) { + var u = makeSequence(s); + return ( + (u.size = new ArraySeq(i) + .map(function (s) { + return s.size; + }) + .min()), + (u.__iterate = function (s, o) { + for ( + var i, u = this.__iterator(U, o), _ = 0; + !(i = u.next()).done && !1 !== s(i.value, _++, this); + ); + return _; + }), + (u.__iteratorUncached = function (s, u) { + var _ = i.map(function (s) { + return ((s = Iterable(s)), getIterator(u ? s.reverse() : s)); + }), + w = 0, + x = !1; + return new Iterator(function () { + var i; + return ( + x || + ((i = _.map(function (s) { + return s.next(); + })), + (x = i.some(function (s) { + return s.done; + }))), + x + ? iteratorDone() + : iteratorValue( + s, + w++, + o.apply( + null, + i.map(function (s) { + return s.value; + }) + ) + ) + ); + }); + }), + u + ); + } + function reify(s, o) { + return isSeq(s) ? o : s.constructor(o); + } + function validateEntry(s) { + if (s !== Object(s)) throw new TypeError('Expected [K, V] tuple: ' + s); + } + function resolveSize(s) { + return (assertNotInfinite(s.size), ensureSize(s)); + } + function iterableClass(s) { + return isKeyed(s) ? KeyedIterable : isIndexed(s) ? IndexedIterable : SetIterable; + } + function makeSequence(s) { + return Object.create( + (isKeyed(s) ? KeyedSeq : isIndexed(s) ? IndexedSeq : SetSeq).prototype + ); + } + function cacheResultThrough() { + return this._iter.cacheResult + ? (this._iter.cacheResult(), (this.size = this._iter.size), this) + : Seq.prototype.cacheResult.call(this); + } + function defaultComparator(s, o) { + return s > o ? 1 : s < o ? -1 : 0; + } + function forceIterator(s) { + var o = getIterator(s); + if (!o) { + if (!isArrayLike(s)) throw new TypeError('Expected iterable or array-like: ' + s); + o = getIterator(Iterable(s)); + } + return o; + } + function Record(s, o) { + var i, + u = function Record(w) { + if (w instanceof u) return w; + if (!(this instanceof u)) return new u(w); + if (!i) { + i = !0; + var x = Object.keys(s); + (setProps(_, x), + (_.size = x.length), + (_._name = o), + (_._keys = x), + (_._defaultValues = s)); + } + this._map = Map(w); + }, + _ = (u.prototype = Object.create(rt)); + return ((_.constructor = u), u); + } + (createClass(OrderedMap, Map), + (OrderedMap.of = function () { + return this(arguments); + }), + (OrderedMap.prototype.toString = function () { + return this.__toString('OrderedMap {', '}'); + }), + (OrderedMap.prototype.get = function (s, o) { + var i = this._map.get(s); + return void 0 !== i ? this._list.get(i)[1] : o; + }), + (OrderedMap.prototype.clear = function () { + return 0 === this.size + ? this + : this.__ownerID + ? ((this.size = 0), this._map.clear(), this._list.clear(), this) + : emptyOrderedMap(); + }), + (OrderedMap.prototype.set = function (s, o) { + return updateOrderedMap(this, s, o); + }), + (OrderedMap.prototype.remove = function (s) { + return updateOrderedMap(this, s, L); + }), + (OrderedMap.prototype.wasAltered = function () { + return this._map.wasAltered() || this._list.wasAltered(); + }), + (OrderedMap.prototype.__iterate = function (s, o) { + var i = this; + return this._list.__iterate(function (o) { + return o && s(o[1], o[0], i); + }, o); + }), + (OrderedMap.prototype.__iterator = function (s, o) { + return this._list.fromEntrySeq().__iterator(s, o); + }), + (OrderedMap.prototype.__ensureOwner = function (s) { + if (s === this.__ownerID) return this; + var o = this._map.__ensureOwner(s), + i = this._list.__ensureOwner(s); + return s + ? makeOrderedMap(o, i, s, this.__hash) + : ((this.__ownerID = s), (this._map = o), (this._list = i), this); + }), + (OrderedMap.isOrderedMap = isOrderedMap), + (OrderedMap.prototype[_] = !0), + (OrderedMap.prototype[w] = OrderedMap.prototype.remove), + createClass(ToKeyedSequence, KeyedSeq), + (ToKeyedSequence.prototype.get = function (s, o) { + return this._iter.get(s, o); + }), + (ToKeyedSequence.prototype.has = function (s) { + return this._iter.has(s); + }), + (ToKeyedSequence.prototype.valueSeq = function () { + return this._iter.valueSeq(); + }), + (ToKeyedSequence.prototype.reverse = function () { + var s = this, + o = reverseFactory(this, !0); + return ( + this._useKeys || + (o.valueSeq = function () { + return s._iter.toSeq().reverse(); + }), + o + ); + }), + (ToKeyedSequence.prototype.map = function (s, o) { + var i = this, + u = mapFactory(this, s, o); + return ( + this._useKeys || + (u.valueSeq = function () { + return i._iter.toSeq().map(s, o); + }), + u + ); + }), + (ToKeyedSequence.prototype.__iterate = function (s, o) { + var i, + u = this; + return this._iter.__iterate( + this._useKeys + ? function (o, i) { + return s(o, i, u); + } + : ((i = o ? resolveSize(this) : 0), + function (_) { + return s(_, o ? --i : i++, u); + }), + o + ); + }), + (ToKeyedSequence.prototype.__iterator = function (s, o) { + if (this._useKeys) return this._iter.__iterator(s, o); + var i = this._iter.__iterator(U, o), + u = o ? resolveSize(this) : 0; + return new Iterator(function () { + var _ = i.next(); + return _.done ? _ : iteratorValue(s, o ? --u : u++, _.value, _); + }); + }), + (ToKeyedSequence.prototype[_] = !0), + createClass(ToIndexedSequence, IndexedSeq), + (ToIndexedSequence.prototype.includes = function (s) { + return this._iter.includes(s); + }), + (ToIndexedSequence.prototype.__iterate = function (s, o) { + var i = this, + u = 0; + return this._iter.__iterate(function (o) { + return s(o, u++, i); + }, o); + }), + (ToIndexedSequence.prototype.__iterator = function (s, o) { + var i = this._iter.__iterator(U, o), + u = 0; + return new Iterator(function () { + var o = i.next(); + return o.done ? o : iteratorValue(s, u++, o.value, o); + }); + }), + createClass(ToSetSequence, SetSeq), + (ToSetSequence.prototype.has = function (s) { + return this._iter.includes(s); + }), + (ToSetSequence.prototype.__iterate = function (s, o) { + var i = this; + return this._iter.__iterate(function (o) { + return s(o, o, i); + }, o); + }), + (ToSetSequence.prototype.__iterator = function (s, o) { + var i = this._iter.__iterator(U, o); + return new Iterator(function () { + var o = i.next(); + return o.done ? o : iteratorValue(s, o.value, o.value, o); + }); + }), + createClass(FromEntriesSequence, KeyedSeq), + (FromEntriesSequence.prototype.entrySeq = function () { + return this._iter.toSeq(); + }), + (FromEntriesSequence.prototype.__iterate = function (s, o) { + var i = this; + return this._iter.__iterate(function (o) { + if (o) { + validateEntry(o); + var u = isIterable(o); + return s(u ? o.get(1) : o[1], u ? o.get(0) : o[0], i); + } + }, o); + }), + (FromEntriesSequence.prototype.__iterator = function (s, o) { + var i = this._iter.__iterator(U, o); + return new Iterator(function () { + for (;;) { + var o = i.next(); + if (o.done) return o; + var u = o.value; + if (u) { + validateEntry(u); + var _ = isIterable(u); + return iteratorValue(s, _ ? u.get(0) : u[0], _ ? u.get(1) : u[1], o); + } + } + }); + }), + (ToIndexedSequence.prototype.cacheResult = + ToKeyedSequence.prototype.cacheResult = + ToSetSequence.prototype.cacheResult = + FromEntriesSequence.prototype.cacheResult = + cacheResultThrough), + createClass(Record, KeyedCollection), + (Record.prototype.toString = function () { + return this.__toString(recordName(this) + ' {', '}'); + }), + (Record.prototype.has = function (s) { + return this._defaultValues.hasOwnProperty(s); + }), + (Record.prototype.get = function (s, o) { + if (!this.has(s)) return o; + var i = this._defaultValues[s]; + return this._map ? this._map.get(s, i) : i; + }), + (Record.prototype.clear = function () { + if (this.__ownerID) return (this._map && this._map.clear(), this); + var s = this.constructor; + return s._empty || (s._empty = makeRecord(this, emptyMap())); + }), + (Record.prototype.set = function (s, o) { + if (!this.has(s)) + throw new Error('Cannot set unknown key "' + s + '" on ' + recordName(this)); + if (this._map && !this._map.has(s) && o === this._defaultValues[s]) return this; + var i = this._map && this._map.set(s, o); + return this.__ownerID || i === this._map ? this : makeRecord(this, i); + }), + (Record.prototype.remove = function (s) { + if (!this.has(s)) return this; + var o = this._map && this._map.remove(s); + return this.__ownerID || o === this._map ? this : makeRecord(this, o); + }), + (Record.prototype.wasAltered = function () { + return this._map.wasAltered(); + }), + (Record.prototype.__iterator = function (s, o) { + var i = this; + return KeyedIterable(this._defaultValues) + .map(function (s, o) { + return i.get(o); + }) + .__iterator(s, o); + }), + (Record.prototype.__iterate = function (s, o) { + var i = this; + return KeyedIterable(this._defaultValues) + .map(function (s, o) { + return i.get(o); + }) + .__iterate(s, o); + }), + (Record.prototype.__ensureOwner = function (s) { + if (s === this.__ownerID) return this; + var o = this._map && this._map.__ensureOwner(s); + return s ? makeRecord(this, o, s) : ((this.__ownerID = s), (this._map = o), this); + })); + var rt = Record.prototype; + function makeRecord(s, o, i) { + var u = Object.create(Object.getPrototypeOf(s)); + return ((u._map = o), (u.__ownerID = i), u); + } + function recordName(s) { + return s._name || s.constructor.name || 'Record'; + } + function setProps(s, o) { + try { + o.forEach(setProp.bind(void 0, s)); + } catch (s) {} + } + function setProp(s, o) { + Object.defineProperty(s, o, { + get: function () { + return this.get(o); + }, + set: function (s) { + (invariant(this.__ownerID, 'Cannot set on an immutable record.'), this.set(o, s)); + } + }); + } + function Set(s) { + return null == s + ? emptySet() + : isSet(s) && !isOrdered(s) + ? s + : emptySet().withMutations(function (o) { + var i = SetIterable(s); + (assertNotInfinite(i.size), + i.forEach(function (s) { + return o.add(s); + })); + }); + } + function isSet(s) { + return !(!s || !s[st]); + } + ((rt[w] = rt.remove), + (rt.deleteIn = rt.removeIn = $e.removeIn), + (rt.merge = $e.merge), + (rt.mergeWith = $e.mergeWith), + (rt.mergeIn = $e.mergeIn), + (rt.mergeDeep = $e.mergeDeep), + (rt.mergeDeepWith = $e.mergeDeepWith), + (rt.mergeDeepIn = $e.mergeDeepIn), + (rt.setIn = $e.setIn), + (rt.update = $e.update), + (rt.updateIn = $e.updateIn), + (rt.withMutations = $e.withMutations), + (rt.asMutable = $e.asMutable), + (rt.asImmutable = $e.asImmutable), + createClass(Set, SetCollection), + (Set.of = function () { + return this(arguments); + }), + (Set.fromKeys = function (s) { + return this(KeyedIterable(s).keySeq()); + }), + (Set.prototype.toString = function () { + return this.__toString('Set {', '}'); + }), + (Set.prototype.has = function (s) { + return this._map.has(s); + }), + (Set.prototype.add = function (s) { + return updateSet(this, this._map.set(s, !0)); + }), + (Set.prototype.remove = function (s) { + return updateSet(this, this._map.remove(s)); + }), + (Set.prototype.clear = function () { + return updateSet(this, this._map.clear()); + }), + (Set.prototype.union = function () { + var o = s.call(arguments, 0); + return 0 === + (o = o.filter(function (s) { + return 0 !== s.size; + })).length + ? this + : 0 !== this.size || this.__ownerID || 1 !== o.length + ? this.withMutations(function (s) { + for (var i = 0; i < o.length; i++) + SetIterable(o[i]).forEach(function (o) { + return s.add(o); + }); + }) + : this.constructor(o[0]); + }), + (Set.prototype.intersect = function () { + var o = s.call(arguments, 0); + if (0 === o.length) return this; + o = o.map(function (s) { + return SetIterable(s); + }); + var i = this; + return this.withMutations(function (s) { + i.forEach(function (i) { + o.every(function (s) { + return s.includes(i); + }) || s.remove(i); + }); + }); + }), + (Set.prototype.subtract = function () { + var o = s.call(arguments, 0); + if (0 === o.length) return this; + o = o.map(function (s) { + return SetIterable(s); + }); + var i = this; + return this.withMutations(function (s) { + i.forEach(function (i) { + o.some(function (s) { + return s.includes(i); + }) && s.remove(i); + }); + }); + }), + (Set.prototype.merge = function () { + return this.union.apply(this, arguments); + }), + (Set.prototype.mergeWith = function (o) { + var i = s.call(arguments, 1); + return this.union.apply(this, i); + }), + (Set.prototype.sort = function (s) { + return OrderedSet(sortFactory(this, s)); + }), + (Set.prototype.sortBy = function (s, o) { + return OrderedSet(sortFactory(this, o, s)); + }), + (Set.prototype.wasAltered = function () { + return this._map.wasAltered(); + }), + (Set.prototype.__iterate = function (s, o) { + var i = this; + return this._map.__iterate(function (o, u) { + return s(u, u, i); + }, o); + }), + (Set.prototype.__iterator = function (s, o) { + return this._map + .map(function (s, o) { + return o; + }) + .__iterator(s, o); + }), + (Set.prototype.__ensureOwner = function (s) { + if (s === this.__ownerID) return this; + var o = this._map.__ensureOwner(s); + return s ? this.__make(o, s) : ((this.__ownerID = s), (this._map = o), this); + }), + (Set.isSet = isSet)); + var nt, + st = '@@__IMMUTABLE_SET__@@', + ot = Set.prototype; + function updateSet(s, o) { + return s.__ownerID + ? ((s.size = o.size), (s._map = o), s) + : o === s._map + ? s + : 0 === o.size + ? s.__empty() + : s.__make(o); + } + function makeSet(s, o) { + var i = Object.create(ot); + return ((i.size = s ? s.size : 0), (i._map = s), (i.__ownerID = o), i); + } + function emptySet() { + return nt || (nt = makeSet(emptyMap())); + } + function OrderedSet(s) { + return null == s + ? emptyOrderedSet() + : isOrderedSet(s) + ? s + : emptyOrderedSet().withMutations(function (o) { + var i = SetIterable(s); + (assertNotInfinite(i.size), + i.forEach(function (s) { + return o.add(s); + })); + }); + } + function isOrderedSet(s) { + return isSet(s) && isOrdered(s); + } + ((ot[st] = !0), + (ot[w] = ot.remove), + (ot.mergeDeep = ot.merge), + (ot.mergeDeepWith = ot.mergeWith), + (ot.withMutations = $e.withMutations), + (ot.asMutable = $e.asMutable), + (ot.asImmutable = $e.asImmutable), + (ot.__empty = emptySet), + (ot.__make = makeSet), + createClass(OrderedSet, Set), + (OrderedSet.of = function () { + return this(arguments); + }), + (OrderedSet.fromKeys = function (s) { + return this(KeyedIterable(s).keySeq()); + }), + (OrderedSet.prototype.toString = function () { + return this.__toString('OrderedSet {', '}'); + }), + (OrderedSet.isOrderedSet = isOrderedSet)); + var it, + at = OrderedSet.prototype; + function makeOrderedSet(s, o) { + var i = Object.create(at); + return ((i.size = s ? s.size : 0), (i._map = s), (i.__ownerID = o), i); + } + function emptyOrderedSet() { + return it || (it = makeOrderedSet(emptyOrderedMap())); + } + function Stack(s) { + return null == s ? emptyStack() : isStack(s) ? s : emptyStack().unshiftAll(s); + } + function isStack(s) { + return !(!s || !s[ct]); + } + ((at[_] = !0), + (at.__empty = emptyOrderedSet), + (at.__make = makeOrderedSet), + createClass(Stack, IndexedCollection), + (Stack.of = function () { + return this(arguments); + }), + (Stack.prototype.toString = function () { + return this.__toString('Stack [', ']'); + }), + (Stack.prototype.get = function (s, o) { + var i = this._head; + for (s = wrapIndex(this, s); i && s--; ) i = i.next; + return i ? i.value : o; + }), + (Stack.prototype.peek = function () { + return this._head && this._head.value; + }), + (Stack.prototype.push = function () { + if (0 === arguments.length) return this; + for ( + var s = this.size + arguments.length, o = this._head, i = arguments.length - 1; + i >= 0; + i-- + ) + o = { value: arguments[i], next: o }; + return this.__ownerID + ? ((this.size = s), + (this._head = o), + (this.__hash = void 0), + (this.__altered = !0), + this) + : makeStack(s, o); + }), + (Stack.prototype.pushAll = function (s) { + if (0 === (s = IndexedIterable(s)).size) return this; + assertNotInfinite(s.size); + var o = this.size, + i = this._head; + return ( + s.reverse().forEach(function (s) { + (o++, (i = { value: s, next: i })); + }), + this.__ownerID + ? ((this.size = o), + (this._head = i), + (this.__hash = void 0), + (this.__altered = !0), + this) + : makeStack(o, i) + ); + }), + (Stack.prototype.pop = function () { + return this.slice(1); + }), + (Stack.prototype.unshift = function () { + return this.push.apply(this, arguments); + }), + (Stack.prototype.unshiftAll = function (s) { + return this.pushAll(s); + }), + (Stack.prototype.shift = function () { + return this.pop.apply(this, arguments); + }), + (Stack.prototype.clear = function () { + return 0 === this.size + ? this + : this.__ownerID + ? ((this.size = 0), + (this._head = void 0), + (this.__hash = void 0), + (this.__altered = !0), + this) + : emptyStack(); + }), + (Stack.prototype.slice = function (s, o) { + if (wholeSlice(s, o, this.size)) return this; + var i = resolveBegin(s, this.size); + if (resolveEnd(o, this.size) !== this.size) + return IndexedCollection.prototype.slice.call(this, s, o); + for (var u = this.size - i, _ = this._head; i--; ) _ = _.next; + return this.__ownerID + ? ((this.size = u), + (this._head = _), + (this.__hash = void 0), + (this.__altered = !0), + this) + : makeStack(u, _); + }), + (Stack.prototype.__ensureOwner = function (s) { + return s === this.__ownerID + ? this + : s + ? makeStack(this.size, this._head, s, this.__hash) + : ((this.__ownerID = s), (this.__altered = !1), this); + }), + (Stack.prototype.__iterate = function (s, o) { + if (o) return this.reverse().__iterate(s); + for (var i = 0, u = this._head; u && !1 !== s(u.value, i++, this); ) u = u.next; + return i; + }), + (Stack.prototype.__iterator = function (s, o) { + if (o) return this.reverse().__iterator(s); + var i = 0, + u = this._head; + return new Iterator(function () { + if (u) { + var o = u.value; + return ((u = u.next), iteratorValue(s, i++, o)); + } + return iteratorDone(); + }); + }), + (Stack.isStack = isStack)); + var lt, + ct = '@@__IMMUTABLE_STACK__@@', + ut = Stack.prototype; + function makeStack(s, o, i, u) { + var _ = Object.create(ut); + return ( + (_.size = s), + (_._head = o), + (_.__ownerID = i), + (_.__hash = u), + (_.__altered = !1), + _ + ); + } + function emptyStack() { + return lt || (lt = makeStack(0)); + } + function mixin(s, o) { + var keyCopier = function (i) { + s.prototype[i] = o[i]; + }; + return ( + Object.keys(o).forEach(keyCopier), + Object.getOwnPropertySymbols && Object.getOwnPropertySymbols(o).forEach(keyCopier), + s + ); + } + ((ut[ct] = !0), + (ut.withMutations = $e.withMutations), + (ut.asMutable = $e.asMutable), + (ut.asImmutable = $e.asImmutable), + (ut.wasAltered = $e.wasAltered), + (Iterable.Iterator = Iterator), + mixin(Iterable, { + toArray: function () { + assertNotInfinite(this.size); + var s = new Array(this.size || 0); + return ( + this.valueSeq().__iterate(function (o, i) { + s[i] = o; + }), + s + ); + }, + toIndexedSeq: function () { + return new ToIndexedSequence(this); + }, + toJS: function () { + return this.toSeq() + .map(function (s) { + return s && 'function' == typeof s.toJS ? s.toJS() : s; + }) + .__toJS(); + }, + toJSON: function () { + return this.toSeq() + .map(function (s) { + return s && 'function' == typeof s.toJSON ? s.toJSON() : s; + }) + .__toJS(); + }, + toKeyedSeq: function () { + return new ToKeyedSequence(this, !0); + }, + toMap: function () { + return Map(this.toKeyedSeq()); + }, + toObject: function () { + assertNotInfinite(this.size); + var s = {}; + return ( + this.__iterate(function (o, i) { + s[i] = o; + }), + s + ); + }, + toOrderedMap: function () { + return OrderedMap(this.toKeyedSeq()); + }, + toOrderedSet: function () { + return OrderedSet(isKeyed(this) ? this.valueSeq() : this); + }, + toSet: function () { + return Set(isKeyed(this) ? this.valueSeq() : this); + }, + toSetSeq: function () { + return new ToSetSequence(this); + }, + toSeq: function () { + return isIndexed(this) + ? this.toIndexedSeq() + : isKeyed(this) + ? this.toKeyedSeq() + : this.toSetSeq(); + }, + toStack: function () { + return Stack(isKeyed(this) ? this.valueSeq() : this); + }, + toList: function () { + return List(isKeyed(this) ? this.valueSeq() : this); + }, + toString: function () { + return '[Iterable]'; + }, + __toString: function (s, o) { + return 0 === this.size + ? s + o + : s + ' ' + this.toSeq().map(this.__toStringMapper).join(', ') + ' ' + o; + }, + concat: function () { + return reify(this, concatFactory(this, s.call(arguments, 0))); + }, + includes: function (s) { + return this.some(function (o) { + return is(o, s); + }); + }, + entries: function () { + return this.__iterator(z); + }, + every: function (s, o) { + assertNotInfinite(this.size); + var i = !0; + return ( + this.__iterate(function (u, _, w) { + if (!s.call(o, u, _, w)) return ((i = !1), !1); + }), + i + ); + }, + filter: function (s, o) { + return reify(this, filterFactory(this, s, o, !0)); + }, + find: function (s, o, i) { + var u = this.findEntry(s, o); + return u ? u[1] : i; + }, + forEach: function (s, o) { + return (assertNotInfinite(this.size), this.__iterate(o ? s.bind(o) : s)); + }, + join: function (s) { + (assertNotInfinite(this.size), (s = void 0 !== s ? '' + s : ',')); + var o = '', + i = !0; + return ( + this.__iterate(function (u) { + (i ? (i = !1) : (o += s), (o += null != u ? u.toString() : '')); + }), + o + ); + }, + keys: function () { + return this.__iterator(V); + }, + map: function (s, o) { + return reify(this, mapFactory(this, s, o)); + }, + reduce: function (s, o, i) { + var u, _; + return ( + assertNotInfinite(this.size), + arguments.length < 2 ? (_ = !0) : (u = o), + this.__iterate(function (o, w, x) { + _ ? ((_ = !1), (u = o)) : (u = s.call(i, u, o, w, x)); + }), + u + ); + }, + reduceRight: function (s, o, i) { + var u = this.toKeyedSeq().reverse(); + return u.reduce.apply(u, arguments); + }, + reverse: function () { + return reify(this, reverseFactory(this, !0)); + }, + slice: function (s, o) { + return reify(this, sliceFactory(this, s, o, !0)); + }, + some: function (s, o) { + return !this.every(not(s), o); + }, + sort: function (s) { + return reify(this, sortFactory(this, s)); + }, + values: function () { + return this.__iterator(U); + }, + butLast: function () { + return this.slice(0, -1); + }, + isEmpty: function () { + return void 0 !== this.size + ? 0 === this.size + : !this.some(function () { + return !0; + }); + }, + count: function (s, o) { + return ensureSize(s ? this.toSeq().filter(s, o) : this); + }, + countBy: function (s, o) { + return countByFactory(this, s, o); + }, + equals: function (s) { + return deepEqual(this, s); + }, + entrySeq: function () { + var s = this; + if (s._cache) return new ArraySeq(s._cache); + var o = s.toSeq().map(entryMapper).toIndexedSeq(); + return ( + (o.fromEntrySeq = function () { + return s.toSeq(); + }), + o + ); + }, + filterNot: function (s, o) { + return this.filter(not(s), o); + }, + findEntry: function (s, o, i) { + var u = i; + return ( + this.__iterate(function (i, _, w) { + if (s.call(o, i, _, w)) return ((u = [_, i]), !1); + }), + u + ); + }, + findKey: function (s, o) { + var i = this.findEntry(s, o); + return i && i[0]; + }, + findLast: function (s, o, i) { + return this.toKeyedSeq().reverse().find(s, o, i); + }, + findLastEntry: function (s, o, i) { + return this.toKeyedSeq().reverse().findEntry(s, o, i); + }, + findLastKey: function (s, o) { + return this.toKeyedSeq().reverse().findKey(s, o); + }, + first: function () { + return this.find(returnTrue); + }, + flatMap: function (s, o) { + return reify(this, flatMapFactory(this, s, o)); + }, + flatten: function (s) { + return reify(this, flattenFactory(this, s, !0)); + }, + fromEntrySeq: function () { + return new FromEntriesSequence(this); + }, + get: function (s, o) { + return this.find( + function (o, i) { + return is(i, s); + }, + void 0, + o + ); + }, + getIn: function (s, o) { + for (var i, u = this, _ = forceIterator(s); !(i = _.next()).done; ) { + var w = i.value; + if ((u = u && u.get ? u.get(w, L) : L) === L) return o; + } + return u; + }, + groupBy: function (s, o) { + return groupByFactory(this, s, o); + }, + has: function (s) { + return this.get(s, L) !== L; + }, + hasIn: function (s) { + return this.getIn(s, L) !== L; + }, + isSubset: function (s) { + return ( + (s = 'function' == typeof s.includes ? s : Iterable(s)), + this.every(function (o) { + return s.includes(o); + }) + ); + }, + isSuperset: function (s) { + return (s = 'function' == typeof s.isSubset ? s : Iterable(s)).isSubset(this); + }, + keyOf: function (s) { + return this.findKey(function (o) { + return is(o, s); + }); + }, + keySeq: function () { + return this.toSeq().map(keyMapper).toIndexedSeq(); + }, + last: function () { + return this.toSeq().reverse().first(); + }, + lastKeyOf: function (s) { + return this.toKeyedSeq().reverse().keyOf(s); + }, + max: function (s) { + return maxFactory(this, s); + }, + maxBy: function (s, o) { + return maxFactory(this, o, s); + }, + min: function (s) { + return maxFactory(this, s ? neg(s) : defaultNegComparator); + }, + minBy: function (s, o) { + return maxFactory(this, o ? neg(o) : defaultNegComparator, s); + }, + rest: function () { + return this.slice(1); + }, + skip: function (s) { + return this.slice(Math.max(0, s)); + }, + skipLast: function (s) { + return reify(this, this.toSeq().reverse().skip(s).reverse()); + }, + skipWhile: function (s, o) { + return reify(this, skipWhileFactory(this, s, o, !0)); + }, + skipUntil: function (s, o) { + return this.skipWhile(not(s), o); + }, + sortBy: function (s, o) { + return reify(this, sortFactory(this, o, s)); + }, + take: function (s) { + return this.slice(0, Math.max(0, s)); + }, + takeLast: function (s) { + return reify(this, this.toSeq().reverse().take(s).reverse()); + }, + takeWhile: function (s, o) { + return reify(this, takeWhileFactory(this, s, o)); + }, + takeUntil: function (s, o) { + return this.takeWhile(not(s), o); + }, + valueSeq: function () { + return this.toIndexedSeq(); + }, + hashCode: function () { + return this.__hash || (this.__hash = hashIterable(this)); + } + })); + var pt = Iterable.prototype; + ((pt[o] = !0), + (pt[ee] = pt.values), + (pt.__toJS = pt.toArray), + (pt.__toStringMapper = quoteString), + (pt.inspect = pt.toSource = + function () { + return this.toString(); + }), + (pt.chain = pt.flatMap), + (pt.contains = pt.includes), + mixin(KeyedIterable, { + flip: function () { + return reify(this, flipFactory(this)); + }, + mapEntries: function (s, o) { + var i = this, + u = 0; + return reify( + this, + this.toSeq() + .map(function (_, w) { + return s.call(o, [w, _], u++, i); + }) + .fromEntrySeq() + ); + }, + mapKeys: function (s, o) { + var i = this; + return reify( + this, + this.toSeq() + .flip() + .map(function (u, _) { + return s.call(o, u, _, i); + }) + .flip() + ); + } + })); + var ht = KeyedIterable.prototype; + function keyMapper(s, o) { + return o; + } + function entryMapper(s, o) { + return [o, s]; + } + function not(s) { + return function () { + return !s.apply(this, arguments); + }; + } + function neg(s) { + return function () { + return -s.apply(this, arguments); + }; + } + function quoteString(s) { + return 'string' == typeof s ? JSON.stringify(s) : String(s); + } + function defaultZipper() { + return arrCopy(arguments); + } + function defaultNegComparator(s, o) { + return s < o ? 1 : s > o ? -1 : 0; + } + function hashIterable(s) { + if (s.size === 1 / 0) return 0; + var o = isOrdered(s), + i = isKeyed(s), + u = o ? 1 : 0; + return murmurHashOfSize( + s.__iterate( + i + ? o + ? function (s, o) { + u = (31 * u + hashMerge(hash(s), hash(o))) | 0; + } + : function (s, o) { + u = (u + hashMerge(hash(s), hash(o))) | 0; + } + : o + ? function (s) { + u = (31 * u + hash(s)) | 0; + } + : function (s) { + u = (u + hash(s)) | 0; + } + ), + u + ); + } + function murmurHashOfSize(s, o) { + return ( + (o = pe(o, 3432918353)), + (o = pe((o << 15) | (o >>> -15), 461845907)), + (o = pe((o << 13) | (o >>> -13), 5)), + (o = pe((o = (o + 3864292196) ^ s) ^ (o >>> 16), 2246822507)), + (o = smi((o = pe(o ^ (o >>> 13), 3266489909)) ^ (o >>> 16))) + ); + } + function hashMerge(s, o) { + return s ^ (o + 2654435769 + (s << 6) + (s >> 2)); + } + return ( + (ht[i] = !0), + (ht[ee] = pt.entries), + (ht.__toJS = pt.toObject), + (ht.__toStringMapper = function (s, o) { + return JSON.stringify(o) + ': ' + quoteString(s); + }), + mixin(IndexedIterable, { + toKeyedSeq: function () { + return new ToKeyedSequence(this, !1); + }, + filter: function (s, o) { + return reify(this, filterFactory(this, s, o, !1)); + }, + findIndex: function (s, o) { + var i = this.findEntry(s, o); + return i ? i[0] : -1; + }, + indexOf: function (s) { + var o = this.keyOf(s); + return void 0 === o ? -1 : o; + }, + lastIndexOf: function (s) { + var o = this.lastKeyOf(s); + return void 0 === o ? -1 : o; + }, + reverse: function () { + return reify(this, reverseFactory(this, !1)); + }, + slice: function (s, o) { + return reify(this, sliceFactory(this, s, o, !1)); + }, + splice: function (s, o) { + var i = arguments.length; + if (((o = Math.max(0 | o, 0)), 0 === i || (2 === i && !o))) return this; + s = resolveBegin(s, s < 0 ? this.count() : this.size); + var u = this.slice(0, s); + return reify( + this, + 1 === i ? u : u.concat(arrCopy(arguments, 2), this.slice(s + o)) + ); + }, + findLastIndex: function (s, o) { + var i = this.findLastEntry(s, o); + return i ? i[0] : -1; + }, + first: function () { + return this.get(0); + }, + flatten: function (s) { + return reify(this, flattenFactory(this, s, !1)); + }, + get: function (s, o) { + return (s = wrapIndex(this, s)) < 0 || + this.size === 1 / 0 || + (void 0 !== this.size && s > this.size) + ? o + : this.find( + function (o, i) { + return i === s; + }, + void 0, + o + ); + }, + has: function (s) { + return ( + (s = wrapIndex(this, s)) >= 0 && + (void 0 !== this.size + ? this.size === 1 / 0 || s < this.size + : -1 !== this.indexOf(s)) + ); + }, + interpose: function (s) { + return reify(this, interposeFactory(this, s)); + }, + interleave: function () { + var s = [this].concat(arrCopy(arguments)), + o = zipWithFactory(this.toSeq(), IndexedSeq.of, s), + i = o.flatten(!0); + return (o.size && (i.size = o.size * s.length), reify(this, i)); + }, + keySeq: function () { + return Range(0, this.size); + }, + last: function () { + return this.get(-1); + }, + skipWhile: function (s, o) { + return reify(this, skipWhileFactory(this, s, o, !1)); + }, + zip: function () { + return reify( + this, + zipWithFactory(this, defaultZipper, [this].concat(arrCopy(arguments))) + ); + }, + zipWith: function (s) { + var o = arrCopy(arguments); + return ((o[0] = this), reify(this, zipWithFactory(this, s, o))); + } + }), + (IndexedIterable.prototype[u] = !0), + (IndexedIterable.prototype[_] = !0), + mixin(SetIterable, { + get: function (s, o) { + return this.has(s) ? s : o; + }, + includes: function (s) { + return this.has(s); + }, + keySeq: function () { + return this.valueSeq(); + } + }), + (SetIterable.prototype.has = pt.includes), + (SetIterable.prototype.contains = SetIterable.prototype.includes), + mixin(KeyedSeq, KeyedIterable.prototype), + mixin(IndexedSeq, IndexedIterable.prototype), + mixin(SetSeq, SetIterable.prototype), + mixin(KeyedCollection, KeyedIterable.prototype), + mixin(IndexedCollection, IndexedIterable.prototype), + mixin(SetCollection, SetIterable.prototype), + { + Iterable, + Seq, + Collection, + Map, + OrderedMap, + List, + Stack, + Set, + OrderedSet, + Record, + Range, + Repeat, + is, + fromJS + } + ); + })(); + }, + 56698: (s) => { + 'function' == typeof Object.create + ? (s.exports = function inherits(s, o) { + o && + ((s.super_ = o), + (s.prototype = Object.create(o.prototype, { + constructor: { value: s, enumerable: !1, writable: !0, configurable: !0 } + }))); + }) + : (s.exports = function inherits(s, o) { + if (o) { + s.super_ = o; + var TempCtor = function () {}; + ((TempCtor.prototype = o.prototype), + (s.prototype = new TempCtor()), + (s.prototype.constructor = s)); + } + }); + }, + 5419: (s) => { + s.exports = function (s, o, i, u) { + var _ = new Blob(void 0 !== u ? [u, s] : [s], { + type: i || 'application/octet-stream' + }); + if (void 0 !== window.navigator.msSaveBlob) window.navigator.msSaveBlob(_, o); + else { + var w = + window.URL && window.URL.createObjectURL + ? window.URL.createObjectURL(_) + : window.webkitURL.createObjectURL(_), + x = document.createElement('a'); + ((x.style.display = 'none'), + (x.href = w), + x.setAttribute('download', o), + void 0 === x.download && x.setAttribute('target', '_blank'), + document.body.appendChild(x), + x.click(), + setTimeout(function () { + (document.body.removeChild(x), window.URL.revokeObjectURL(w)); + }, 200)); + } + }; + }, + 20181: (s, o, i) => { + var u = /^\s+|\s+$/g, + _ = /^[-+]0x[0-9a-f]+$/i, + w = /^0b[01]+$/i, + x = /^0o[0-7]+$/i, + C = parseInt, + j = 'object' == typeof i.g && i.g && i.g.Object === Object && i.g, + L = 'object' == typeof self && self && self.Object === Object && self, + B = j || L || Function('return this')(), + $ = Object.prototype.toString, + V = Math.max, + U = Math.min, + now = function () { + return B.Date.now(); + }; + function isObject(s) { + var o = typeof s; + return !!s && ('object' == o || 'function' == o); + } + function toNumber(s) { + if ('number' == typeof s) return s; + if ( + (function isSymbol(s) { + return ( + 'symbol' == typeof s || + ((function isObjectLike(s) { + return !!s && 'object' == typeof s; + })(s) && + '[object Symbol]' == $.call(s)) + ); + })(s) + ) + return NaN; + if (isObject(s)) { + var o = 'function' == typeof s.valueOf ? s.valueOf() : s; + s = isObject(o) ? o + '' : o; + } + if ('string' != typeof s) return 0 === s ? s : +s; + s = s.replace(u, ''); + var i = w.test(s); + return i || x.test(s) ? C(s.slice(2), i ? 2 : 8) : _.test(s) ? NaN : +s; + } + s.exports = function debounce(s, o, i) { + var u, + _, + w, + x, + C, + j, + L = 0, + B = !1, + $ = !1, + z = !0; + if ('function' != typeof s) throw new TypeError('Expected a function'); + function invokeFunc(o) { + var i = u, + w = _; + return ((u = _ = void 0), (L = o), (x = s.apply(w, i))); + } + function shouldInvoke(s) { + var i = s - j; + return void 0 === j || i >= o || i < 0 || ($ && s - L >= w); + } + function timerExpired() { + var s = now(); + if (shouldInvoke(s)) return trailingEdge(s); + C = setTimeout( + timerExpired, + (function remainingWait(s) { + var i = o - (s - j); + return $ ? U(i, w - (s - L)) : i; + })(s) + ); + } + function trailingEdge(s) { + return ((C = void 0), z && u ? invokeFunc(s) : ((u = _ = void 0), x)); + } + function debounced() { + var s = now(), + i = shouldInvoke(s); + if (((u = arguments), (_ = this), (j = s), i)) { + if (void 0 === C) + return (function leadingEdge(s) { + return ((L = s), (C = setTimeout(timerExpired, o)), B ? invokeFunc(s) : x); + })(j); + if ($) return ((C = setTimeout(timerExpired, o)), invokeFunc(j)); + } + return (void 0 === C && (C = setTimeout(timerExpired, o)), x); + } + return ( + (o = toNumber(o) || 0), + isObject(i) && + ((B = !!i.leading), + (w = ($ = 'maxWait' in i) ? V(toNumber(i.maxWait) || 0, o) : w), + (z = 'trailing' in i ? !!i.trailing : z)), + (debounced.cancel = function cancel() { + (void 0 !== C && clearTimeout(C), (L = 0), (u = j = _ = C = void 0)); + }), + (debounced.flush = function flush() { + return void 0 === C ? x : trailingEdge(now()); + }), + debounced + ); + }; + }, + 55580: (s, o, i) => { + var u = i(56110)(i(9325), 'DataView'); + s.exports = u; + }, + 21549: (s, o, i) => { + var u = i(22032), + _ = i(63862), + w = i(66721), + x = i(12749), + C = i(35749); + function Hash(s) { + var o = -1, + i = null == s ? 0 : s.length; + for (this.clear(); ++o < i; ) { + var u = s[o]; + this.set(u[0], u[1]); + } + } + ((Hash.prototype.clear = u), + (Hash.prototype.delete = _), + (Hash.prototype.get = w), + (Hash.prototype.has = x), + (Hash.prototype.set = C), + (s.exports = Hash)); + }, + 30980: (s, o, i) => { + var u = i(39344), + _ = i(94033); + function LazyWrapper(s) { + ((this.__wrapped__ = s), + (this.__actions__ = []), + (this.__dir__ = 1), + (this.__filtered__ = !1), + (this.__iteratees__ = []), + (this.__takeCount__ = 4294967295), + (this.__views__ = [])); + } + ((LazyWrapper.prototype = u(_.prototype)), + (LazyWrapper.prototype.constructor = LazyWrapper), + (s.exports = LazyWrapper)); + }, + 80079: (s, o, i) => { + var u = i(63702), + _ = i(70080), + w = i(24739), + x = i(48655), + C = i(31175); + function ListCache(s) { + var o = -1, + i = null == s ? 0 : s.length; + for (this.clear(); ++o < i; ) { + var u = s[o]; + this.set(u[0], u[1]); + } + } + ((ListCache.prototype.clear = u), + (ListCache.prototype.delete = _), + (ListCache.prototype.get = w), + (ListCache.prototype.has = x), + (ListCache.prototype.set = C), + (s.exports = ListCache)); + }, + 56017: (s, o, i) => { + var u = i(39344), + _ = i(94033); + function LodashWrapper(s, o) { + ((this.__wrapped__ = s), + (this.__actions__ = []), + (this.__chain__ = !!o), + (this.__index__ = 0), + (this.__values__ = void 0)); + } + ((LodashWrapper.prototype = u(_.prototype)), + (LodashWrapper.prototype.constructor = LodashWrapper), + (s.exports = LodashWrapper)); + }, + 68223: (s, o, i) => { + var u = i(56110)(i(9325), 'Map'); + s.exports = u; + }, + 53661: (s, o, i) => { + var u = i(63040), + _ = i(17670), + w = i(90289), + x = i(4509), + C = i(72949); + function MapCache(s) { + var o = -1, + i = null == s ? 0 : s.length; + for (this.clear(); ++o < i; ) { + var u = s[o]; + this.set(u[0], u[1]); + } + } + ((MapCache.prototype.clear = u), + (MapCache.prototype.delete = _), + (MapCache.prototype.get = w), + (MapCache.prototype.has = x), + (MapCache.prototype.set = C), + (s.exports = MapCache)); + }, + 32804: (s, o, i) => { + var u = i(56110)(i(9325), 'Promise'); + s.exports = u; + }, + 76545: (s, o, i) => { + var u = i(56110)(i(9325), 'Set'); + s.exports = u; + }, + 38859: (s, o, i) => { + var u = i(53661), + _ = i(31380), + w = i(51459); + function SetCache(s) { + var o = -1, + i = null == s ? 0 : s.length; + for (this.__data__ = new u(); ++o < i; ) this.add(s[o]); + } + ((SetCache.prototype.add = SetCache.prototype.push = _), + (SetCache.prototype.has = w), + (s.exports = SetCache)); + }, + 37217: (s, o, i) => { + var u = i(80079), + _ = i(51420), + w = i(90938), + x = i(63605), + C = i(29817), + j = i(80945); + function Stack(s) { + var o = (this.__data__ = new u(s)); + this.size = o.size; + } + ((Stack.prototype.clear = _), + (Stack.prototype.delete = w), + (Stack.prototype.get = x), + (Stack.prototype.has = C), + (Stack.prototype.set = j), + (s.exports = Stack)); + }, + 51873: (s, o, i) => { + var u = i(9325).Symbol; + s.exports = u; + }, + 37828: (s, o, i) => { + var u = i(9325).Uint8Array; + s.exports = u; + }, + 28303: (s, o, i) => { + var u = i(56110)(i(9325), 'WeakMap'); + s.exports = u; + }, + 91033: (s) => { + s.exports = function apply(s, o, i) { + switch (i.length) { + case 0: + return s.call(o); + case 1: + return s.call(o, i[0]); + case 2: + return s.call(o, i[0], i[1]); + case 3: + return s.call(o, i[0], i[1], i[2]); + } + return s.apply(o, i); + }; + }, + 83729: (s) => { + s.exports = function arrayEach(s, o) { + for (var i = -1, u = null == s ? 0 : s.length; ++i < u && !1 !== o(s[i], i, s); ); + return s; + }; + }, + 79770: (s) => { + s.exports = function arrayFilter(s, o) { + for (var i = -1, u = null == s ? 0 : s.length, _ = 0, w = []; ++i < u; ) { + var x = s[i]; + o(x, i, s) && (w[_++] = x); + } + return w; + }; + }, + 15325: (s, o, i) => { + var u = i(96131); + s.exports = function arrayIncludes(s, o) { + return !!(null == s ? 0 : s.length) && u(s, o, 0) > -1; + }; + }, + 70695: (s, o, i) => { + var u = i(78096), + _ = i(72428), + w = i(56449), + x = i(3656), + C = i(30361), + j = i(37167), + L = Object.prototype.hasOwnProperty; + s.exports = function arrayLikeKeys(s, o) { + var i = w(s), + B = !i && _(s), + $ = !i && !B && x(s), + V = !i && !B && !$ && j(s), + U = i || B || $ || V, + z = U ? u(s.length, String) : [], + Y = z.length; + for (var Z in s) + (!o && !L.call(s, Z)) || + (U && + ('length' == Z || + ($ && ('offset' == Z || 'parent' == Z)) || + (V && ('buffer' == Z || 'byteLength' == Z || 'byteOffset' == Z)) || + C(Z, Y))) || + z.push(Z); + return z; + }; + }, + 34932: (s) => { + s.exports = function arrayMap(s, o) { + for (var i = -1, u = null == s ? 0 : s.length, _ = Array(u); ++i < u; ) + _[i] = o(s[i], i, s); + return _; + }; + }, + 14528: (s) => { + s.exports = function arrayPush(s, o) { + for (var i = -1, u = o.length, _ = s.length; ++i < u; ) s[_ + i] = o[i]; + return s; + }; + }, + 40882: (s) => { + s.exports = function arrayReduce(s, o, i, u) { + var _ = -1, + w = null == s ? 0 : s.length; + for (u && w && (i = s[++_]); ++_ < w; ) i = o(i, s[_], _, s); + return i; + }; + }, + 14248: (s) => { + s.exports = function arraySome(s, o) { + for (var i = -1, u = null == s ? 0 : s.length; ++i < u; ) if (o(s[i], i, s)) return !0; + return !1; + }; + }, + 61074: (s) => { + s.exports = function asciiToArray(s) { + return s.split(''); + }; + }, + 1733: (s) => { + var o = /[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g; + s.exports = function asciiWords(s) { + return s.match(o) || []; + }; + }, + 87805: (s, o, i) => { + var u = i(43360), + _ = i(75288); + s.exports = function assignMergeValue(s, o, i) { + ((void 0 !== i && !_(s[o], i)) || (void 0 === i && !(o in s))) && u(s, o, i); + }; + }, + 16547: (s, o, i) => { + var u = i(43360), + _ = i(75288), + w = Object.prototype.hasOwnProperty; + s.exports = function assignValue(s, o, i) { + var x = s[o]; + (w.call(s, o) && _(x, i) && (void 0 !== i || o in s)) || u(s, o, i); + }; + }, + 26025: (s, o, i) => { + var u = i(75288); + s.exports = function assocIndexOf(s, o) { + for (var i = s.length; i--; ) if (u(s[i][0], o)) return i; + return -1; + }; + }, + 74733: (s, o, i) => { + var u = i(21791), + _ = i(95950); + s.exports = function baseAssign(s, o) { + return s && u(o, _(o), s); + }; + }, + 43838: (s, o, i) => { + var u = i(21791), + _ = i(37241); + s.exports = function baseAssignIn(s, o) { + return s && u(o, _(o), s); + }; + }, + 43360: (s, o, i) => { + var u = i(93243); + s.exports = function baseAssignValue(s, o, i) { + '__proto__' == o && u + ? u(s, o, { configurable: !0, enumerable: !0, value: i, writable: !0 }) + : (s[o] = i); + }; + }, + 9999: (s, o, i) => { + var u = i(37217), + _ = i(83729), + w = i(16547), + x = i(74733), + C = i(43838), + j = i(93290), + L = i(23007), + B = i(92271), + $ = i(48948), + V = i(50002), + U = i(83349), + z = i(5861), + Y = i(76189), + Z = i(77199), + ee = i(35529), + ie = i(56449), + ae = i(3656), + le = i(87730), + ce = i(23805), + pe = i(38440), + de = i(95950), + fe = i(37241), + ye = '[object Arguments]', + be = '[object Function]', + _e = '[object Object]', + we = {}; + ((we[ye] = + we['[object Array]'] = + we['[object ArrayBuffer]'] = + we['[object DataView]'] = + we['[object Boolean]'] = + we['[object Date]'] = + we['[object Float32Array]'] = + we['[object Float64Array]'] = + we['[object Int8Array]'] = + we['[object Int16Array]'] = + we['[object Int32Array]'] = + we['[object Map]'] = + we['[object Number]'] = + we[_e] = + we['[object RegExp]'] = + we['[object Set]'] = + we['[object String]'] = + we['[object Symbol]'] = + we['[object Uint8Array]'] = + we['[object Uint8ClampedArray]'] = + we['[object Uint16Array]'] = + we['[object Uint32Array]'] = + !0), + (we['[object Error]'] = we[be] = we['[object WeakMap]'] = !1), + (s.exports = function baseClone(s, o, i, Se, xe, Pe) { + var Te, + Re = 1 & o, + qe = 2 & o, + $e = 4 & o; + if ((i && (Te = xe ? i(s, Se, xe, Pe) : i(s)), void 0 !== Te)) return Te; + if (!ce(s)) return s; + var ze = ie(s); + if (ze) { + if (((Te = Y(s)), !Re)) return L(s, Te); + } else { + var We = z(s), + He = We == be || '[object GeneratorFunction]' == We; + if (ae(s)) return j(s, Re); + if (We == _e || We == ye || (He && !xe)) { + if (((Te = qe || He ? {} : ee(s)), !Re)) + return qe ? $(s, C(Te, s)) : B(s, x(Te, s)); + } else { + if (!we[We]) return xe ? s : {}; + Te = Z(s, We, Re); + } + } + Pe || (Pe = new u()); + var Ye = Pe.get(s); + if (Ye) return Ye; + (Pe.set(s, Te), + pe(s) + ? s.forEach(function (u) { + Te.add(baseClone(u, o, i, u, s, Pe)); + }) + : le(s) && + s.forEach(function (u, _) { + Te.set(_, baseClone(u, o, i, _, s, Pe)); + })); + var Xe = ze ? void 0 : ($e ? (qe ? U : V) : qe ? fe : de)(s); + return ( + _(Xe || s, function (u, _) { + (Xe && (u = s[(_ = u)]), w(Te, _, baseClone(u, o, i, _, s, Pe))); + }), + Te + ); + })); + }, + 39344: (s, o, i) => { + var u = i(23805), + _ = Object.create, + w = (function () { + function object() {} + return function (s) { + if (!u(s)) return {}; + if (_) return _(s); + object.prototype = s; + var o = new object(); + return ((object.prototype = void 0), o); + }; + })(); + s.exports = w; + }, + 80909: (s, o, i) => { + var u = i(30641), + _ = i(38329)(u); + s.exports = _; + }, + 2523: (s) => { + s.exports = function baseFindIndex(s, o, i, u) { + for (var _ = s.length, w = i + (u ? 1 : -1); u ? w-- : ++w < _; ) + if (o(s[w], w, s)) return w; + return -1; + }; + }, + 83120: (s, o, i) => { + var u = i(14528), + _ = i(45891); + s.exports = function baseFlatten(s, o, i, w, x) { + var C = -1, + j = s.length; + for (i || (i = _), x || (x = []); ++C < j; ) { + var L = s[C]; + o > 0 && i(L) + ? o > 1 + ? baseFlatten(L, o - 1, i, w, x) + : u(x, L) + : w || (x[x.length] = L); + } + return x; + }; + }, + 86649: (s, o, i) => { + var u = i(83221)(); + s.exports = u; + }, + 30641: (s, o, i) => { + var u = i(86649), + _ = i(95950); + s.exports = function baseForOwn(s, o) { + return s && u(s, o, _); + }; + }, + 47422: (s, o, i) => { + var u = i(31769), + _ = i(77797); + s.exports = function baseGet(s, o) { + for (var i = 0, w = (o = u(o, s)).length; null != s && i < w; ) s = s[_(o[i++])]; + return i && i == w ? s : void 0; + }; + }, + 82199: (s, o, i) => { + var u = i(14528), + _ = i(56449); + s.exports = function baseGetAllKeys(s, o, i) { + var w = o(s); + return _(s) ? w : u(w, i(s)); + }; + }, + 72552: (s, o, i) => { + var u = i(51873), + _ = i(659), + w = i(59350), + x = u ? u.toStringTag : void 0; + s.exports = function baseGetTag(s) { + return null == s + ? void 0 === s + ? '[object Undefined]' + : '[object Null]' + : x && x in Object(s) + ? _(s) + : w(s); + }; + }, + 20426: (s) => { + var o = Object.prototype.hasOwnProperty; + s.exports = function baseHas(s, i) { + return null != s && o.call(s, i); + }; + }, + 28077: (s) => { + s.exports = function baseHasIn(s, o) { + return null != s && o in Object(s); + }; + }, + 96131: (s, o, i) => { + var u = i(2523), + _ = i(85463), + w = i(76959); + s.exports = function baseIndexOf(s, o, i) { + return o == o ? w(s, o, i) : u(s, _, i); + }; + }, + 27534: (s, o, i) => { + var u = i(72552), + _ = i(40346); + s.exports = function baseIsArguments(s) { + return _(s) && '[object Arguments]' == u(s); + }; + }, + 60270: (s, o, i) => { + var u = i(87068), + _ = i(40346); + s.exports = function baseIsEqual(s, o, i, w, x) { + return ( + s === o || + (null == s || null == o || (!_(s) && !_(o)) + ? s != s && o != o + : u(s, o, i, w, baseIsEqual, x)) + ); + }; + }, + 87068: (s, o, i) => { + var u = i(37217), + _ = i(25911), + w = i(21986), + x = i(50689), + C = i(5861), + j = i(56449), + L = i(3656), + B = i(37167), + $ = '[object Arguments]', + V = '[object Array]', + U = '[object Object]', + z = Object.prototype.hasOwnProperty; + s.exports = function baseIsEqualDeep(s, o, i, Y, Z, ee) { + var ie = j(s), + ae = j(o), + le = ie ? V : C(s), + ce = ae ? V : C(o), + pe = (le = le == $ ? U : le) == U, + de = (ce = ce == $ ? U : ce) == U, + fe = le == ce; + if (fe && L(s)) { + if (!L(o)) return !1; + ((ie = !0), (pe = !1)); + } + if (fe && !pe) + return ( + ee || (ee = new u()), + ie || B(s) ? _(s, o, i, Y, Z, ee) : w(s, o, le, i, Y, Z, ee) + ); + if (!(1 & i)) { + var ye = pe && z.call(s, '__wrapped__'), + be = de && z.call(o, '__wrapped__'); + if (ye || be) { + var _e = ye ? s.value() : s, + we = be ? o.value() : o; + return (ee || (ee = new u()), Z(_e, we, i, Y, ee)); + } + } + return !!fe && (ee || (ee = new u()), x(s, o, i, Y, Z, ee)); + }; + }, + 29172: (s, o, i) => { + var u = i(5861), + _ = i(40346); + s.exports = function baseIsMap(s) { + return _(s) && '[object Map]' == u(s); + }; + }, + 41799: (s, o, i) => { + var u = i(37217), + _ = i(60270); + s.exports = function baseIsMatch(s, o, i, w) { + var x = i.length, + C = x, + j = !w; + if (null == s) return !C; + for (s = Object(s); x--; ) { + var L = i[x]; + if (j && L[2] ? L[1] !== s[L[0]] : !(L[0] in s)) return !1; + } + for (; ++x < C; ) { + var B = (L = i[x])[0], + $ = s[B], + V = L[1]; + if (j && L[2]) { + if (void 0 === $ && !(B in s)) return !1; + } else { + var U = new u(); + if (w) var z = w($, V, B, s, o, U); + if (!(void 0 === z ? _(V, $, 3, w, U) : z)) return !1; + } + } + return !0; + }; + }, + 85463: (s) => { + s.exports = function baseIsNaN(s) { + return s != s; + }; + }, + 45083: (s, o, i) => { + var u = i(1882), + _ = i(87296), + w = i(23805), + x = i(47473), + C = /^\[object .+?Constructor\]$/, + j = Function.prototype, + L = Object.prototype, + B = j.toString, + $ = L.hasOwnProperty, + V = RegExp( + '^' + + B.call($) + .replace(/[\\^$.*+?()[\]{}|]/g, '\\$&') + .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + + '$' + ); + s.exports = function baseIsNative(s) { + return !(!w(s) || _(s)) && (u(s) ? V : C).test(x(s)); + }; + }, + 16038: (s, o, i) => { + var u = i(5861), + _ = i(40346); + s.exports = function baseIsSet(s) { + return _(s) && '[object Set]' == u(s); + }; + }, + 4901: (s, o, i) => { + var u = i(72552), + _ = i(30294), + w = i(40346), + x = {}; + ((x['[object Float32Array]'] = + x['[object Float64Array]'] = + x['[object Int8Array]'] = + x['[object Int16Array]'] = + x['[object Int32Array]'] = + x['[object Uint8Array]'] = + x['[object Uint8ClampedArray]'] = + x['[object Uint16Array]'] = + x['[object Uint32Array]'] = + !0), + (x['[object Arguments]'] = + x['[object Array]'] = + x['[object ArrayBuffer]'] = + x['[object Boolean]'] = + x['[object DataView]'] = + x['[object Date]'] = + x['[object Error]'] = + x['[object Function]'] = + x['[object Map]'] = + x['[object Number]'] = + x['[object Object]'] = + x['[object RegExp]'] = + x['[object Set]'] = + x['[object String]'] = + x['[object WeakMap]'] = + !1), + (s.exports = function baseIsTypedArray(s) { + return w(s) && _(s.length) && !!x[u(s)]; + })); + }, + 15389: (s, o, i) => { + var u = i(93663), + _ = i(87978), + w = i(83488), + x = i(56449), + C = i(50583); + s.exports = function baseIteratee(s) { + return 'function' == typeof s + ? s + : null == s + ? w + : 'object' == typeof s + ? x(s) + ? _(s[0], s[1]) + : u(s) + : C(s); + }; + }, + 88984: (s, o, i) => { + var u = i(55527), + _ = i(3650), + w = Object.prototype.hasOwnProperty; + s.exports = function baseKeys(s) { + if (!u(s)) return _(s); + var o = []; + for (var i in Object(s)) w.call(s, i) && 'constructor' != i && o.push(i); + return o; + }; + }, + 72903: (s, o, i) => { + var u = i(23805), + _ = i(55527), + w = i(90181), + x = Object.prototype.hasOwnProperty; + s.exports = function baseKeysIn(s) { + if (!u(s)) return w(s); + var o = _(s), + i = []; + for (var C in s) ('constructor' != C || (!o && x.call(s, C))) && i.push(C); + return i; + }; + }, + 94033: (s) => { + s.exports = function baseLodash() {}; + }, + 93663: (s, o, i) => { + var u = i(41799), + _ = i(10776), + w = i(67197); + s.exports = function baseMatches(s) { + var o = _(s); + return 1 == o.length && o[0][2] + ? w(o[0][0], o[0][1]) + : function (i) { + return i === s || u(i, s, o); + }; + }; + }, + 87978: (s, o, i) => { + var u = i(60270), + _ = i(58156), + w = i(80631), + x = i(28586), + C = i(30756), + j = i(67197), + L = i(77797); + s.exports = function baseMatchesProperty(s, o) { + return x(s) && C(o) + ? j(L(s), o) + : function (i) { + var x = _(i, s); + return void 0 === x && x === o ? w(i, s) : u(o, x, 3); + }; + }; + }, + 85250: (s, o, i) => { + var u = i(37217), + _ = i(87805), + w = i(86649), + x = i(42824), + C = i(23805), + j = i(37241), + L = i(14974); + s.exports = function baseMerge(s, o, i, B, $) { + s !== o && + w( + o, + function (w, j) { + if (($ || ($ = new u()), C(w))) x(s, o, j, i, baseMerge, B, $); + else { + var V = B ? B(L(s, j), w, j + '', s, o, $) : void 0; + (void 0 === V && (V = w), _(s, j, V)); + } + }, + j + ); + }; + }, + 42824: (s, o, i) => { + var u = i(87805), + _ = i(93290), + w = i(71961), + x = i(23007), + C = i(35529), + j = i(72428), + L = i(56449), + B = i(83693), + $ = i(3656), + V = i(1882), + U = i(23805), + z = i(11331), + Y = i(37167), + Z = i(14974), + ee = i(69884); + s.exports = function baseMergeDeep(s, o, i, ie, ae, le, ce) { + var pe = Z(s, i), + de = Z(o, i), + fe = ce.get(de); + if (fe) u(s, i, fe); + else { + var ye = le ? le(pe, de, i + '', s, o, ce) : void 0, + be = void 0 === ye; + if (be) { + var _e = L(de), + we = !_e && $(de), + Se = !_e && !we && Y(de); + ((ye = de), + _e || we || Se + ? L(pe) + ? (ye = pe) + : B(pe) + ? (ye = x(pe)) + : we + ? ((be = !1), (ye = _(de, !0))) + : Se + ? ((be = !1), (ye = w(de, !0))) + : (ye = []) + : z(de) || j(de) + ? ((ye = pe), j(pe) ? (ye = ee(pe)) : (U(pe) && !V(pe)) || (ye = C(de))) + : (be = !1)); + } + (be && (ce.set(de, ye), ae(ye, de, ie, le, ce), ce.delete(de)), u(s, i, ye)); + } + }; + }, + 47237: (s) => { + s.exports = function baseProperty(s) { + return function (o) { + return null == o ? void 0 : o[s]; + }; + }; + }, + 17255: (s, o, i) => { + var u = i(47422); + s.exports = function basePropertyDeep(s) { + return function (o) { + return u(o, s); + }; + }; + }, + 54552: (s) => { + s.exports = function basePropertyOf(s) { + return function (o) { + return null == s ? void 0 : s[o]; + }; + }; + }, + 85558: (s) => { + s.exports = function baseReduce(s, o, i, u, _) { + return ( + _(s, function (s, _, w) { + i = u ? ((u = !1), s) : o(i, s, _, w); + }), + i + ); + }; + }, + 69302: (s, o, i) => { + var u = i(83488), + _ = i(56757), + w = i(32865); + s.exports = function baseRest(s, o) { + return w(_(s, o, u), s + ''); + }; + }, + 73170: (s, o, i) => { + var u = i(16547), + _ = i(31769), + w = i(30361), + x = i(23805), + C = i(77797); + s.exports = function baseSet(s, o, i, j) { + if (!x(s)) return s; + for (var L = -1, B = (o = _(o, s)).length, $ = B - 1, V = s; null != V && ++L < B; ) { + var U = C(o[L]), + z = i; + if ('__proto__' === U || 'constructor' === U || 'prototype' === U) return s; + if (L != $) { + var Y = V[U]; + void 0 === (z = j ? j(Y, U, V) : void 0) && (z = x(Y) ? Y : w(o[L + 1]) ? [] : {}); + } + (u(V, U, z), (V = V[U])); + } + return s; + }; + }, + 68882: (s, o, i) => { + var u = i(83488), + _ = i(48152), + w = _ + ? function (s, o) { + return (_.set(s, o), s); + } + : u; + s.exports = w; + }, + 19570: (s, o, i) => { + var u = i(37334), + _ = i(93243), + w = i(83488), + x = _ + ? function (s, o) { + return _(s, 'toString', { + configurable: !0, + enumerable: !1, + value: u(o), + writable: !0 + }); + } + : w; + s.exports = x; + }, + 25160: (s) => { + s.exports = function baseSlice(s, o, i) { + var u = -1, + _ = s.length; + (o < 0 && (o = -o > _ ? 0 : _ + o), + (i = i > _ ? _ : i) < 0 && (i += _), + (_ = o > i ? 0 : (i - o) >>> 0), + (o >>>= 0)); + for (var w = Array(_); ++u < _; ) w[u] = s[u + o]; + return w; + }; + }, + 90916: (s, o, i) => { + var u = i(80909); + s.exports = function baseSome(s, o) { + var i; + return ( + u(s, function (s, u, _) { + return !(i = o(s, u, _)); + }), + !!i + ); + }; + }, + 78096: (s) => { + s.exports = function baseTimes(s, o) { + for (var i = -1, u = Array(s); ++i < s; ) u[i] = o(i); + return u; + }; + }, + 77556: (s, o, i) => { + var u = i(51873), + _ = i(34932), + w = i(56449), + x = i(44394), + C = u ? u.prototype : void 0, + j = C ? C.toString : void 0; + s.exports = function baseToString(s) { + if ('string' == typeof s) return s; + if (w(s)) return _(s, baseToString) + ''; + if (x(s)) return j ? j.call(s) : ''; + var o = s + ''; + return '0' == o && 1 / s == -1 / 0 ? '-0' : o; + }; + }, + 54128: (s, o, i) => { + var u = i(31800), + _ = /^\s+/; + s.exports = function baseTrim(s) { + return s ? s.slice(0, u(s) + 1).replace(_, '') : s; + }; + }, + 27301: (s) => { + s.exports = function baseUnary(s) { + return function (o) { + return s(o); + }; + }; + }, + 19931: (s, o, i) => { + var u = i(31769), + _ = i(68090), + w = i(68969), + x = i(77797); + s.exports = function baseUnset(s, o) { + return ((o = u(o, s)), null == (s = w(s, o)) || delete s[x(_(o))]); + }; + }, + 51234: (s) => { + s.exports = function baseZipObject(s, o, i) { + for (var u = -1, _ = s.length, w = o.length, x = {}; ++u < _; ) { + var C = u < w ? o[u] : void 0; + i(x, s[u], C); + } + return x; + }; + }, + 19219: (s) => { + s.exports = function cacheHas(s, o) { + return s.has(o); + }; + }, + 31769: (s, o, i) => { + var u = i(56449), + _ = i(28586), + w = i(61802), + x = i(13222); + s.exports = function castPath(s, o) { + return u(s) ? s : _(s, o) ? [s] : w(x(s)); + }; + }, + 28754: (s, o, i) => { + var u = i(25160); + s.exports = function castSlice(s, o, i) { + var _ = s.length; + return ((i = void 0 === i ? _ : i), !o && i >= _ ? s : u(s, o, i)); + }; + }, + 49653: (s, o, i) => { + var u = i(37828); + s.exports = function cloneArrayBuffer(s) { + var o = new s.constructor(s.byteLength); + return (new u(o).set(new u(s)), o); + }; + }, + 93290: (s, o, i) => { + s = i.nmd(s); + var u = i(9325), + _ = o && !o.nodeType && o, + w = _ && s && !s.nodeType && s, + x = w && w.exports === _ ? u.Buffer : void 0, + C = x ? x.allocUnsafe : void 0; + s.exports = function cloneBuffer(s, o) { + if (o) return s.slice(); + var i = s.length, + u = C ? C(i) : new s.constructor(i); + return (s.copy(u), u); + }; + }, + 76169: (s, o, i) => { + var u = i(49653); + s.exports = function cloneDataView(s, o) { + var i = o ? u(s.buffer) : s.buffer; + return new s.constructor(i, s.byteOffset, s.byteLength); + }; + }, + 73201: (s) => { + var o = /\w*$/; + s.exports = function cloneRegExp(s) { + var i = new s.constructor(s.source, o.exec(s)); + return ((i.lastIndex = s.lastIndex), i); + }; + }, + 93736: (s, o, i) => { + var u = i(51873), + _ = u ? u.prototype : void 0, + w = _ ? _.valueOf : void 0; + s.exports = function cloneSymbol(s) { + return w ? Object(w.call(s)) : {}; + }; + }, + 71961: (s, o, i) => { + var u = i(49653); + s.exports = function cloneTypedArray(s, o) { + var i = o ? u(s.buffer) : s.buffer; + return new s.constructor(i, s.byteOffset, s.length); + }; + }, + 91596: (s) => { + var o = Math.max; + s.exports = function composeArgs(s, i, u, _) { + for ( + var w = -1, + x = s.length, + C = u.length, + j = -1, + L = i.length, + B = o(x - C, 0), + $ = Array(L + B), + V = !_; + ++j < L; + ) + $[j] = i[j]; + for (; ++w < C; ) (V || w < x) && ($[u[w]] = s[w]); + for (; B--; ) $[j++] = s[w++]; + return $; + }; + }, + 53320: (s) => { + var o = Math.max; + s.exports = function composeArgsRight(s, i, u, _) { + for ( + var w = -1, + x = s.length, + C = -1, + j = u.length, + L = -1, + B = i.length, + $ = o(x - j, 0), + V = Array($ + B), + U = !_; + ++w < $; + ) + V[w] = s[w]; + for (var z = w; ++L < B; ) V[z + L] = i[L]; + for (; ++C < j; ) (U || w < x) && (V[z + u[C]] = s[w++]); + return V; + }; + }, + 23007: (s) => { + s.exports = function copyArray(s, o) { + var i = -1, + u = s.length; + for (o || (o = Array(u)); ++i < u; ) o[i] = s[i]; + return o; + }; + }, + 21791: (s, o, i) => { + var u = i(16547), + _ = i(43360); + s.exports = function copyObject(s, o, i, w) { + var x = !i; + i || (i = {}); + for (var C = -1, j = o.length; ++C < j; ) { + var L = o[C], + B = w ? w(i[L], s[L], L, i, s) : void 0; + (void 0 === B && (B = s[L]), x ? _(i, L, B) : u(i, L, B)); + } + return i; + }; + }, + 92271: (s, o, i) => { + var u = i(21791), + _ = i(4664); + s.exports = function copySymbols(s, o) { + return u(s, _(s), o); + }; + }, + 48948: (s, o, i) => { + var u = i(21791), + _ = i(86375); + s.exports = function copySymbolsIn(s, o) { + return u(s, _(s), o); + }; + }, + 55481: (s, o, i) => { + var u = i(9325)['__core-js_shared__']; + s.exports = u; + }, + 58523: (s) => { + s.exports = function countHolders(s, o) { + for (var i = s.length, u = 0; i--; ) s[i] === o && ++u; + return u; + }; + }, + 20999: (s, o, i) => { + var u = i(69302), + _ = i(36800); + s.exports = function createAssigner(s) { + return u(function (o, i) { + var u = -1, + w = i.length, + x = w > 1 ? i[w - 1] : void 0, + C = w > 2 ? i[2] : void 0; + for ( + x = s.length > 3 && 'function' == typeof x ? (w--, x) : void 0, + C && _(i[0], i[1], C) && ((x = w < 3 ? void 0 : x), (w = 1)), + o = Object(o); + ++u < w; + ) { + var j = i[u]; + j && s(o, j, u, x); + } + return o; + }); + }; + }, + 38329: (s, o, i) => { + var u = i(64894); + s.exports = function createBaseEach(s, o) { + return function (i, _) { + if (null == i) return i; + if (!u(i)) return s(i, _); + for ( + var w = i.length, x = o ? w : -1, C = Object(i); + (o ? x-- : ++x < w) && !1 !== _(C[x], x, C); + ); + return i; + }; + }; + }, + 83221: (s) => { + s.exports = function createBaseFor(s) { + return function (o, i, u) { + for (var _ = -1, w = Object(o), x = u(o), C = x.length; C--; ) { + var j = x[s ? C : ++_]; + if (!1 === i(w[j], j, w)) break; + } + return o; + }; + }; + }, + 11842: (s, o, i) => { + var u = i(82819), + _ = i(9325); + s.exports = function createBind(s, o, i) { + var w = 1 & o, + x = u(s); + return function wrapper() { + return (this && this !== _ && this instanceof wrapper ? x : s).apply( + w ? i : this, + arguments + ); + }; + }; + }, + 12507: (s, o, i) => { + var u = i(28754), + _ = i(49698), + w = i(63912), + x = i(13222); + s.exports = function createCaseFirst(s) { + return function (o) { + o = x(o); + var i = _(o) ? w(o) : void 0, + C = i ? i[0] : o.charAt(0), + j = i ? u(i, 1).join('') : o.slice(1); + return C[s]() + j; + }; + }; + }, + 45539: (s, o, i) => { + var u = i(40882), + _ = i(50828), + w = i(66645), + x = RegExp("['’]", 'g'); + s.exports = function createCompounder(s) { + return function (o) { + return u(w(_(o).replace(x, '')), s, ''); + }; + }; + }, + 82819: (s, o, i) => { + var u = i(39344), + _ = i(23805); + s.exports = function createCtor(s) { + return function () { + var o = arguments; + switch (o.length) { + case 0: + return new s(); + case 1: + return new s(o[0]); + case 2: + return new s(o[0], o[1]); + case 3: + return new s(o[0], o[1], o[2]); + case 4: + return new s(o[0], o[1], o[2], o[3]); + case 5: + return new s(o[0], o[1], o[2], o[3], o[4]); + case 6: + return new s(o[0], o[1], o[2], o[3], o[4], o[5]); + case 7: + return new s(o[0], o[1], o[2], o[3], o[4], o[5], o[6]); + } + var i = u(s.prototype), + w = s.apply(i, o); + return _(w) ? w : i; + }; + }; + }, + 77078: (s, o, i) => { + var u = i(91033), + _ = i(82819), + w = i(37471), + x = i(18073), + C = i(11287), + j = i(36306), + L = i(9325); + s.exports = function createCurry(s, o, i) { + var B = _(s); + return function wrapper() { + for (var _ = arguments.length, $ = Array(_), V = _, U = C(wrapper); V--; ) + $[V] = arguments[V]; + var z = _ < 3 && $[0] !== U && $[_ - 1] !== U ? [] : j($, U); + return (_ -= z.length) < i + ? x(s, o, w, wrapper.placeholder, void 0, $, z, void 0, void 0, i - _) + : u(this && this !== L && this instanceof wrapper ? B : s, this, $); + }; + }; + }, + 62006: (s, o, i) => { + var u = i(15389), + _ = i(64894), + w = i(95950); + s.exports = function createFind(s) { + return function (o, i, x) { + var C = Object(o); + if (!_(o)) { + var j = u(i, 3); + ((o = w(o)), + (i = function (s) { + return j(C[s], s, C); + })); + } + var L = s(o, i, x); + return L > -1 ? C[j ? o[L] : L] : void 0; + }; + }; + }, + 37471: (s, o, i) => { + var u = i(91596), + _ = i(53320), + w = i(58523), + x = i(82819), + C = i(18073), + j = i(11287), + L = i(68294), + B = i(36306), + $ = i(9325); + s.exports = function createHybrid(s, o, i, V, U, z, Y, Z, ee, ie) { + var ae = 128 & o, + le = 1 & o, + ce = 2 & o, + pe = 24 & o, + de = 512 & o, + fe = ce ? void 0 : x(s); + return function wrapper() { + for (var ye = arguments.length, be = Array(ye), _e = ye; _e--; ) + be[_e] = arguments[_e]; + if (pe) + var we = j(wrapper), + Se = w(be, we); + if ( + (V && (be = u(be, V, U, pe)), + z && (be = _(be, z, Y, pe)), + (ye -= Se), + pe && ye < ie) + ) { + var xe = B(be, we); + return C(s, o, createHybrid, wrapper.placeholder, i, be, xe, Z, ee, ie - ye); + } + var Pe = le ? i : this, + Te = ce ? Pe[s] : s; + return ( + (ye = be.length), + Z ? (be = L(be, Z)) : de && ye > 1 && be.reverse(), + ae && ee < ye && (be.length = ee), + this && this !== $ && this instanceof wrapper && (Te = fe || x(Te)), + Te.apply(Pe, be) + ); + }; + }; + }, + 24168: (s, o, i) => { + var u = i(91033), + _ = i(82819), + w = i(9325); + s.exports = function createPartial(s, o, i, x) { + var C = 1 & o, + j = _(s); + return function wrapper() { + for ( + var o = -1, + _ = arguments.length, + L = -1, + B = x.length, + $ = Array(B + _), + V = this && this !== w && this instanceof wrapper ? j : s; + ++L < B; + ) + $[L] = x[L]; + for (; _--; ) $[L++] = arguments[++o]; + return u(V, C ? i : this, $); + }; + }; + }, + 18073: (s, o, i) => { + var u = i(85087), + _ = i(54641), + w = i(70981); + s.exports = function createRecurry(s, o, i, x, C, j, L, B, $, V) { + var U = 8 & o; + ((o |= U ? 32 : 64), 4 & (o &= ~(U ? 64 : 32)) || (o &= -4)); + var z = [ + s, + o, + C, + U ? j : void 0, + U ? L : void 0, + U ? void 0 : j, + U ? void 0 : L, + B, + $, + V + ], + Y = i.apply(void 0, z); + return (u(s) && _(Y, z), (Y.placeholder = x), w(Y, s, o)); + }; + }, + 66977: (s, o, i) => { + var u = i(68882), + _ = i(11842), + w = i(77078), + x = i(37471), + C = i(24168), + j = i(37381), + L = i(3209), + B = i(54641), + $ = i(70981), + V = i(61489), + U = Math.max; + s.exports = function createWrap(s, o, i, z, Y, Z, ee, ie) { + var ae = 2 & o; + if (!ae && 'function' != typeof s) throw new TypeError('Expected a function'); + var le = z ? z.length : 0; + if ( + (le || ((o &= -97), (z = Y = void 0)), + (ee = void 0 === ee ? ee : U(V(ee), 0)), + (ie = void 0 === ie ? ie : V(ie)), + (le -= Y ? Y.length : 0), + 64 & o) + ) { + var ce = z, + pe = Y; + z = Y = void 0; + } + var de = ae ? void 0 : j(s), + fe = [s, o, i, z, Y, ce, pe, Z, ee, ie]; + if ( + (de && L(fe, de), + (s = fe[0]), + (o = fe[1]), + (i = fe[2]), + (z = fe[3]), + (Y = fe[4]), + !(ie = fe[9] = void 0 === fe[9] ? (ae ? 0 : s.length) : U(fe[9] - le, 0)) && + 24 & o && + (o &= -25), + o && 1 != o) + ) + ye = + 8 == o || 16 == o + ? w(s, o, ie) + : (32 != o && 33 != o) || Y.length + ? x.apply(void 0, fe) + : C(s, o, i, z); + else var ye = _(s, o, i); + return $((de ? u : B)(ye, fe), s, o); + }; + }, + 53138: (s, o, i) => { + var u = i(11331); + s.exports = function customOmitClone(s) { + return u(s) ? void 0 : s; + }; + }, + 24647: (s, o, i) => { + var u = i(54552)({ + À: 'A', + Á: 'A', + Â: 'A', + Ã: 'A', + Ä: 'A', + Å: 'A', + à: 'a', + á: 'a', + â: 'a', + ã: 'a', + ä: 'a', + å: 'a', + Ç: 'C', + ç: 'c', + Ð: 'D', + ð: 'd', + È: 'E', + É: 'E', + Ê: 'E', + Ë: 'E', + è: 'e', + é: 'e', + ê: 'e', + ë: 'e', + Ì: 'I', + Í: 'I', + Î: 'I', + Ï: 'I', + ì: 'i', + í: 'i', + î: 'i', + ï: 'i', + Ñ: 'N', + ñ: 'n', + Ò: 'O', + Ó: 'O', + Ô: 'O', + Õ: 'O', + Ö: 'O', + Ø: 'O', + ò: 'o', + ó: 'o', + ô: 'o', + õ: 'o', + ö: 'o', + ø: 'o', + Ù: 'U', + Ú: 'U', + Û: 'U', + Ü: 'U', + ù: 'u', + ú: 'u', + û: 'u', + ü: 'u', + Ý: 'Y', + ý: 'y', + ÿ: 'y', + Æ: 'Ae', + æ: 'ae', + Þ: 'Th', + þ: 'th', + ß: 'ss', + Ā: 'A', + Ă: 'A', + Ą: 'A', + ā: 'a', + ă: 'a', + ą: 'a', + Ć: 'C', + Ĉ: 'C', + Ċ: 'C', + Č: 'C', + ć: 'c', + ĉ: 'c', + ċ: 'c', + č: 'c', + Ď: 'D', + Đ: 'D', + ď: 'd', + đ: 'd', + Ē: 'E', + Ĕ: 'E', + Ė: 'E', + Ę: 'E', + Ě: 'E', + ē: 'e', + ĕ: 'e', + ė: 'e', + ę: 'e', + ě: 'e', + Ĝ: 'G', + Ğ: 'G', + Ġ: 'G', + Ģ: 'G', + ĝ: 'g', + ğ: 'g', + ġ: 'g', + ģ: 'g', + Ĥ: 'H', + Ħ: 'H', + ĥ: 'h', + ħ: 'h', + Ĩ: 'I', + Ī: 'I', + Ĭ: 'I', + Į: 'I', + İ: 'I', + ĩ: 'i', + ī: 'i', + ĭ: 'i', + į: 'i', + ı: 'i', + Ĵ: 'J', + ĵ: 'j', + Ķ: 'K', + ķ: 'k', + ĸ: 'k', + Ĺ: 'L', + Ļ: 'L', + Ľ: 'L', + Ŀ: 'L', + Ł: 'L', + ĺ: 'l', + ļ: 'l', + ľ: 'l', + ŀ: 'l', + ł: 'l', + Ń: 'N', + Ņ: 'N', + Ň: 'N', + Ŋ: 'N', + ń: 'n', + ņ: 'n', + ň: 'n', + ŋ: 'n', + Ō: 'O', + Ŏ: 'O', + Ő: 'O', + ō: 'o', + ŏ: 'o', + ő: 'o', + Ŕ: 'R', + Ŗ: 'R', + Ř: 'R', + ŕ: 'r', + ŗ: 'r', + ř: 'r', + Ś: 'S', + Ŝ: 'S', + Ş: 'S', + Š: 'S', + ś: 's', + ŝ: 's', + ş: 's', + š: 's', + Ţ: 'T', + Ť: 'T', + Ŧ: 'T', + ţ: 't', + ť: 't', + ŧ: 't', + Ũ: 'U', + Ū: 'U', + Ŭ: 'U', + Ů: 'U', + Ű: 'U', + Ų: 'U', + ũ: 'u', + ū: 'u', + ŭ: 'u', + ů: 'u', + ű: 'u', + ų: 'u', + Ŵ: 'W', + ŵ: 'w', + Ŷ: 'Y', + ŷ: 'y', + Ÿ: 'Y', + Ź: 'Z', + Ż: 'Z', + Ž: 'Z', + ź: 'z', + ż: 'z', + ž: 'z', + IJ: 'IJ', + ij: 'ij', + Œ: 'Oe', + œ: 'oe', + ʼn: "'n", + ſ: 's' + }); + s.exports = u; + }, + 93243: (s, o, i) => { + var u = i(56110), + _ = (function () { + try { + var s = u(Object, 'defineProperty'); + return (s({}, '', {}), s); + } catch (s) {} + })(); + s.exports = _; + }, + 25911: (s, o, i) => { + var u = i(38859), + _ = i(14248), + w = i(19219); + s.exports = function equalArrays(s, o, i, x, C, j) { + var L = 1 & i, + B = s.length, + $ = o.length; + if (B != $ && !(L && $ > B)) return !1; + var V = j.get(s), + U = j.get(o); + if (V && U) return V == o && U == s; + var z = -1, + Y = !0, + Z = 2 & i ? new u() : void 0; + for (j.set(s, o), j.set(o, s); ++z < B; ) { + var ee = s[z], + ie = o[z]; + if (x) var ae = L ? x(ie, ee, z, o, s, j) : x(ee, ie, z, s, o, j); + if (void 0 !== ae) { + if (ae) continue; + Y = !1; + break; + } + if (Z) { + if ( + !_(o, function (s, o) { + if (!w(Z, o) && (ee === s || C(ee, s, i, x, j))) return Z.push(o); + }) + ) { + Y = !1; + break; + } + } else if (ee !== ie && !C(ee, ie, i, x, j)) { + Y = !1; + break; + } + } + return (j.delete(s), j.delete(o), Y); + }; + }, + 21986: (s, o, i) => { + var u = i(51873), + _ = i(37828), + w = i(75288), + x = i(25911), + C = i(20317), + j = i(84247), + L = u ? u.prototype : void 0, + B = L ? L.valueOf : void 0; + s.exports = function equalByTag(s, o, i, u, L, $, V) { + switch (i) { + case '[object DataView]': + if (s.byteLength != o.byteLength || s.byteOffset != o.byteOffset) return !1; + ((s = s.buffer), (o = o.buffer)); + case '[object ArrayBuffer]': + return !(s.byteLength != o.byteLength || !$(new _(s), new _(o))); + case '[object Boolean]': + case '[object Date]': + case '[object Number]': + return w(+s, +o); + case '[object Error]': + return s.name == o.name && s.message == o.message; + case '[object RegExp]': + case '[object String]': + return s == o + ''; + case '[object Map]': + var U = C; + case '[object Set]': + var z = 1 & u; + if ((U || (U = j), s.size != o.size && !z)) return !1; + var Y = V.get(s); + if (Y) return Y == o; + ((u |= 2), V.set(s, o)); + var Z = x(U(s), U(o), u, L, $, V); + return (V.delete(s), Z); + case '[object Symbol]': + if (B) return B.call(s) == B.call(o); + } + return !1; + }; + }, + 50689: (s, o, i) => { + var u = i(50002), + _ = Object.prototype.hasOwnProperty; + s.exports = function equalObjects(s, o, i, w, x, C) { + var j = 1 & i, + L = u(s), + B = L.length; + if (B != u(o).length && !j) return !1; + for (var $ = B; $--; ) { + var V = L[$]; + if (!(j ? V in o : _.call(o, V))) return !1; + } + var U = C.get(s), + z = C.get(o); + if (U && z) return U == o && z == s; + var Y = !0; + (C.set(s, o), C.set(o, s)); + for (var Z = j; ++$ < B; ) { + var ee = s[(V = L[$])], + ie = o[V]; + if (w) var ae = j ? w(ie, ee, V, o, s, C) : w(ee, ie, V, s, o, C); + if (!(void 0 === ae ? ee === ie || x(ee, ie, i, w, C) : ae)) { + Y = !1; + break; + } + Z || (Z = 'constructor' == V); + } + if (Y && !Z) { + var le = s.constructor, + ce = o.constructor; + le == ce || + !('constructor' in s) || + !('constructor' in o) || + ('function' == typeof le && + le instanceof le && + 'function' == typeof ce && + ce instanceof ce) || + (Y = !1); + } + return (C.delete(s), C.delete(o), Y); + }; + }, + 38816: (s, o, i) => { + var u = i(35970), + _ = i(56757), + w = i(32865); + s.exports = function flatRest(s) { + return w(_(s, void 0, u), s + ''); + }; + }, + 34840: (s, o, i) => { + var u = 'object' == typeof i.g && i.g && i.g.Object === Object && i.g; + s.exports = u; + }, + 50002: (s, o, i) => { + var u = i(82199), + _ = i(4664), + w = i(95950); + s.exports = function getAllKeys(s) { + return u(s, w, _); + }; + }, + 83349: (s, o, i) => { + var u = i(82199), + _ = i(86375), + w = i(37241); + s.exports = function getAllKeysIn(s) { + return u(s, w, _); + }; + }, + 37381: (s, o, i) => { + var u = i(48152), + _ = i(63950), + w = u + ? function (s) { + return u.get(s); + } + : _; + s.exports = w; + }, + 62284: (s, o, i) => { + var u = i(84629), + _ = Object.prototype.hasOwnProperty; + s.exports = function getFuncName(s) { + for (var o = s.name + '', i = u[o], w = _.call(u, o) ? i.length : 0; w--; ) { + var x = i[w], + C = x.func; + if (null == C || C == s) return x.name; + } + return o; + }; + }, + 11287: (s) => { + s.exports = function getHolder(s) { + return s.placeholder; + }; + }, + 12651: (s, o, i) => { + var u = i(74218); + s.exports = function getMapData(s, o) { + var i = s.__data__; + return u(o) ? i['string' == typeof o ? 'string' : 'hash'] : i.map; + }; + }, + 10776: (s, o, i) => { + var u = i(30756), + _ = i(95950); + s.exports = function getMatchData(s) { + for (var o = _(s), i = o.length; i--; ) { + var w = o[i], + x = s[w]; + o[i] = [w, x, u(x)]; + } + return o; + }; + }, + 56110: (s, o, i) => { + var u = i(45083), + _ = i(10392); + s.exports = function getNative(s, o) { + var i = _(s, o); + return u(i) ? i : void 0; + }; + }, + 28879: (s, o, i) => { + var u = i(74335)(Object.getPrototypeOf, Object); + s.exports = u; + }, + 659: (s, o, i) => { + var u = i(51873), + _ = Object.prototype, + w = _.hasOwnProperty, + x = _.toString, + C = u ? u.toStringTag : void 0; + s.exports = function getRawTag(s) { + var o = w.call(s, C), + i = s[C]; + try { + s[C] = void 0; + var u = !0; + } catch (s) {} + var _ = x.call(s); + return (u && (o ? (s[C] = i) : delete s[C]), _); + }; + }, + 4664: (s, o, i) => { + var u = i(79770), + _ = i(63345), + w = Object.prototype.propertyIsEnumerable, + x = Object.getOwnPropertySymbols, + C = x + ? function (s) { + return null == s + ? [] + : ((s = Object(s)), + u(x(s), function (o) { + return w.call(s, o); + })); + } + : _; + s.exports = C; + }, + 86375: (s, o, i) => { + var u = i(14528), + _ = i(28879), + w = i(4664), + x = i(63345), + C = Object.getOwnPropertySymbols + ? function (s) { + for (var o = []; s; ) (u(o, w(s)), (s = _(s))); + return o; + } + : x; + s.exports = C; + }, + 5861: (s, o, i) => { + var u = i(55580), + _ = i(68223), + w = i(32804), + x = i(76545), + C = i(28303), + j = i(72552), + L = i(47473), + B = '[object Map]', + $ = '[object Promise]', + V = '[object Set]', + U = '[object WeakMap]', + z = '[object DataView]', + Y = L(u), + Z = L(_), + ee = L(w), + ie = L(x), + ae = L(C), + le = j; + (((u && le(new u(new ArrayBuffer(1))) != z) || + (_ && le(new _()) != B) || + (w && le(w.resolve()) != $) || + (x && le(new x()) != V) || + (C && le(new C()) != U)) && + (le = function (s) { + var o = j(s), + i = '[object Object]' == o ? s.constructor : void 0, + u = i ? L(i) : ''; + if (u) + switch (u) { + case Y: + return z; + case Z: + return B; + case ee: + return $; + case ie: + return V; + case ae: + return U; + } + return o; + }), + (s.exports = le)); + }, + 10392: (s) => { + s.exports = function getValue(s, o) { + return null == s ? void 0 : s[o]; + }; + }, + 75251: (s) => { + var o = /\{\n\/\* \[wrapped with (.+)\] \*/, + i = /,? & /; + s.exports = function getWrapDetails(s) { + var u = s.match(o); + return u ? u[1].split(i) : []; + }; + }, + 49326: (s, o, i) => { + var u = i(31769), + _ = i(72428), + w = i(56449), + x = i(30361), + C = i(30294), + j = i(77797); + s.exports = function hasPath(s, o, i) { + for (var L = -1, B = (o = u(o, s)).length, $ = !1; ++L < B; ) { + var V = j(o[L]); + if (!($ = null != s && i(s, V))) break; + s = s[V]; + } + return $ || ++L != B + ? $ + : !!(B = null == s ? 0 : s.length) && C(B) && x(V, B) && (w(s) || _(s)); + }; + }, + 49698: (s) => { + var o = RegExp( + '[\\u200d\\ud800-\\udfff\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\ufe0e\\ufe0f]' + ); + s.exports = function hasUnicode(s) { + return o.test(s); + }; + }, + 45434: (s) => { + var o = /[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/; + s.exports = function hasUnicodeWord(s) { + return o.test(s); + }; + }, + 22032: (s, o, i) => { + var u = i(81042); + s.exports = function hashClear() { + ((this.__data__ = u ? u(null) : {}), (this.size = 0)); + }; + }, + 63862: (s) => { + s.exports = function hashDelete(s) { + var o = this.has(s) && delete this.__data__[s]; + return ((this.size -= o ? 1 : 0), o); + }; + }, + 66721: (s, o, i) => { + var u = i(81042), + _ = Object.prototype.hasOwnProperty; + s.exports = function hashGet(s) { + var o = this.__data__; + if (u) { + var i = o[s]; + return '__lodash_hash_undefined__' === i ? void 0 : i; + } + return _.call(o, s) ? o[s] : void 0; + }; + }, + 12749: (s, o, i) => { + var u = i(81042), + _ = Object.prototype.hasOwnProperty; + s.exports = function hashHas(s) { + var o = this.__data__; + return u ? void 0 !== o[s] : _.call(o, s); + }; + }, + 35749: (s, o, i) => { + var u = i(81042); + s.exports = function hashSet(s, o) { + var i = this.__data__; + return ( + (this.size += this.has(s) ? 0 : 1), + (i[s] = u && void 0 === o ? '__lodash_hash_undefined__' : o), + this + ); + }; + }, + 76189: (s) => { + var o = Object.prototype.hasOwnProperty; + s.exports = function initCloneArray(s) { + var i = s.length, + u = new s.constructor(i); + return ( + i && + 'string' == typeof s[0] && + o.call(s, 'index') && + ((u.index = s.index), (u.input = s.input)), + u + ); + }; + }, + 77199: (s, o, i) => { + var u = i(49653), + _ = i(76169), + w = i(73201), + x = i(93736), + C = i(71961); + s.exports = function initCloneByTag(s, o, i) { + var j = s.constructor; + switch (o) { + case '[object ArrayBuffer]': + return u(s); + case '[object Boolean]': + case '[object Date]': + return new j(+s); + case '[object DataView]': + return _(s, i); + case '[object Float32Array]': + case '[object Float64Array]': + case '[object Int8Array]': + case '[object Int16Array]': + case '[object Int32Array]': + case '[object Uint8Array]': + case '[object Uint8ClampedArray]': + case '[object Uint16Array]': + case '[object Uint32Array]': + return C(s, i); + case '[object Map]': + case '[object Set]': + return new j(); + case '[object Number]': + case '[object String]': + return new j(s); + case '[object RegExp]': + return w(s); + case '[object Symbol]': + return x(s); + } + }; + }, + 35529: (s, o, i) => { + var u = i(39344), + _ = i(28879), + w = i(55527); + s.exports = function initCloneObject(s) { + return 'function' != typeof s.constructor || w(s) ? {} : u(_(s)); + }; + }, + 62060: (s) => { + var o = /\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/; + s.exports = function insertWrapDetails(s, i) { + var u = i.length; + if (!u) return s; + var _ = u - 1; + return ( + (i[_] = (u > 1 ? '& ' : '') + i[_]), + (i = i.join(u > 2 ? ', ' : ' ')), + s.replace(o, '{\n/* [wrapped with ' + i + '] */\n') + ); + }; + }, + 45891: (s, o, i) => { + var u = i(51873), + _ = i(72428), + w = i(56449), + x = u ? u.isConcatSpreadable : void 0; + s.exports = function isFlattenable(s) { + return w(s) || _(s) || !!(x && s && s[x]); + }; + }, + 30361: (s) => { + var o = /^(?:0|[1-9]\d*)$/; + s.exports = function isIndex(s, i) { + var u = typeof s; + return ( + !!(i = null == i ? 9007199254740991 : i) && + ('number' == u || ('symbol' != u && o.test(s))) && + s > -1 && + s % 1 == 0 && + s < i + ); + }; + }, + 36800: (s, o, i) => { + var u = i(75288), + _ = i(64894), + w = i(30361), + x = i(23805); + s.exports = function isIterateeCall(s, o, i) { + if (!x(i)) return !1; + var C = typeof o; + return ( + !!('number' == C ? _(i) && w(o, i.length) : 'string' == C && o in i) && u(i[o], s) + ); + }; + }, + 28586: (s, o, i) => { + var u = i(56449), + _ = i(44394), + w = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/, + x = /^\w*$/; + s.exports = function isKey(s, o) { + if (u(s)) return !1; + var i = typeof s; + return ( + !('number' != i && 'symbol' != i && 'boolean' != i && null != s && !_(s)) || + x.test(s) || + !w.test(s) || + (null != o && s in Object(o)) + ); + }; + }, + 74218: (s) => { + s.exports = function isKeyable(s) { + var o = typeof s; + return 'string' == o || 'number' == o || 'symbol' == o || 'boolean' == o + ? '__proto__' !== s + : null === s; + }; + }, + 85087: (s, o, i) => { + var u = i(30980), + _ = i(37381), + w = i(62284), + x = i(53758); + s.exports = function isLaziable(s) { + var o = w(s), + i = x[o]; + if ('function' != typeof i || !(o in u.prototype)) return !1; + if (s === i) return !0; + var C = _(i); + return !!C && s === C[0]; + }; + }, + 87296: (s, o, i) => { + var u, + _ = i(55481), + w = (u = /[^.]+$/.exec((_ && _.keys && _.keys.IE_PROTO) || '')) + ? 'Symbol(src)_1.' + u + : ''; + s.exports = function isMasked(s) { + return !!w && w in s; + }; + }, + 55527: (s) => { + var o = Object.prototype; + s.exports = function isPrototype(s) { + var i = s && s.constructor; + return s === (('function' == typeof i && i.prototype) || o); + }; + }, + 30756: (s, o, i) => { + var u = i(23805); + s.exports = function isStrictComparable(s) { + return s == s && !u(s); + }; + }, + 63702: (s) => { + s.exports = function listCacheClear() { + ((this.__data__ = []), (this.size = 0)); + }; + }, + 70080: (s, o, i) => { + var u = i(26025), + _ = Array.prototype.splice; + s.exports = function listCacheDelete(s) { + var o = this.__data__, + i = u(o, s); + return !(i < 0) && (i == o.length - 1 ? o.pop() : _.call(o, i, 1), --this.size, !0); + }; + }, + 24739: (s, o, i) => { + var u = i(26025); + s.exports = function listCacheGet(s) { + var o = this.__data__, + i = u(o, s); + return i < 0 ? void 0 : o[i][1]; + }; + }, + 48655: (s, o, i) => { + var u = i(26025); + s.exports = function listCacheHas(s) { + return u(this.__data__, s) > -1; + }; + }, + 31175: (s, o, i) => { + var u = i(26025); + s.exports = function listCacheSet(s, o) { + var i = this.__data__, + _ = u(i, s); + return (_ < 0 ? (++this.size, i.push([s, o])) : (i[_][1] = o), this); + }; + }, + 63040: (s, o, i) => { + var u = i(21549), + _ = i(80079), + w = i(68223); + s.exports = function mapCacheClear() { + ((this.size = 0), + (this.__data__ = { hash: new u(), map: new (w || _)(), string: new u() })); + }; + }, + 17670: (s, o, i) => { + var u = i(12651); + s.exports = function mapCacheDelete(s) { + var o = u(this, s).delete(s); + return ((this.size -= o ? 1 : 0), o); + }; + }, + 90289: (s, o, i) => { + var u = i(12651); + s.exports = function mapCacheGet(s) { + return u(this, s).get(s); + }; + }, + 4509: (s, o, i) => { + var u = i(12651); + s.exports = function mapCacheHas(s) { + return u(this, s).has(s); + }; + }, + 72949: (s, o, i) => { + var u = i(12651); + s.exports = function mapCacheSet(s, o) { + var i = u(this, s), + _ = i.size; + return (i.set(s, o), (this.size += i.size == _ ? 0 : 1), this); + }; + }, + 20317: (s) => { + s.exports = function mapToArray(s) { + var o = -1, + i = Array(s.size); + return ( + s.forEach(function (s, u) { + i[++o] = [u, s]; + }), + i + ); + }; + }, + 67197: (s) => { + s.exports = function matchesStrictComparable(s, o) { + return function (i) { + return null != i && i[s] === o && (void 0 !== o || s in Object(i)); + }; + }; + }, + 62224: (s, o, i) => { + var u = i(50104); + s.exports = function memoizeCapped(s) { + var o = u(s, function (s) { + return (500 === i.size && i.clear(), s); + }), + i = o.cache; + return o; + }; + }, + 3209: (s, o, i) => { + var u = i(91596), + _ = i(53320), + w = i(36306), + x = '__lodash_placeholder__', + C = 128, + j = Math.min; + s.exports = function mergeData(s, o) { + var i = s[1], + L = o[1], + B = i | L, + $ = B < 131, + V = + (L == C && 8 == i) || + (L == C && 256 == i && s[7].length <= o[8]) || + (384 == L && o[7].length <= o[8] && 8 == i); + if (!$ && !V) return s; + 1 & L && ((s[2] = o[2]), (B |= 1 & i ? 0 : 4)); + var U = o[3]; + if (U) { + var z = s[3]; + ((s[3] = z ? u(z, U, o[4]) : U), (s[4] = z ? w(s[3], x) : o[4])); + } + return ( + (U = o[5]) && + ((z = s[5]), (s[5] = z ? _(z, U, o[6]) : U), (s[6] = z ? w(s[5], x) : o[6])), + (U = o[7]) && (s[7] = U), + L & C && (s[8] = null == s[8] ? o[8] : j(s[8], o[8])), + null == s[9] && (s[9] = o[9]), + (s[0] = o[0]), + (s[1] = B), + s + ); + }; + }, + 48152: (s, o, i) => { + var u = i(28303), + _ = u && new u(); + s.exports = _; + }, + 81042: (s, o, i) => { + var u = i(56110)(Object, 'create'); + s.exports = u; + }, + 3650: (s, o, i) => { + var u = i(74335)(Object.keys, Object); + s.exports = u; + }, + 90181: (s) => { + s.exports = function nativeKeysIn(s) { + var o = []; + if (null != s) for (var i in Object(s)) o.push(i); + return o; + }; + }, + 86009: (s, o, i) => { + s = i.nmd(s); + var u = i(34840), + _ = o && !o.nodeType && o, + w = _ && s && !s.nodeType && s, + x = w && w.exports === _ && u.process, + C = (function () { + try { + var s = w && w.require && w.require('util').types; + return s || (x && x.binding && x.binding('util')); + } catch (s) {} + })(); + s.exports = C; + }, + 59350: (s) => { + var o = Object.prototype.toString; + s.exports = function objectToString(s) { + return o.call(s); + }; + }, + 74335: (s) => { + s.exports = function overArg(s, o) { + return function (i) { + return s(o(i)); + }; + }; + }, + 56757: (s, o, i) => { + var u = i(91033), + _ = Math.max; + s.exports = function overRest(s, o, i) { + return ( + (o = _(void 0 === o ? s.length - 1 : o, 0)), + function () { + for (var w = arguments, x = -1, C = _(w.length - o, 0), j = Array(C); ++x < C; ) + j[x] = w[o + x]; + x = -1; + for (var L = Array(o + 1); ++x < o; ) L[x] = w[x]; + return ((L[o] = i(j)), u(s, this, L)); + } + ); + }; + }, + 68969: (s, o, i) => { + var u = i(47422), + _ = i(25160); + s.exports = function parent(s, o) { + return o.length < 2 ? s : u(s, _(o, 0, -1)); + }; + }, + 84629: (s) => { + s.exports = {}; + }, + 68294: (s, o, i) => { + var u = i(23007), + _ = i(30361), + w = Math.min; + s.exports = function reorder(s, o) { + for (var i = s.length, x = w(o.length, i), C = u(s); x--; ) { + var j = o[x]; + s[x] = _(j, i) ? C[j] : void 0; + } + return s; + }; + }, + 36306: (s) => { + var o = '__lodash_placeholder__'; + s.exports = function replaceHolders(s, i) { + for (var u = -1, _ = s.length, w = 0, x = []; ++u < _; ) { + var C = s[u]; + (C !== i && C !== o) || ((s[u] = o), (x[w++] = u)); + } + return x; + }; + }, + 9325: (s, o, i) => { + var u = i(34840), + _ = 'object' == typeof self && self && self.Object === Object && self, + w = u || _ || Function('return this')(); + s.exports = w; + }, + 14974: (s) => { + s.exports = function safeGet(s, o) { + if (('constructor' !== o || 'function' != typeof s[o]) && '__proto__' != o) return s[o]; + }; + }, + 31380: (s) => { + s.exports = function setCacheAdd(s) { + return (this.__data__.set(s, '__lodash_hash_undefined__'), this); + }; + }, + 51459: (s) => { + s.exports = function setCacheHas(s) { + return this.__data__.has(s); + }; + }, + 54641: (s, o, i) => { + var u = i(68882), + _ = i(51811)(u); + s.exports = _; + }, + 84247: (s) => { + s.exports = function setToArray(s) { + var o = -1, + i = Array(s.size); + return ( + s.forEach(function (s) { + i[++o] = s; + }), + i + ); + }; + }, + 32865: (s, o, i) => { + var u = i(19570), + _ = i(51811)(u); + s.exports = _; + }, + 70981: (s, o, i) => { + var u = i(75251), + _ = i(62060), + w = i(32865), + x = i(75948); + s.exports = function setWrapToString(s, o, i) { + var C = o + ''; + return w(s, _(C, x(u(C), i))); + }; + }, + 51811: (s) => { + var o = Date.now; + s.exports = function shortOut(s) { + var i = 0, + u = 0; + return function () { + var _ = o(), + w = 16 - (_ - u); + if (((u = _), w > 0)) { + if (++i >= 800) return arguments[0]; + } else i = 0; + return s.apply(void 0, arguments); + }; + }; + }, + 51420: (s, o, i) => { + var u = i(80079); + s.exports = function stackClear() { + ((this.__data__ = new u()), (this.size = 0)); + }; + }, + 90938: (s) => { + s.exports = function stackDelete(s) { + var o = this.__data__, + i = o.delete(s); + return ((this.size = o.size), i); + }; + }, + 63605: (s) => { + s.exports = function stackGet(s) { + return this.__data__.get(s); + }; + }, + 29817: (s) => { + s.exports = function stackHas(s) { + return this.__data__.has(s); + }; + }, + 80945: (s, o, i) => { + var u = i(80079), + _ = i(68223), + w = i(53661); + s.exports = function stackSet(s, o) { + var i = this.__data__; + if (i instanceof u) { + var x = i.__data__; + if (!_ || x.length < 199) return (x.push([s, o]), (this.size = ++i.size), this); + i = this.__data__ = new w(x); + } + return (i.set(s, o), (this.size = i.size), this); + }; + }, + 76959: (s) => { + s.exports = function strictIndexOf(s, o, i) { + for (var u = i - 1, _ = s.length; ++u < _; ) if (s[u] === o) return u; + return -1; + }; + }, + 63912: (s, o, i) => { + var u = i(61074), + _ = i(49698), + w = i(42054); + s.exports = function stringToArray(s) { + return _(s) ? w(s) : u(s); + }; + }, + 61802: (s, o, i) => { + var u = i(62224), + _ = + /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g, + w = /\\(\\)?/g, + x = u(function (s) { + var o = []; + return ( + 46 === s.charCodeAt(0) && o.push(''), + s.replace(_, function (s, i, u, _) { + o.push(u ? _.replace(w, '$1') : i || s); + }), + o + ); + }); + s.exports = x; + }, + 77797: (s, o, i) => { + var u = i(44394); + s.exports = function toKey(s) { + if ('string' == typeof s || u(s)) return s; + var o = s + ''; + return '0' == o && 1 / s == -1 / 0 ? '-0' : o; + }; + }, + 47473: (s) => { + var o = Function.prototype.toString; + s.exports = function toSource(s) { + if (null != s) { + try { + return o.call(s); + } catch (s) {} + try { + return s + ''; + } catch (s) {} + } + return ''; + }; + }, + 31800: (s) => { + var o = /\s/; + s.exports = function trimmedEndIndex(s) { + for (var i = s.length; i-- && o.test(s.charAt(i)); ); + return i; + }; + }, + 42054: (s) => { + var o = '\\ud800-\\udfff', + i = '[' + o + ']', + u = '[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]', + _ = '\\ud83c[\\udffb-\\udfff]', + w = '[^' + o + ']', + x = '(?:\\ud83c[\\udde6-\\uddff]){2}', + C = '[\\ud800-\\udbff][\\udc00-\\udfff]', + j = '(?:' + u + '|' + _ + ')' + '?', + L = '[\\ufe0e\\ufe0f]?', + B = L + j + ('(?:\\u200d(?:' + [w, x, C].join('|') + ')' + L + j + ')*'), + $ = '(?:' + [w + u + '?', u, x, C, i].join('|') + ')', + V = RegExp(_ + '(?=' + _ + ')|' + $ + B, 'g'); + s.exports = function unicodeToArray(s) { + return s.match(V) || []; + }; + }, + 22225: (s) => { + var o = '\\ud800-\\udfff', + i = '\\u2700-\\u27bf', + u = 'a-z\\xdf-\\xf6\\xf8-\\xff', + _ = 'A-Z\\xc0-\\xd6\\xd8-\\xde', + w = + '\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000', + x = '[' + w + ']', + C = '\\d+', + j = '[' + i + ']', + L = '[' + u + ']', + B = '[^' + o + w + C + i + u + _ + ']', + $ = '(?:\\ud83c[\\udde6-\\uddff]){2}', + V = '[\\ud800-\\udbff][\\udc00-\\udfff]', + U = '[' + _ + ']', + z = '(?:' + L + '|' + B + ')', + Y = '(?:' + U + '|' + B + ')', + Z = "(?:['’](?:d|ll|m|re|s|t|ve))?", + ee = "(?:['’](?:D|LL|M|RE|S|T|VE))?", + ie = '(?:[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]|\\ud83c[\\udffb-\\udfff])?', + ae = '[\\ufe0e\\ufe0f]?', + le = + ae + ie + ('(?:\\u200d(?:' + ['[^' + o + ']', $, V].join('|') + ')' + ae + ie + ')*'), + ce = '(?:' + [j, $, V].join('|') + ')' + le, + pe = RegExp( + [ + U + '?' + L + '+' + Z + '(?=' + [x, U, '$'].join('|') + ')', + Y + '+' + ee + '(?=' + [x, U + z, '$'].join('|') + ')', + U + '?' + z + '+' + Z, + U + '+' + ee, + '\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])', + '\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])', + C, + ce + ].join('|'), + 'g' + ); + s.exports = function unicodeWords(s) { + return s.match(pe) || []; + }; + }, + 75948: (s, o, i) => { + var u = i(83729), + _ = i(15325), + w = [ + ['ary', 128], + ['bind', 1], + ['bindKey', 2], + ['curry', 8], + ['curryRight', 16], + ['flip', 512], + ['partial', 32], + ['partialRight', 64], + ['rearg', 256] + ]; + s.exports = function updateWrapDetails(s, o) { + return ( + u(w, function (i) { + var u = '_.' + i[0]; + o & i[1] && !_(s, u) && s.push(u); + }), + s.sort() + ); + }; + }, + 80257: (s, o, i) => { + var u = i(30980), + _ = i(56017), + w = i(23007); + s.exports = function wrapperClone(s) { + if (s instanceof u) return s.clone(); + var o = new _(s.__wrapped__, s.__chain__); + return ( + (o.__actions__ = w(s.__actions__)), + (o.__index__ = s.__index__), + (o.__values__ = s.__values__), + o + ); + }; + }, + 64626: (s, o, i) => { + var u = i(66977); + s.exports = function ary(s, o, i) { + return ( + (o = i ? void 0 : o), + (o = s && null == o ? s.length : o), + u(s, 128, void 0, void 0, void 0, void 0, o) + ); + }; + }, + 84058: (s, o, i) => { + var u = i(14792), + _ = i(45539)(function (s, o, i) { + return ((o = o.toLowerCase()), s + (i ? u(o) : o)); + }); + s.exports = _; + }, + 14792: (s, o, i) => { + var u = i(13222), + _ = i(55808); + s.exports = function capitalize(s) { + return _(u(s).toLowerCase()); + }; + }, + 32629: (s, o, i) => { + var u = i(9999); + s.exports = function clone(s) { + return u(s, 4); + }; + }, + 37334: (s) => { + s.exports = function constant(s) { + return function () { + return s; + }; + }; + }, + 49747: (s, o, i) => { + var u = i(66977); + function curry(s, o, i) { + var _ = u(s, 8, void 0, void 0, void 0, void 0, void 0, (o = i ? void 0 : o)); + return ((_.placeholder = curry.placeholder), _); + } + ((curry.placeholder = {}), (s.exports = curry)); + }, + 38221: (s, o, i) => { + var u = i(23805), + _ = i(10124), + w = i(99374), + x = Math.max, + C = Math.min; + s.exports = function debounce(s, o, i) { + var j, + L, + B, + $, + V, + U, + z = 0, + Y = !1, + Z = !1, + ee = !0; + if ('function' != typeof s) throw new TypeError('Expected a function'); + function invokeFunc(o) { + var i = j, + u = L; + return ((j = L = void 0), (z = o), ($ = s.apply(u, i))); + } + function shouldInvoke(s) { + var i = s - U; + return void 0 === U || i >= o || i < 0 || (Z && s - z >= B); + } + function timerExpired() { + var s = _(); + if (shouldInvoke(s)) return trailingEdge(s); + V = setTimeout( + timerExpired, + (function remainingWait(s) { + var i = o - (s - U); + return Z ? C(i, B - (s - z)) : i; + })(s) + ); + } + function trailingEdge(s) { + return ((V = void 0), ee && j ? invokeFunc(s) : ((j = L = void 0), $)); + } + function debounced() { + var s = _(), + i = shouldInvoke(s); + if (((j = arguments), (L = this), (U = s), i)) { + if (void 0 === V) + return (function leadingEdge(s) { + return ((z = s), (V = setTimeout(timerExpired, o)), Y ? invokeFunc(s) : $); + })(U); + if (Z) return (clearTimeout(V), (V = setTimeout(timerExpired, o)), invokeFunc(U)); + } + return (void 0 === V && (V = setTimeout(timerExpired, o)), $); + } + return ( + (o = w(o) || 0), + u(i) && + ((Y = !!i.leading), + (B = (Z = 'maxWait' in i) ? x(w(i.maxWait) || 0, o) : B), + (ee = 'trailing' in i ? !!i.trailing : ee)), + (debounced.cancel = function cancel() { + (void 0 !== V && clearTimeout(V), (z = 0), (j = U = L = V = void 0)); + }), + (debounced.flush = function flush() { + return void 0 === V ? $ : trailingEdge(_()); + }), + debounced + ); + }; + }, + 50828: (s, o, i) => { + var u = i(24647), + _ = i(13222), + w = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g, + x = RegExp('[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]', 'g'); + s.exports = function deburr(s) { + return (s = _(s)) && s.replace(w, u).replace(x, ''); + }; + }, + 75288: (s) => { + s.exports = function eq(s, o) { + return s === o || (s != s && o != o); + }; + }, + 60680: (s, o, i) => { + var u = i(13222), + _ = /[\\^$.*+?()[\]{}|]/g, + w = RegExp(_.source); + s.exports = function escapeRegExp(s) { + return (s = u(s)) && w.test(s) ? s.replace(_, '\\$&') : s; + }; + }, + 7309: (s, o, i) => { + var u = i(62006)(i(24713)); + s.exports = u; + }, + 24713: (s, o, i) => { + var u = i(2523), + _ = i(15389), + w = i(61489), + x = Math.max; + s.exports = function findIndex(s, o, i) { + var C = null == s ? 0 : s.length; + if (!C) return -1; + var j = null == i ? 0 : w(i); + return (j < 0 && (j = x(C + j, 0)), u(s, _(o, 3), j)); + }; + }, + 35970: (s, o, i) => { + var u = i(83120); + s.exports = function flatten(s) { + return (null == s ? 0 : s.length) ? u(s, 1) : []; + }; + }, + 73424: (s, o, i) => { + var u = i(16962), + _ = i(2874), + w = Array.prototype.push; + function baseAry(s, o) { + return 2 == o + ? function (o, i) { + return s(o, i); + } + : function (o) { + return s(o); + }; + } + function cloneArray(s) { + for (var o = s ? s.length : 0, i = Array(o); o--; ) i[o] = s[o]; + return i; + } + function wrapImmutable(s, o) { + return function () { + var i = arguments.length; + if (i) { + for (var u = Array(i); i--; ) u[i] = arguments[i]; + var _ = (u[0] = o.apply(void 0, u)); + return (s.apply(void 0, u), _); + } + }; + } + s.exports = function baseConvert(s, o, i, x) { + var C = 'function' == typeof o, + j = o === Object(o); + if ((j && ((x = i), (i = o), (o = void 0)), null == i)) throw new TypeError(); + x || (x = {}); + var L = !('cap' in x) || x.cap, + B = !('curry' in x) || x.curry, + $ = !('fixed' in x) || x.fixed, + V = !('immutable' in x) || x.immutable, + U = !('rearg' in x) || x.rearg, + z = C ? i : _, + Y = 'curry' in x && x.curry, + Z = 'fixed' in x && x.fixed, + ee = 'rearg' in x && x.rearg, + ie = C ? i.runInContext() : void 0, + ae = C + ? i + : { + ary: s.ary, + assign: s.assign, + clone: s.clone, + curry: s.curry, + forEach: s.forEach, + isArray: s.isArray, + isError: s.isError, + isFunction: s.isFunction, + isWeakMap: s.isWeakMap, + iteratee: s.iteratee, + keys: s.keys, + rearg: s.rearg, + toInteger: s.toInteger, + toPath: s.toPath + }, + le = ae.ary, + ce = ae.assign, + pe = ae.clone, + de = ae.curry, + fe = ae.forEach, + ye = ae.isArray, + be = ae.isError, + _e = ae.isFunction, + we = ae.isWeakMap, + Se = ae.keys, + xe = ae.rearg, + Pe = ae.toInteger, + Te = ae.toPath, + Re = Se(u.aryMethod), + qe = { + castArray: function (s) { + return function () { + var o = arguments[0]; + return ye(o) ? s(cloneArray(o)) : s.apply(void 0, arguments); + }; + }, + iteratee: function (s) { + return function () { + var o = arguments[1], + i = s(arguments[0], o), + u = i.length; + return L && 'number' == typeof o + ? ((o = o > 2 ? o - 2 : 1), u && u <= o ? i : baseAry(i, o)) + : i; + }; + }, + mixin: function (s) { + return function (o) { + var i = this; + if (!_e(i)) return s(i, Object(o)); + var u = []; + return ( + fe(Se(o), function (s) { + _e(o[s]) && u.push([s, i.prototype[s]]); + }), + s(i, Object(o)), + fe(u, function (s) { + var o = s[1]; + _e(o) ? (i.prototype[s[0]] = o) : delete i.prototype[s[0]]; + }), + i + ); + }; + }, + nthArg: function (s) { + return function (o) { + var i = o < 0 ? 1 : Pe(o) + 1; + return de(s(o), i); + }; + }, + rearg: function (s) { + return function (o, i) { + var u = i ? i.length : 0; + return de(s(o, i), u); + }; + }, + runInContext: function (o) { + return function (i) { + return baseConvert(s, o(i), x); + }; + } + }; + function castCap(s, o) { + if (L) { + var i = u.iterateeRearg[s]; + if (i) + return (function iterateeRearg(s, o) { + return overArg(s, function (s) { + var i = o.length; + return (function baseArity(s, o) { + return 2 == o + ? function (o, i) { + return s.apply(void 0, arguments); + } + : function (o) { + return s.apply(void 0, arguments); + }; + })(xe(baseAry(s, i), o), i); + }); + })(o, i); + var _ = !C && u.iterateeAry[s]; + if (_) + return (function iterateeAry(s, o) { + return overArg(s, function (s) { + return 'function' == typeof s ? baseAry(s, o) : s; + }); + })(o, _); + } + return o; + } + function castFixed(s, o, i) { + if ($ && (Z || !u.skipFixed[s])) { + var _ = u.methodSpread[s], + x = _ && _.start; + return void 0 === x + ? le(o, i) + : (function flatSpread(s, o) { + return function () { + for (var i = arguments.length, u = i - 1, _ = Array(i); i--; ) + _[i] = arguments[i]; + var x = _[o], + C = _.slice(0, o); + return ( + x && w.apply(C, x), + o != u && w.apply(C, _.slice(o + 1)), + s.apply(this, C) + ); + }; + })(o, x); + } + return o; + } + function castRearg(s, o, i) { + return U && i > 1 && (ee || !u.skipRearg[s]) + ? xe(o, u.methodRearg[s] || u.aryRearg[i]) + : o; + } + function cloneByPath(s, o) { + for ( + var i = -1, u = (o = Te(o)).length, _ = u - 1, w = pe(Object(s)), x = w; + null != x && ++i < u; + ) { + var C = o[i], + j = x[C]; + (null == j || _e(j) || be(j) || we(j) || (x[C] = pe(i == _ ? j : Object(j))), + (x = x[C])); + } + return w; + } + function createConverter(s, o) { + var i = u.aliasToReal[s] || s, + _ = u.remap[i] || i, + w = x; + return function (s) { + var u = C ? ie : ae, + x = C ? ie[_] : o, + j = ce(ce({}, w), s); + return baseConvert(u, i, x, j); + }; + } + function overArg(s, o) { + return function () { + var i = arguments.length; + if (!i) return s(); + for (var u = Array(i); i--; ) u[i] = arguments[i]; + var _ = U ? 0 : i - 1; + return ((u[_] = o(u[_])), s.apply(void 0, u)); + }; + } + function wrap(s, o, i) { + var _, + w = u.aliasToReal[s] || s, + x = o, + C = qe[w]; + return ( + C + ? (x = C(o)) + : V && + (u.mutate.array[w] + ? (x = wrapImmutable(o, cloneArray)) + : u.mutate.object[w] + ? (x = wrapImmutable( + o, + (function createCloner(s) { + return function (o) { + return s({}, o); + }; + })(o) + )) + : u.mutate.set[w] && (x = wrapImmutable(o, cloneByPath))), + fe(Re, function (s) { + return ( + fe(u.aryMethod[s], function (o) { + if (w == o) { + var i = u.methodSpread[w], + C = i && i.afterRearg; + return ( + (_ = C + ? castFixed(w, castRearg(w, x, s), s) + : castRearg(w, castFixed(w, x, s), s)), + (_ = (function castCurry(s, o, i) { + return Y || (B && i > 1) ? de(o, i) : o; + })(0, (_ = castCap(w, _)), s)), + !1 + ); + } + }), + !_ + ); + }), + _ || (_ = x), + _ == o && + (_ = Y + ? de(_, 1) + : function () { + return o.apply(this, arguments); + }), + (_.convert = createConverter(w, o)), + (_.placeholder = o.placeholder = i), + _ + ); + } + if (!j) return wrap(o, i, z); + var $e = i, + ze = []; + return ( + fe(Re, function (s) { + fe(u.aryMethod[s], function (s) { + var o = $e[u.remap[s] || s]; + o && ze.push([s, wrap(s, o, $e)]); + }); + }), + fe(Se($e), function (s) { + var o = $e[s]; + if ('function' == typeof o) { + for (var i = ze.length; i--; ) if (ze[i][0] == s) return; + ((o.convert = createConverter(s, o)), ze.push([s, o])); + } + }), + fe(ze, function (s) { + $e[s[0]] = s[1]; + }), + ($e.convert = function convertLib(s) { + return $e.runInContext.convert(s)(void 0); + }), + ($e.placeholder = $e), + fe(Se($e), function (s) { + fe(u.realToAlias[s] || [], function (o) { + $e[o] = $e[s]; + }); + }), + $e + ); + }; + }, + 16962: (s, o) => { + ((o.aliasToReal = { + each: 'forEach', + eachRight: 'forEachRight', + entries: 'toPairs', + entriesIn: 'toPairsIn', + extend: 'assignIn', + extendAll: 'assignInAll', + extendAllWith: 'assignInAllWith', + extendWith: 'assignInWith', + first: 'head', + conforms: 'conformsTo', + matches: 'isMatch', + property: 'get', + __: 'placeholder', + F: 'stubFalse', + T: 'stubTrue', + all: 'every', + allPass: 'overEvery', + always: 'constant', + any: 'some', + anyPass: 'overSome', + apply: 'spread', + assoc: 'set', + assocPath: 'set', + complement: 'negate', + compose: 'flowRight', + contains: 'includes', + dissoc: 'unset', + dissocPath: 'unset', + dropLast: 'dropRight', + dropLastWhile: 'dropRightWhile', + equals: 'isEqual', + identical: 'eq', + indexBy: 'keyBy', + init: 'initial', + invertObj: 'invert', + juxt: 'over', + omitAll: 'omit', + nAry: 'ary', + path: 'get', + pathEq: 'matchesProperty', + pathOr: 'getOr', + paths: 'at', + pickAll: 'pick', + pipe: 'flow', + pluck: 'map', + prop: 'get', + propEq: 'matchesProperty', + propOr: 'getOr', + props: 'at', + symmetricDifference: 'xor', + symmetricDifferenceBy: 'xorBy', + symmetricDifferenceWith: 'xorWith', + takeLast: 'takeRight', + takeLastWhile: 'takeRightWhile', + unapply: 'rest', + unnest: 'flatten', + useWith: 'overArgs', + where: 'conformsTo', + whereEq: 'isMatch', + zipObj: 'zipObject' + }), + (o.aryMethod = { + 1: [ + 'assignAll', + 'assignInAll', + 'attempt', + 'castArray', + 'ceil', + 'create', + 'curry', + 'curryRight', + 'defaultsAll', + 'defaultsDeepAll', + 'floor', + 'flow', + 'flowRight', + 'fromPairs', + 'invert', + 'iteratee', + 'memoize', + 'method', + 'mergeAll', + 'methodOf', + 'mixin', + 'nthArg', + 'over', + 'overEvery', + 'overSome', + 'rest', + 'reverse', + 'round', + 'runInContext', + 'spread', + 'template', + 'trim', + 'trimEnd', + 'trimStart', + 'uniqueId', + 'words', + 'zipAll' + ], + 2: [ + 'add', + 'after', + 'ary', + 'assign', + 'assignAllWith', + 'assignIn', + 'assignInAllWith', + 'at', + 'before', + 'bind', + 'bindAll', + 'bindKey', + 'chunk', + 'cloneDeepWith', + 'cloneWith', + 'concat', + 'conformsTo', + 'countBy', + 'curryN', + 'curryRightN', + 'debounce', + 'defaults', + 'defaultsDeep', + 'defaultTo', + 'delay', + 'difference', + 'divide', + 'drop', + 'dropRight', + 'dropRightWhile', + 'dropWhile', + 'endsWith', + 'eq', + 'every', + 'filter', + 'find', + 'findIndex', + 'findKey', + 'findLast', + 'findLastIndex', + 'findLastKey', + 'flatMap', + 'flatMapDeep', + 'flattenDepth', + 'forEach', + 'forEachRight', + 'forIn', + 'forInRight', + 'forOwn', + 'forOwnRight', + 'get', + 'groupBy', + 'gt', + 'gte', + 'has', + 'hasIn', + 'includes', + 'indexOf', + 'intersection', + 'invertBy', + 'invoke', + 'invokeMap', + 'isEqual', + 'isMatch', + 'join', + 'keyBy', + 'lastIndexOf', + 'lt', + 'lte', + 'map', + 'mapKeys', + 'mapValues', + 'matchesProperty', + 'maxBy', + 'meanBy', + 'merge', + 'mergeAllWith', + 'minBy', + 'multiply', + 'nth', + 'omit', + 'omitBy', + 'overArgs', + 'pad', + 'padEnd', + 'padStart', + 'parseInt', + 'partial', + 'partialRight', + 'partition', + 'pick', + 'pickBy', + 'propertyOf', + 'pull', + 'pullAll', + 'pullAt', + 'random', + 'range', + 'rangeRight', + 'rearg', + 'reject', + 'remove', + 'repeat', + 'restFrom', + 'result', + 'sampleSize', + 'some', + 'sortBy', + 'sortedIndex', + 'sortedIndexOf', + 'sortedLastIndex', + 'sortedLastIndexOf', + 'sortedUniqBy', + 'split', + 'spreadFrom', + 'startsWith', + 'subtract', + 'sumBy', + 'take', + 'takeRight', + 'takeRightWhile', + 'takeWhile', + 'tap', + 'throttle', + 'thru', + 'times', + 'trimChars', + 'trimCharsEnd', + 'trimCharsStart', + 'truncate', + 'union', + 'uniqBy', + 'uniqWith', + 'unset', + 'unzipWith', + 'without', + 'wrap', + 'xor', + 'zip', + 'zipObject', + 'zipObjectDeep' + ], + 3: [ + 'assignInWith', + 'assignWith', + 'clamp', + 'differenceBy', + 'differenceWith', + 'findFrom', + 'findIndexFrom', + 'findLastFrom', + 'findLastIndexFrom', + 'getOr', + 'includesFrom', + 'indexOfFrom', + 'inRange', + 'intersectionBy', + 'intersectionWith', + 'invokeArgs', + 'invokeArgsMap', + 'isEqualWith', + 'isMatchWith', + 'flatMapDepth', + 'lastIndexOfFrom', + 'mergeWith', + 'orderBy', + 'padChars', + 'padCharsEnd', + 'padCharsStart', + 'pullAllBy', + 'pullAllWith', + 'rangeStep', + 'rangeStepRight', + 'reduce', + 'reduceRight', + 'replace', + 'set', + 'slice', + 'sortedIndexBy', + 'sortedLastIndexBy', + 'transform', + 'unionBy', + 'unionWith', + 'update', + 'xorBy', + 'xorWith', + 'zipWith' + ], + 4: ['fill', 'setWith', 'updateWith'] + }), + (o.aryRearg = { 2: [1, 0], 3: [2, 0, 1], 4: [3, 2, 0, 1] }), + (o.iterateeAry = { + dropRightWhile: 1, + dropWhile: 1, + every: 1, + filter: 1, + find: 1, + findFrom: 1, + findIndex: 1, + findIndexFrom: 1, + findKey: 1, + findLast: 1, + findLastFrom: 1, + findLastIndex: 1, + findLastIndexFrom: 1, + findLastKey: 1, + flatMap: 1, + flatMapDeep: 1, + flatMapDepth: 1, + forEach: 1, + forEachRight: 1, + forIn: 1, + forInRight: 1, + forOwn: 1, + forOwnRight: 1, + map: 1, + mapKeys: 1, + mapValues: 1, + partition: 1, + reduce: 2, + reduceRight: 2, + reject: 1, + remove: 1, + some: 1, + takeRightWhile: 1, + takeWhile: 1, + times: 1, + transform: 2 + }), + (o.iterateeRearg = { mapKeys: [1], reduceRight: [1, 0] }), + (o.methodRearg = { + assignInAllWith: [1, 0], + assignInWith: [1, 2, 0], + assignAllWith: [1, 0], + assignWith: [1, 2, 0], + differenceBy: [1, 2, 0], + differenceWith: [1, 2, 0], + getOr: [2, 1, 0], + intersectionBy: [1, 2, 0], + intersectionWith: [1, 2, 0], + isEqualWith: [1, 2, 0], + isMatchWith: [2, 1, 0], + mergeAllWith: [1, 0], + mergeWith: [1, 2, 0], + padChars: [2, 1, 0], + padCharsEnd: [2, 1, 0], + padCharsStart: [2, 1, 0], + pullAllBy: [2, 1, 0], + pullAllWith: [2, 1, 0], + rangeStep: [1, 2, 0], + rangeStepRight: [1, 2, 0], + setWith: [3, 1, 2, 0], + sortedIndexBy: [2, 1, 0], + sortedLastIndexBy: [2, 1, 0], + unionBy: [1, 2, 0], + unionWith: [1, 2, 0], + updateWith: [3, 1, 2, 0], + xorBy: [1, 2, 0], + xorWith: [1, 2, 0], + zipWith: [1, 2, 0] + }), + (o.methodSpread = { + assignAll: { start: 0 }, + assignAllWith: { start: 0 }, + assignInAll: { start: 0 }, + assignInAllWith: { start: 0 }, + defaultsAll: { start: 0 }, + defaultsDeepAll: { start: 0 }, + invokeArgs: { start: 2 }, + invokeArgsMap: { start: 2 }, + mergeAll: { start: 0 }, + mergeAllWith: { start: 0 }, + partial: { start: 1 }, + partialRight: { start: 1 }, + without: { start: 1 }, + zipAll: { start: 0 } + }), + (o.mutate = { + array: { + fill: !0, + pull: !0, + pullAll: !0, + pullAllBy: !0, + pullAllWith: !0, + pullAt: !0, + remove: !0, + reverse: !0 + }, + object: { + assign: !0, + assignAll: !0, + assignAllWith: !0, + assignIn: !0, + assignInAll: !0, + assignInAllWith: !0, + assignInWith: !0, + assignWith: !0, + defaults: !0, + defaultsAll: !0, + defaultsDeep: !0, + defaultsDeepAll: !0, + merge: !0, + mergeAll: !0, + mergeAllWith: !0, + mergeWith: !0 + }, + set: { set: !0, setWith: !0, unset: !0, update: !0, updateWith: !0 } + }), + (o.realToAlias = (function () { + var s = Object.prototype.hasOwnProperty, + i = o.aliasToReal, + u = {}; + for (var _ in i) { + var w = i[_]; + s.call(u, w) ? u[w].push(_) : (u[w] = [_]); + } + return u; + })()), + (o.remap = { + assignAll: 'assign', + assignAllWith: 'assignWith', + assignInAll: 'assignIn', + assignInAllWith: 'assignInWith', + curryN: 'curry', + curryRightN: 'curryRight', + defaultsAll: 'defaults', + defaultsDeepAll: 'defaultsDeep', + findFrom: 'find', + findIndexFrom: 'findIndex', + findLastFrom: 'findLast', + findLastIndexFrom: 'findLastIndex', + getOr: 'get', + includesFrom: 'includes', + indexOfFrom: 'indexOf', + invokeArgs: 'invoke', + invokeArgsMap: 'invokeMap', + lastIndexOfFrom: 'lastIndexOf', + mergeAll: 'merge', + mergeAllWith: 'mergeWith', + padChars: 'pad', + padCharsEnd: 'padEnd', + padCharsStart: 'padStart', + propertyOf: 'get', + rangeStep: 'range', + rangeStepRight: 'rangeRight', + restFrom: 'rest', + spreadFrom: 'spread', + trimChars: 'trim', + trimCharsEnd: 'trimEnd', + trimCharsStart: 'trimStart', + zipAll: 'zip' + }), + (o.skipFixed = { + castArray: !0, + flow: !0, + flowRight: !0, + iteratee: !0, + mixin: !0, + rearg: !0, + runInContext: !0 + }), + (o.skipRearg = { + add: !0, + assign: !0, + assignIn: !0, + bind: !0, + bindKey: !0, + concat: !0, + difference: !0, + divide: !0, + eq: !0, + gt: !0, + gte: !0, + isEqual: !0, + lt: !0, + lte: !0, + matchesProperty: !0, + merge: !0, + multiply: !0, + overArgs: !0, + partial: !0, + partialRight: !0, + propertyOf: !0, + random: !0, + range: !0, + rangeRight: !0, + subtract: !0, + zip: !0, + zipObject: !0, + zipObjectDeep: !0 + })); + }, + 47934: (s, o, i) => { + s.exports = { + ary: i(64626), + assign: i(74733), + clone: i(32629), + curry: i(49747), + forEach: i(83729), + isArray: i(56449), + isError: i(23546), + isFunction: i(1882), + isWeakMap: i(47886), + iteratee: i(33855), + keys: i(88984), + rearg: i(84195), + toInteger: i(61489), + toPath: i(42072) + }; + }, + 56367: (s, o, i) => { + s.exports = i(77731); + }, + 79920: (s, o, i) => { + var u = i(73424), + _ = i(47934); + s.exports = function convert(s, o, i) { + return u(_, s, o, i); + }; + }, + 2874: (s) => { + s.exports = {}; + }, + 77731: (s, o, i) => { + var u = i(79920)('set', i(63560)); + ((u.placeholder = i(2874)), (s.exports = u)); + }, + 58156: (s, o, i) => { + var u = i(47422); + s.exports = function get(s, o, i) { + var _ = null == s ? void 0 : u(s, o); + return void 0 === _ ? i : _; + }; + }, + 61448: (s, o, i) => { + var u = i(20426), + _ = i(49326); + s.exports = function has(s, o) { + return null != s && _(s, o, u); + }; + }, + 80631: (s, o, i) => { + var u = i(28077), + _ = i(49326); + s.exports = function hasIn(s, o) { + return null != s && _(s, o, u); + }; + }, + 83488: (s) => { + s.exports = function identity(s) { + return s; + }; + }, + 72428: (s, o, i) => { + var u = i(27534), + _ = i(40346), + w = Object.prototype, + x = w.hasOwnProperty, + C = w.propertyIsEnumerable, + j = u( + (function () { + return arguments; + })() + ) + ? u + : function (s) { + return _(s) && x.call(s, 'callee') && !C.call(s, 'callee'); + }; + s.exports = j; + }, + 56449: (s) => { + var o = Array.isArray; + s.exports = o; + }, + 64894: (s, o, i) => { + var u = i(1882), + _ = i(30294); + s.exports = function isArrayLike(s) { + return null != s && _(s.length) && !u(s); + }; + }, + 83693: (s, o, i) => { + var u = i(64894), + _ = i(40346); + s.exports = function isArrayLikeObject(s) { + return _(s) && u(s); + }; + }, + 53812: (s, o, i) => { + var u = i(72552), + _ = i(40346); + s.exports = function isBoolean(s) { + return !0 === s || !1 === s || (_(s) && '[object Boolean]' == u(s)); + }; + }, + 3656: (s, o, i) => { + s = i.nmd(s); + var u = i(9325), + _ = i(89935), + w = o && !o.nodeType && o, + x = w && s && !s.nodeType && s, + C = x && x.exports === w ? u.Buffer : void 0, + j = (C ? C.isBuffer : void 0) || _; + s.exports = j; + }, + 62193: (s, o, i) => { + var u = i(88984), + _ = i(5861), + w = i(72428), + x = i(56449), + C = i(64894), + j = i(3656), + L = i(55527), + B = i(37167), + $ = Object.prototype.hasOwnProperty; + s.exports = function isEmpty(s) { + if (null == s) return !0; + if ( + C(s) && + (x(s) || + 'string' == typeof s || + 'function' == typeof s.splice || + j(s) || + B(s) || + w(s)) + ) + return !s.length; + var o = _(s); + if ('[object Map]' == o || '[object Set]' == o) return !s.size; + if (L(s)) return !u(s).length; + for (var i in s) if ($.call(s, i)) return !1; + return !0; + }; + }, + 2404: (s, o, i) => { + var u = i(60270); + s.exports = function isEqual(s, o) { + return u(s, o); + }; + }, + 23546: (s, o, i) => { + var u = i(72552), + _ = i(40346), + w = i(11331); + s.exports = function isError(s) { + if (!_(s)) return !1; + var o = u(s); + return ( + '[object Error]' == o || + '[object DOMException]' == o || + ('string' == typeof s.message && 'string' == typeof s.name && !w(s)) + ); + }; + }, + 1882: (s, o, i) => { + var u = i(72552), + _ = i(23805); + s.exports = function isFunction(s) { + if (!_(s)) return !1; + var o = u(s); + return ( + '[object Function]' == o || + '[object GeneratorFunction]' == o || + '[object AsyncFunction]' == o || + '[object Proxy]' == o + ); + }; + }, + 30294: (s) => { + s.exports = function isLength(s) { + return 'number' == typeof s && s > -1 && s % 1 == 0 && s <= 9007199254740991; + }; + }, + 87730: (s, o, i) => { + var u = i(29172), + _ = i(27301), + w = i(86009), + x = w && w.isMap, + C = x ? _(x) : u; + s.exports = C; + }, + 5187: (s) => { + s.exports = function isNull(s) { + return null === s; + }; + }, + 98023: (s, o, i) => { + var u = i(72552), + _ = i(40346); + s.exports = function isNumber(s) { + return 'number' == typeof s || (_(s) && '[object Number]' == u(s)); + }; + }, + 23805: (s) => { + s.exports = function isObject(s) { + var o = typeof s; + return null != s && ('object' == o || 'function' == o); + }; + }, + 40346: (s) => { + s.exports = function isObjectLike(s) { + return null != s && 'object' == typeof s; + }; + }, + 11331: (s, o, i) => { + var u = i(72552), + _ = i(28879), + w = i(40346), + x = Function.prototype, + C = Object.prototype, + j = x.toString, + L = C.hasOwnProperty, + B = j.call(Object); + s.exports = function isPlainObject(s) { + if (!w(s) || '[object Object]' != u(s)) return !1; + var o = _(s); + if (null === o) return !0; + var i = L.call(o, 'constructor') && o.constructor; + return 'function' == typeof i && i instanceof i && j.call(i) == B; + }; + }, + 38440: (s, o, i) => { + var u = i(16038), + _ = i(27301), + w = i(86009), + x = w && w.isSet, + C = x ? _(x) : u; + s.exports = C; + }, + 85015: (s, o, i) => { + var u = i(72552), + _ = i(56449), + w = i(40346); + s.exports = function isString(s) { + return 'string' == typeof s || (!_(s) && w(s) && '[object String]' == u(s)); + }; + }, + 44394: (s, o, i) => { + var u = i(72552), + _ = i(40346); + s.exports = function isSymbol(s) { + return 'symbol' == typeof s || (_(s) && '[object Symbol]' == u(s)); + }; + }, + 37167: (s, o, i) => { + var u = i(4901), + _ = i(27301), + w = i(86009), + x = w && w.isTypedArray, + C = x ? _(x) : u; + s.exports = C; + }, + 47886: (s, o, i) => { + var u = i(5861), + _ = i(40346); + s.exports = function isWeakMap(s) { + return _(s) && '[object WeakMap]' == u(s); + }; + }, + 33855: (s, o, i) => { + var u = i(9999), + _ = i(15389); + s.exports = function iteratee(s) { + return _('function' == typeof s ? s : u(s, 1)); + }; + }, + 95950: (s, o, i) => { + var u = i(70695), + _ = i(88984), + w = i(64894); + s.exports = function keys(s) { + return w(s) ? u(s) : _(s); + }; + }, + 37241: (s, o, i) => { + var u = i(70695), + _ = i(72903), + w = i(64894); + s.exports = function keysIn(s) { + return w(s) ? u(s, !0) : _(s); + }; + }, + 68090: (s) => { + s.exports = function last(s) { + var o = null == s ? 0 : s.length; + return o ? s[o - 1] : void 0; + }; + }, + 50104: (s, o, i) => { + var u = i(53661); + function memoize(s, o) { + if ('function' != typeof s || (null != o && 'function' != typeof o)) + throw new TypeError('Expected a function'); + var memoized = function () { + var i = arguments, + u = o ? o.apply(this, i) : i[0], + _ = memoized.cache; + if (_.has(u)) return _.get(u); + var w = s.apply(this, i); + return ((memoized.cache = _.set(u, w) || _), w); + }; + return ((memoized.cache = new (memoize.Cache || u)()), memoized); + } + ((memoize.Cache = u), (s.exports = memoize)); + }, + 55364: (s, o, i) => { + var u = i(85250), + _ = i(20999)(function (s, o, i) { + u(s, o, i); + }); + s.exports = _; + }, + 6048: (s) => { + s.exports = function negate(s) { + if ('function' != typeof s) throw new TypeError('Expected a function'); + return function () { + var o = arguments; + switch (o.length) { + case 0: + return !s.call(this); + case 1: + return !s.call(this, o[0]); + case 2: + return !s.call(this, o[0], o[1]); + case 3: + return !s.call(this, o[0], o[1], o[2]); + } + return !s.apply(this, o); + }; + }; + }, + 63950: (s) => { + s.exports = function noop() {}; + }, + 10124: (s, o, i) => { + var u = i(9325); + s.exports = function () { + return u.Date.now(); + }; + }, + 90179: (s, o, i) => { + var u = i(34932), + _ = i(9999), + w = i(19931), + x = i(31769), + C = i(21791), + j = i(53138), + L = i(38816), + B = i(83349), + $ = L(function (s, o) { + var i = {}; + if (null == s) return i; + var L = !1; + ((o = u(o, function (o) { + return ((o = x(o, s)), L || (L = o.length > 1), o); + })), + C(s, B(s), i), + L && (i = _(i, 7, j))); + for (var $ = o.length; $--; ) w(i, o[$]); + return i; + }); + s.exports = $; + }, + 50583: (s, o, i) => { + var u = i(47237), + _ = i(17255), + w = i(28586), + x = i(77797); + s.exports = function property(s) { + return w(s) ? u(x(s)) : _(s); + }; + }, + 84195: (s, o, i) => { + var u = i(66977), + _ = i(38816), + w = _(function (s, o) { + return u(s, 256, void 0, void 0, void 0, o); + }); + s.exports = w; + }, + 40860: (s, o, i) => { + var u = i(40882), + _ = i(80909), + w = i(15389), + x = i(85558), + C = i(56449); + s.exports = function reduce(s, o, i) { + var j = C(s) ? u : x, + L = arguments.length < 3; + return j(s, w(o, 4), i, L, _); + }; + }, + 63560: (s, o, i) => { + var u = i(73170); + s.exports = function set(s, o, i) { + return null == s ? s : u(s, o, i); + }; + }, + 42426: (s, o, i) => { + var u = i(14248), + _ = i(15389), + w = i(90916), + x = i(56449), + C = i(36800); + s.exports = function some(s, o, i) { + var j = x(s) ? u : w; + return (i && C(s, o, i) && (o = void 0), j(s, _(o, 3))); + }; + }, + 63345: (s) => { + s.exports = function stubArray() { + return []; + }; + }, + 89935: (s) => { + s.exports = function stubFalse() { + return !1; + }; + }, + 17400: (s, o, i) => { + var u = i(99374), + _ = 1 / 0; + s.exports = function toFinite(s) { + return s + ? (s = u(s)) === _ || s === -1 / 0 + ? 17976931348623157e292 * (s < 0 ? -1 : 1) + : s == s + ? s + : 0 + : 0 === s + ? s + : 0; + }; + }, + 61489: (s, o, i) => { + var u = i(17400); + s.exports = function toInteger(s) { + var o = u(s), + i = o % 1; + return o == o ? (i ? o - i : o) : 0; + }; + }, + 80218: (s, o, i) => { + var u = i(13222); + s.exports = function toLower(s) { + return u(s).toLowerCase(); + }; + }, + 99374: (s, o, i) => { + var u = i(54128), + _ = i(23805), + w = i(44394), + x = /^[-+]0x[0-9a-f]+$/i, + C = /^0b[01]+$/i, + j = /^0o[0-7]+$/i, + L = parseInt; + s.exports = function toNumber(s) { + if ('number' == typeof s) return s; + if (w(s)) return NaN; + if (_(s)) { + var o = 'function' == typeof s.valueOf ? s.valueOf() : s; + s = _(o) ? o + '' : o; + } + if ('string' != typeof s) return 0 === s ? s : +s; + s = u(s); + var i = C.test(s); + return i || j.test(s) ? L(s.slice(2), i ? 2 : 8) : x.test(s) ? NaN : +s; + }; + }, + 42072: (s, o, i) => { + var u = i(34932), + _ = i(23007), + w = i(56449), + x = i(44394), + C = i(61802), + j = i(77797), + L = i(13222); + s.exports = function toPath(s) { + return w(s) ? u(s, j) : x(s) ? [s] : _(C(L(s))); + }; + }, + 69884: (s, o, i) => { + var u = i(21791), + _ = i(37241); + s.exports = function toPlainObject(s) { + return u(s, _(s)); + }; + }, + 13222: (s, o, i) => { + var u = i(77556); + s.exports = function toString(s) { + return null == s ? '' : u(s); + }; + }, + 55808: (s, o, i) => { + var u = i(12507)('toUpperCase'); + s.exports = u; + }, + 66645: (s, o, i) => { + var u = i(1733), + _ = i(45434), + w = i(13222), + x = i(22225); + s.exports = function words(s, o, i) { + return ( + (s = w(s)), + void 0 === (o = i ? void 0 : o) ? (_(s) ? x(s) : u(s)) : s.match(o) || [] + ); + }; + }, + 53758: (s, o, i) => { + var u = i(30980), + _ = i(56017), + w = i(94033), + x = i(56449), + C = i(40346), + j = i(80257), + L = Object.prototype.hasOwnProperty; + function lodash(s) { + if (C(s) && !x(s) && !(s instanceof u)) { + if (s instanceof _) return s; + if (L.call(s, '__wrapped__')) return j(s); + } + return new _(s); + } + ((lodash.prototype = w.prototype), + (lodash.prototype.constructor = lodash), + (s.exports = lodash)); + }, + 47248: (s, o, i) => { + var u = i(16547), + _ = i(51234); + s.exports = function zipObject(s, o) { + return _(s || [], o || [], u); + }; + }, + 43768: (s, o, i) => { + 'use strict'; + var u = i(45981), + _ = i(85587); + ((o.highlight = highlight), + (o.highlightAuto = function highlightAuto(s, o) { + var i, + x, + C, + j, + L = o || {}, + B = L.subset || u.listLanguages(), + $ = L.prefix, + V = B.length, + U = -1; + null == $ && ($ = w); + if ('string' != typeof s) throw _('Expected `string` for value, got `%s`', s); + ((x = { relevance: 0, language: null, value: [] }), + (i = { relevance: 0, language: null, value: [] })); + for (; ++U < V; ) + ((j = B[U]), + u.getLanguage(j) && + (((C = highlight(j, s, o)).language = j), + C.relevance > x.relevance && (x = C), + C.relevance > i.relevance && ((x = i), (i = C)))); + x.language && (i.secondBest = x); + return i; + }), + (o.registerLanguage = function registerLanguage(s, o) { + u.registerLanguage(s, o); + }), + (o.listLanguages = function listLanguages() { + return u.listLanguages(); + }), + (o.registerAlias = function registerAlias(s, o) { + var i, + _ = s; + o && ((_ = {})[s] = o); + for (i in _) u.registerAliases(_[i], { languageName: i }); + }), + (Emitter.prototype.addText = function text(s) { + var o, + i, + u = this.stack; + if ('' === s) return; + ((o = u[u.length - 1]), + (i = o.children[o.children.length - 1]) && 'text' === i.type + ? (i.value += s) + : o.children.push({ type: 'text', value: s })); + }), + (Emitter.prototype.addKeyword = function addKeyword(s, o) { + (this.openNode(o), this.addText(s), this.closeNode()); + }), + (Emitter.prototype.addSublanguage = function addSublanguage(s, o) { + var i = this.stack, + u = i[i.length - 1], + _ = s.rootNode.children, + w = o + ? { + type: 'element', + tagName: 'span', + properties: { className: [o] }, + children: _ + } + : _; + u.children = u.children.concat(w); + }), + (Emitter.prototype.openNode = function open(s) { + var o = this.stack, + i = this.options.classPrefix + s, + u = o[o.length - 1], + _ = { + type: 'element', + tagName: 'span', + properties: { className: [i] }, + children: [] + }; + (u.children.push(_), o.push(_)); + }), + (Emitter.prototype.closeNode = function close() { + this.stack.pop(); + }), + (Emitter.prototype.closeAllNodes = noop), + (Emitter.prototype.finalize = noop), + (Emitter.prototype.toHTML = function toHtmlNoop() { + return ''; + })); + var w = 'hljs-'; + function highlight(s, o, i) { + var x, + C = u.configure({}), + j = (i || {}).prefix; + if ('string' != typeof s) throw _('Expected `string` for name, got `%s`', s); + if (!u.getLanguage(s)) throw _('Unknown language: `%s` is not registered', s); + if ('string' != typeof o) throw _('Expected `string` for value, got `%s`', o); + if ( + (null == j && (j = w), + u.configure({ __emitter: Emitter, classPrefix: j }), + (x = u.highlight(o, { language: s, ignoreIllegals: !0 })), + u.configure(C || {}), + x.errorRaised) + ) + throw x.errorRaised; + return { + relevance: x.relevance, + language: x.language, + value: x.emitter.rootNode.children + }; + } + function Emitter(s) { + ((this.options = s), + (this.rootNode = { children: [] }), + (this.stack = [this.rootNode])); + } + function noop() {} + }, + 92340: (s, o, i) => { + const u = i(6048); + function coerceElementMatchingCallback(s) { + return 'string' == typeof s + ? (o) => o.element === s + : s.constructor && s.extend + ? (o) => o instanceof s + : s; + } + class ArraySlice { + constructor(s) { + this.elements = s || []; + } + toValue() { + return this.elements.map((s) => s.toValue()); + } + map(s, o) { + return this.elements.map(s, o); + } + flatMap(s, o) { + return this.map(s, o).reduce((s, o) => s.concat(o), []); + } + compactMap(s, o) { + const i = []; + return ( + this.forEach((u) => { + const _ = s.bind(o)(u); + _ && i.push(_); + }), + i + ); + } + filter(s, o) { + return ( + (s = coerceElementMatchingCallback(s)), + new ArraySlice(this.elements.filter(s, o)) + ); + } + reject(s, o) { + return ( + (s = coerceElementMatchingCallback(s)), + new ArraySlice(this.elements.filter(u(s), o)) + ); + } + find(s, o) { + return ((s = coerceElementMatchingCallback(s)), this.elements.find(s, o)); + } + forEach(s, o) { + this.elements.forEach(s, o); + } + reduce(s, o) { + return this.elements.reduce(s, o); + } + includes(s) { + return this.elements.some((o) => o.equals(s)); + } + shift() { + return this.elements.shift(); + } + unshift(s) { + this.elements.unshift(this.refract(s)); + } + push(s) { + return (this.elements.push(this.refract(s)), this); + } + add(s) { + this.push(s); + } + get(s) { + return this.elements[s]; + } + getValue(s) { + const o = this.elements[s]; + if (o) return o.toValue(); + } + get length() { + return this.elements.length; + } + get isEmpty() { + return 0 === this.elements.length; + } + get first() { + return this.elements[0]; + } + } + ('undefined' != typeof Symbol && + (ArraySlice.prototype[Symbol.iterator] = function symbol() { + return this.elements[Symbol.iterator](); + }), + (s.exports = ArraySlice)); + }, + 55973: (s) => { + class KeyValuePair { + constructor(s, o) { + ((this.key = s), (this.value = o)); + } + clone() { + const s = new KeyValuePair(); + return ( + this.key && (s.key = this.key.clone()), + this.value && (s.value = this.value.clone()), + s + ); + } + } + s.exports = KeyValuePair; + }, + 3110: (s, o, i) => { + const u = i(5187), + _ = i(85015), + w = i(98023), + x = i(53812), + C = i(23805), + j = i(85105), + L = i(86804); + class Namespace { + constructor(s) { + ((this.elementMap = {}), + (this.elementDetection = []), + (this.Element = L.Element), + (this.KeyValuePair = L.KeyValuePair), + (s && s.noDefault) || this.useDefault(), + (this._attributeElementKeys = []), + (this._attributeElementArrayKeys = [])); + } + use(s) { + return ( + s.namespace && s.namespace({ base: this }), + s.load && s.load({ base: this }), + this + ); + } + useDefault() { + return ( + this.register('null', L.NullElement) + .register('string', L.StringElement) + .register('number', L.NumberElement) + .register('boolean', L.BooleanElement) + .register('array', L.ArrayElement) + .register('object', L.ObjectElement) + .register('member', L.MemberElement) + .register('ref', L.RefElement) + .register('link', L.LinkElement), + this.detect(u, L.NullElement, !1) + .detect(_, L.StringElement, !1) + .detect(w, L.NumberElement, !1) + .detect(x, L.BooleanElement, !1) + .detect(Array.isArray, L.ArrayElement, !1) + .detect(C, L.ObjectElement, !1), + this + ); + } + register(s, o) { + return ((this._elements = void 0), (this.elementMap[s] = o), this); + } + unregister(s) { + return ((this._elements = void 0), delete this.elementMap[s], this); + } + detect(s, o, i) { + return ( + void 0 === i || i + ? this.elementDetection.unshift([s, o]) + : this.elementDetection.push([s, o]), + this + ); + } + toElement(s) { + if (s instanceof this.Element) return s; + let o; + for (let i = 0; i < this.elementDetection.length; i += 1) { + const u = this.elementDetection[i][0], + _ = this.elementDetection[i][1]; + if (u(s)) { + o = new _(s); + break; + } + } + return o; + } + getElementClass(s) { + const o = this.elementMap[s]; + return void 0 === o ? this.Element : o; + } + fromRefract(s) { + return this.serialiser.deserialise(s); + } + toRefract(s) { + return this.serialiser.serialise(s); + } + get elements() { + return ( + void 0 === this._elements && + ((this._elements = { Element: this.Element }), + Object.keys(this.elementMap).forEach((s) => { + const o = s[0].toUpperCase() + s.substr(1); + this._elements[o] = this.elementMap[s]; + })), + this._elements + ); + } + get serialiser() { + return new j(this); + } + } + ((j.prototype.Namespace = Namespace), (s.exports = Namespace)); + }, + 10866: (s, o, i) => { + const u = i(6048), + _ = i(92340); + class ObjectSlice extends _ { + map(s, o) { + return this.elements.map((i) => s.bind(o)(i.value, i.key, i)); + } + filter(s, o) { + return new ObjectSlice(this.elements.filter((i) => s.bind(o)(i.value, i.key, i))); + } + reject(s, o) { + return this.filter(u(s.bind(o))); + } + forEach(s, o) { + return this.elements.forEach((i, u) => { + s.bind(o)(i.value, i.key, i, u); + }); + } + keys() { + return this.map((s, o) => o.toValue()); + } + values() { + return this.map((s) => s.toValue()); + } + } + s.exports = ObjectSlice; + }, + 86804: (s, o, i) => { + const u = i(10316), + _ = i(41067), + w = i(71167), + x = i(40239), + C = i(12242), + j = i(6233), + L = i(87726), + B = i(61045), + $ = i(86303), + V = i(14540), + U = i(92340), + z = i(10866), + Y = i(55973); + function refract(s) { + if (s instanceof u) return s; + if ('string' == typeof s) return new w(s); + if ('number' == typeof s) return new x(s); + if ('boolean' == typeof s) return new C(s); + if (null === s) return new _(); + if (Array.isArray(s)) return new j(s.map(refract)); + if ('object' == typeof s) { + return new B(s); + } + return s; + } + ((u.prototype.ObjectElement = B), + (u.prototype.RefElement = V), + (u.prototype.MemberElement = L), + (u.prototype.refract = refract), + (U.prototype.refract = refract), + (s.exports = { + Element: u, + NullElement: _, + StringElement: w, + NumberElement: x, + BooleanElement: C, + ArrayElement: j, + MemberElement: L, + ObjectElement: B, + LinkElement: $, + RefElement: V, + refract, + ArraySlice: U, + ObjectSlice: z, + KeyValuePair: Y + })); + }, + 86303: (s, o, i) => { + const u = i(10316); + s.exports = class LinkElement extends u { + constructor(s, o, i) { + (super(s || [], o, i), (this.element = 'link')); + } + get relation() { + return this.attributes.get('relation'); + } + set relation(s) { + this.attributes.set('relation', s); + } + get href() { + return this.attributes.get('href'); + } + set href(s) { + this.attributes.set('href', s); + } + }; + }, + 14540: (s, o, i) => { + const u = i(10316); + s.exports = class RefElement extends u { + constructor(s, o, i) { + (super(s || [], o, i), (this.element = 'ref'), this.path || (this.path = 'element')); + } + get path() { + return this.attributes.get('path'); + } + set path(s) { + this.attributes.set('path', s); + } + }; + }, + 34035: (s, o, i) => { + const u = i(3110), + _ = i(86804); + ((o.g$ = u), + (o.KeyValuePair = i(55973)), + (o.G6 = _.ArraySlice), + (o.ot = _.ObjectSlice), + (o.Hg = _.Element), + (o.Om = _.StringElement), + (o.kT = _.NumberElement), + (o.bd = _.BooleanElement), + (o.Os = _.NullElement), + (o.wE = _.ArrayElement), + (o.Sh = _.ObjectElement), + (o.Pr = _.MemberElement), + (o.sI = _.RefElement), + (o.Ft = _.LinkElement), + (o.e = _.refract), + i(85105), + i(75147)); + }, + 6233: (s, o, i) => { + const u = i(6048), + _ = i(10316), + w = i(92340); + class ArrayElement extends _ { + constructor(s, o, i) { + (super(s || [], o, i), (this.element = 'array')); + } + primitive() { + return 'array'; + } + get(s) { + return this.content[s]; + } + getValue(s) { + const o = this.get(s); + if (o) return o.toValue(); + } + getIndex(s) { + return this.content[s]; + } + set(s, o) { + return ((this.content[s] = this.refract(o)), this); + } + remove(s) { + const o = this.content.splice(s, 1); + return o.length ? o[0] : null; + } + map(s, o) { + return this.content.map(s, o); + } + flatMap(s, o) { + return this.map(s, o).reduce((s, o) => s.concat(o), []); + } + compactMap(s, o) { + const i = []; + return ( + this.forEach((u) => { + const _ = s.bind(o)(u); + _ && i.push(_); + }), + i + ); + } + filter(s, o) { + return new w(this.content.filter(s, o)); + } + reject(s, o) { + return this.filter(u(s), o); + } + reduce(s, o) { + let i, u; + void 0 !== o + ? ((i = 0), (u = this.refract(o))) + : ((i = 1), (u = 'object' === this.primitive() ? this.first.value : this.first)); + for (let o = i; o < this.length; o += 1) { + const i = this.content[o]; + u = + 'object' === this.primitive() + ? this.refract(s(u, i.value, i.key, i, this)) + : this.refract(s(u, i, o, this)); + } + return u; + } + forEach(s, o) { + this.content.forEach((i, u) => { + s.bind(o)(i, this.refract(u)); + }); + } + shift() { + return this.content.shift(); + } + unshift(s) { + this.content.unshift(this.refract(s)); + } + push(s) { + return (this.content.push(this.refract(s)), this); + } + add(s) { + this.push(s); + } + findElements(s, o) { + const i = o || {}, + u = !!i.recursive, + _ = void 0 === i.results ? [] : i.results; + return ( + this.forEach((o, i, w) => { + (u && + void 0 !== o.findElements && + o.findElements(s, { results: _, recursive: u }), + s(o, i, w) && _.push(o)); + }), + _ + ); + } + find(s) { + return new w(this.findElements(s, { recursive: !0 })); + } + findByElement(s) { + return this.find((o) => o.element === s); + } + findByClass(s) { + return this.find((o) => o.classes.includes(s)); + } + getById(s) { + return this.find((o) => o.id.toValue() === s).first; + } + includes(s) { + return this.content.some((o) => o.equals(s)); + } + contains(s) { + return this.includes(s); + } + empty() { + return new this.constructor([]); + } + 'fantasy-land/empty'() { + return this.empty(); + } + concat(s) { + return new this.constructor(this.content.concat(s.content)); + } + 'fantasy-land/concat'(s) { + return this.concat(s); + } + 'fantasy-land/map'(s) { + return new this.constructor(this.map(s)); + } + 'fantasy-land/chain'(s) { + return this.map((o) => s(o), this).reduce((s, o) => s.concat(o), this.empty()); + } + 'fantasy-land/filter'(s) { + return new this.constructor(this.content.filter(s)); + } + 'fantasy-land/reduce'(s, o) { + return this.content.reduce(s, o); + } + get length() { + return this.content.length; + } + get isEmpty() { + return 0 === this.content.length; + } + get first() { + return this.getIndex(0); + } + get second() { + return this.getIndex(1); + } + get last() { + return this.getIndex(this.length - 1); + } + } + ((ArrayElement.empty = function empty() { + return new this(); + }), + (ArrayElement['fantasy-land/empty'] = ArrayElement.empty), + 'undefined' != typeof Symbol && + (ArrayElement.prototype[Symbol.iterator] = function symbol() { + return this.content[Symbol.iterator](); + }), + (s.exports = ArrayElement)); + }, + 12242: (s, o, i) => { + const u = i(10316); + s.exports = class BooleanElement extends u { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'boolean')); + } + primitive() { + return 'boolean'; + } + }; + }, + 10316: (s, o, i) => { + const u = i(2404), + _ = i(55973), + w = i(92340); + class Element { + constructor(s, o, i) { + (o && (this.meta = o), i && (this.attributes = i), (this.content = s)); + } + freeze() { + Object.isFrozen(this) || + (this._meta && ((this.meta.parent = this), this.meta.freeze()), + this._attributes && ((this.attributes.parent = this), this.attributes.freeze()), + this.children.forEach((s) => { + ((s.parent = this), s.freeze()); + }, this), + this.content && Array.isArray(this.content) && Object.freeze(this.content), + Object.freeze(this)); + } + primitive() {} + clone() { + const s = new this.constructor(); + return ( + (s.element = this.element), + this.meta.length && (s._meta = this.meta.clone()), + this.attributes.length && (s._attributes = this.attributes.clone()), + this.content + ? this.content.clone + ? (s.content = this.content.clone()) + : Array.isArray(this.content) + ? (s.content = this.content.map((s) => s.clone())) + : (s.content = this.content) + : (s.content = this.content), + s + ); + } + toValue() { + return this.content instanceof Element + ? this.content.toValue() + : this.content instanceof _ + ? { + key: this.content.key.toValue(), + value: this.content.value ? this.content.value.toValue() : void 0 + } + : this.content && this.content.map + ? this.content.map((s) => s.toValue(), this) + : this.content; + } + toRef(s) { + if ('' === this.id.toValue()) + throw Error('Cannot create reference to an element that does not contain an ID'); + const o = new this.RefElement(this.id.toValue()); + return (s && (o.path = s), o); + } + findRecursive(...s) { + if (arguments.length > 1 && !this.isFrozen) + throw new Error( + 'Cannot find recursive with multiple element names without first freezing the element. Call `element.freeze()`' + ); + const o = s.pop(); + let i = new w(); + const append = (s, o) => (s.push(o), s), + checkElement = (s, i) => { + i.element === o && s.push(i); + const u = i.findRecursive(o); + return ( + u && u.reduce(append, s), + i.content instanceof _ && + (i.content.key && checkElement(s, i.content.key), + i.content.value && checkElement(s, i.content.value)), + s + ); + }; + return ( + this.content && + (this.content.element && checkElement(i, this.content), + Array.isArray(this.content) && this.content.reduce(checkElement, i)), + s.isEmpty || + (i = i.filter((o) => { + let i = o.parents.map((s) => s.element); + for (const o in s) { + const u = s[o], + _ = i.indexOf(u); + if (-1 === _) return !1; + i = i.splice(0, _); + } + return !0; + })), + i + ); + } + set(s) { + return ((this.content = s), this); + } + equals(s) { + return u(this.toValue(), s); + } + getMetaProperty(s, o) { + if (!this.meta.hasKey(s)) { + if (this.isFrozen) { + const s = this.refract(o); + return (s.freeze(), s); + } + this.meta.set(s, o); + } + return this.meta.get(s); + } + setMetaProperty(s, o) { + this.meta.set(s, o); + } + get element() { + return this._storedElement || 'element'; + } + set element(s) { + this._storedElement = s; + } + get content() { + return this._content; + } + set content(s) { + if (s instanceof Element) this._content = s; + else if (s instanceof w) this.content = s.elements; + else if ( + 'string' == typeof s || + 'number' == typeof s || + 'boolean' == typeof s || + 'null' === s || + null == s + ) + this._content = s; + else if (s instanceof _) this._content = s; + else if (Array.isArray(s)) this._content = s.map(this.refract); + else { + if ('object' != typeof s) throw new Error('Cannot set content to given value'); + this._content = Object.keys(s).map((o) => new this.MemberElement(o, s[o])); + } + } + get meta() { + if (!this._meta) { + if (this.isFrozen) { + const s = new this.ObjectElement(); + return (s.freeze(), s); + } + this._meta = new this.ObjectElement(); + } + return this._meta; + } + set meta(s) { + s instanceof this.ObjectElement ? (this._meta = s) : this.meta.set(s || {}); + } + get attributes() { + if (!this._attributes) { + if (this.isFrozen) { + const s = new this.ObjectElement(); + return (s.freeze(), s); + } + this._attributes = new this.ObjectElement(); + } + return this._attributes; + } + set attributes(s) { + s instanceof this.ObjectElement + ? (this._attributes = s) + : this.attributes.set(s || {}); + } + get id() { + return this.getMetaProperty('id', ''); + } + set id(s) { + this.setMetaProperty('id', s); + } + get classes() { + return this.getMetaProperty('classes', []); + } + set classes(s) { + this.setMetaProperty('classes', s); + } + get title() { + return this.getMetaProperty('title', ''); + } + set title(s) { + this.setMetaProperty('title', s); + } + get description() { + return this.getMetaProperty('description', ''); + } + set description(s) { + this.setMetaProperty('description', s); + } + get links() { + return this.getMetaProperty('links', []); + } + set links(s) { + this.setMetaProperty('links', s); + } + get isFrozen() { + return Object.isFrozen(this); + } + get parents() { + let { parent: s } = this; + const o = new w(); + for (; s; ) (o.push(s), (s = s.parent)); + return o; + } + get children() { + if (Array.isArray(this.content)) return new w(this.content); + if (this.content instanceof _) { + const s = new w([this.content.key]); + return (this.content.value && s.push(this.content.value), s); + } + return this.content instanceof Element ? new w([this.content]) : new w(); + } + get recursiveChildren() { + const s = new w(); + return ( + this.children.forEach((o) => { + (s.push(o), + o.recursiveChildren.forEach((o) => { + s.push(o); + })); + }), + s + ); + } + } + s.exports = Element; + }, + 87726: (s, o, i) => { + const u = i(55973), + _ = i(10316); + s.exports = class MemberElement extends _ { + constructor(s, o, i, _) { + (super(new u(), i, _), (this.element = 'member'), (this.key = s), (this.value = o)); + } + get key() { + return this.content.key; + } + set key(s) { + this.content.key = this.refract(s); + } + get value() { + return this.content.value; + } + set value(s) { + this.content.value = this.refract(s); + } + }; + }, + 41067: (s, o, i) => { + const u = i(10316); + s.exports = class NullElement extends u { + constructor(s, o, i) { + (super(s || null, o, i), (this.element = 'null')); + } + primitive() { + return 'null'; + } + set() { + return new Error('Cannot set the value of null'); + } + }; + }, + 40239: (s, o, i) => { + const u = i(10316); + s.exports = class NumberElement extends u { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'number')); + } + primitive() { + return 'number'; + } + }; + }, + 61045: (s, o, i) => { + const u = i(6048), + _ = i(23805), + w = i(6233), + x = i(87726), + C = i(10866); + s.exports = class ObjectElement extends w { + constructor(s, o, i) { + (super(s || [], o, i), (this.element = 'object')); + } + primitive() { + return 'object'; + } + toValue() { + return this.content.reduce( + (s, o) => ((s[o.key.toValue()] = o.value ? o.value.toValue() : void 0), s), + {} + ); + } + get(s) { + const o = this.getMember(s); + if (o) return o.value; + } + getMember(s) { + if (void 0 !== s) return this.content.find((o) => o.key.toValue() === s); + } + remove(s) { + let o = null; + return ( + (this.content = this.content.filter((i) => i.key.toValue() !== s || ((o = i), !1))), + o + ); + } + getKey(s) { + const o = this.getMember(s); + if (o) return o.key; + } + set(s, o) { + if (_(s)) + return ( + Object.keys(s).forEach((o) => { + this.set(o, s[o]); + }), + this + ); + const i = s, + u = this.getMember(i); + return (u ? (u.value = o) : this.content.push(new x(i, o)), this); + } + keys() { + return this.content.map((s) => s.key.toValue()); + } + values() { + return this.content.map((s) => s.value.toValue()); + } + hasKey(s) { + return this.content.some((o) => o.key.equals(s)); + } + items() { + return this.content.map((s) => [s.key.toValue(), s.value.toValue()]); + } + map(s, o) { + return this.content.map((i) => s.bind(o)(i.value, i.key, i)); + } + compactMap(s, o) { + const i = []; + return ( + this.forEach((u, _, w) => { + const x = s.bind(o)(u, _, w); + x && i.push(x); + }), + i + ); + } + filter(s, o) { + return new C(this.content).filter(s, o); + } + reject(s, o) { + return this.filter(u(s), o); + } + forEach(s, o) { + return this.content.forEach((i) => s.bind(o)(i.value, i.key, i)); + } + }; + }, + 71167: (s, o, i) => { + const u = i(10316); + s.exports = class StringElement extends u { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'string')); + } + primitive() { + return 'string'; + } + get length() { + return this.content.length; + } + }; + }, + 75147: (s, o, i) => { + const u = i(85105); + s.exports = class JSON06Serialiser extends u { + serialise(s) { + if (!(s instanceof this.namespace.elements.Element)) + throw new TypeError(`Given element \`${s}\` is not an Element instance`); + let o; + s._attributes && s.attributes.get('variable') && (o = s.attributes.get('variable')); + const i = { element: s.element }; + s._meta && s._meta.length > 0 && (i.meta = this.serialiseObject(s.meta)); + const u = 'enum' === s.element || -1 !== s.attributes.keys().indexOf('enumerations'); + if (u) { + const o = this.enumSerialiseAttributes(s); + o && (i.attributes = o); + } else if (s._attributes && s._attributes.length > 0) { + let { attributes: u } = s; + (u.get('metadata') && + ((u = u.clone()), u.set('meta', u.get('metadata')), u.remove('metadata')), + 'member' === s.element && o && ((u = u.clone()), u.remove('variable')), + u.length > 0 && (i.attributes = this.serialiseObject(u))); + } + if (u) i.content = this.enumSerialiseContent(s, i); + else if (this[`${s.element}SerialiseContent`]) + i.content = this[`${s.element}SerialiseContent`](s, i); + else if (void 0 !== s.content) { + let u; + (o && s.content.key + ? ((u = s.content.clone()), + u.key.attributes.set('variable', o), + (u = this.serialiseContent(u))) + : (u = this.serialiseContent(s.content)), + this.shouldSerialiseContent(s, u) && (i.content = u)); + } else + this.shouldSerialiseContent(s, s.content) && + s instanceof this.namespace.elements.Array && + (i.content = []); + return i; + } + shouldSerialiseContent(s, o) { + return ( + 'parseResult' === s.element || + 'httpRequest' === s.element || + 'httpResponse' === s.element || + 'category' === s.element || + 'link' === s.element || + (void 0 !== o && (!Array.isArray(o) || 0 !== o.length)) + ); + } + refSerialiseContent(s, o) { + return (delete o.attributes, { href: s.toValue(), path: s.path.toValue() }); + } + sourceMapSerialiseContent(s) { + return s.toValue(); + } + dataStructureSerialiseContent(s) { + return [this.serialiseContent(s.content)]; + } + enumSerialiseAttributes(s) { + const o = s.attributes.clone(), + i = o.remove('enumerations') || new this.namespace.elements.Array([]), + u = o.get('default'); + let _ = o.get('samples') || new this.namespace.elements.Array([]); + if ( + (u && + u.content && + (u.content.attributes && u.content.attributes.remove('typeAttributes'), + o.set('default', new this.namespace.elements.Array([u.content]))), + _.forEach((s) => { + s.content && s.content.element && s.content.attributes.remove('typeAttributes'); + }), + s.content && 0 !== i.length && _.unshift(s.content), + (_ = _.map((s) => + s instanceof this.namespace.elements.Array + ? [s] + : new this.namespace.elements.Array([s.content]) + )), + _.length && o.set('samples', _), + o.length > 0) + ) + return this.serialiseObject(o); + } + enumSerialiseContent(s) { + if (s._attributes) { + const o = s.attributes.get('enumerations'); + if (o && o.length > 0) + return o.content.map((s) => { + const o = s.clone(); + return (o.attributes.remove('typeAttributes'), this.serialise(o)); + }); + } + if (s.content) { + const o = s.content.clone(); + return (o.attributes.remove('typeAttributes'), [this.serialise(o)]); + } + return []; + } + deserialise(s) { + if ('string' == typeof s) return new this.namespace.elements.String(s); + if ('number' == typeof s) return new this.namespace.elements.Number(s); + if ('boolean' == typeof s) return new this.namespace.elements.Boolean(s); + if (null === s) return new this.namespace.elements.Null(); + if (Array.isArray(s)) + return new this.namespace.elements.Array(s.map(this.deserialise, this)); + const o = this.namespace.getElementClass(s.element), + i = new o(); + (i.element !== s.element && (i.element = s.element), + s.meta && this.deserialiseObject(s.meta, i.meta), + s.attributes && this.deserialiseObject(s.attributes, i.attributes)); + const u = this.deserialiseContent(s.content); + if (((void 0 === u && null !== i.content) || (i.content = u), 'enum' === i.element)) { + i.content && i.attributes.set('enumerations', i.content); + let s = i.attributes.get('samples'); + if ((i.attributes.remove('samples'), s)) { + const u = s; + ((s = new this.namespace.elements.Array()), + u.forEach((u) => { + u.forEach((u) => { + const _ = new o(u); + ((_.element = i.element), s.push(_)); + }); + })); + const _ = s.shift(); + ((i.content = _ ? _.content : void 0), i.attributes.set('samples', s)); + } else i.content = void 0; + let u = i.attributes.get('default'); + if (u && u.length > 0) { + u = u.get(0); + const s = new o(u); + ((s.element = i.element), i.attributes.set('default', s)); + } + } else if ('dataStructure' === i.element && Array.isArray(i.content)) + [i.content] = i.content; + else if ('category' === i.element) { + const s = i.attributes.get('meta'); + s && (i.attributes.set('metadata', s), i.attributes.remove('meta')); + } else + 'member' === i.element && + i.key && + i.key._attributes && + i.key._attributes.getValue('variable') && + (i.attributes.set('variable', i.key.attributes.get('variable')), + i.key.attributes.remove('variable')); + return i; + } + serialiseContent(s) { + if (s instanceof this.namespace.elements.Element) return this.serialise(s); + if (s instanceof this.namespace.KeyValuePair) { + const o = { key: this.serialise(s.key) }; + return (s.value && (o.value = this.serialise(s.value)), o); + } + return s && s.map ? s.map(this.serialise, this) : s; + } + deserialiseContent(s) { + if (s) { + if (s.element) return this.deserialise(s); + if (s.key) { + const o = new this.namespace.KeyValuePair(this.deserialise(s.key)); + return (s.value && (o.value = this.deserialise(s.value)), o); + } + if (s.map) return s.map(this.deserialise, this); + } + return s; + } + shouldRefract(s) { + return ( + !!( + (s._attributes && s.attributes.keys().length) || + (s._meta && s.meta.keys().length) + ) || + ('enum' !== s.element && (s.element !== s.primitive() || 'member' === s.element)) + ); + } + convertKeyToRefract(s, o) { + return this.shouldRefract(o) + ? this.serialise(o) + : 'enum' === o.element + ? this.serialiseEnum(o) + : 'array' === o.element + ? o.map((o) => + this.shouldRefract(o) || 'default' === s + ? this.serialise(o) + : 'array' === o.element || 'object' === o.element || 'enum' === o.element + ? o.children.map((s) => this.serialise(s)) + : o.toValue() + ) + : 'object' === o.element + ? (o.content || []).map(this.serialise, this) + : o.toValue(); + } + serialiseEnum(s) { + return s.children.map((s) => this.serialise(s)); + } + serialiseObject(s) { + const o = {}; + return ( + s.forEach((s, i) => { + if (s) { + const u = i.toValue(); + o[u] = this.convertKeyToRefract(u, s); + } + }), + o + ); + } + deserialiseObject(s, o) { + Object.keys(s).forEach((i) => { + o.set(i, this.deserialise(s[i])); + }); + } + }; + }, + 85105: (s) => { + s.exports = class JSONSerialiser { + constructor(s) { + this.namespace = s || new this.Namespace(); + } + serialise(s) { + if (!(s instanceof this.namespace.elements.Element)) + throw new TypeError(`Given element \`${s}\` is not an Element instance`); + const o = { element: s.element }; + (s._meta && s._meta.length > 0 && (o.meta = this.serialiseObject(s.meta)), + s._attributes && + s._attributes.length > 0 && + (o.attributes = this.serialiseObject(s.attributes))); + const i = this.serialiseContent(s.content); + return (void 0 !== i && (o.content = i), o); + } + deserialise(s) { + if (!s.element) + throw new Error('Given value is not an object containing an element name'); + const o = new (this.namespace.getElementClass(s.element))(); + (o.element !== s.element && (o.element = s.element), + s.meta && this.deserialiseObject(s.meta, o.meta), + s.attributes && this.deserialiseObject(s.attributes, o.attributes)); + const i = this.deserialiseContent(s.content); + return ((void 0 === i && null !== o.content) || (o.content = i), o); + } + serialiseContent(s) { + if (s instanceof this.namespace.elements.Element) return this.serialise(s); + if (s instanceof this.namespace.KeyValuePair) { + const o = { key: this.serialise(s.key) }; + return (s.value && (o.value = this.serialise(s.value)), o); + } + if (s && s.map) { + if (0 === s.length) return; + return s.map(this.serialise, this); + } + return s; + } + deserialiseContent(s) { + if (s) { + if (s.element) return this.deserialise(s); + if (s.key) { + const o = new this.namespace.KeyValuePair(this.deserialise(s.key)); + return (s.value && (o.value = this.deserialise(s.value)), o); + } + if (s.map) return s.map(this.deserialise, this); + } + return s; + } + serialiseObject(s) { + const o = {}; + if ( + (s.forEach((s, i) => { + s && (o[i.toValue()] = this.serialise(s)); + }), + 0 !== Object.keys(o).length) + ) + return o; + } + deserialiseObject(s, o) { + Object.keys(s).forEach((i) => { + o.set(i, this.deserialise(s[i])); + }); + } + }; + }, + 65606: (s) => { + var o, + i, + u = (s.exports = {}); + function defaultSetTimout() { + throw new Error('setTimeout has not been defined'); + } + function defaultClearTimeout() { + throw new Error('clearTimeout has not been defined'); + } + function runTimeout(s) { + if (o === setTimeout) return setTimeout(s, 0); + if ((o === defaultSetTimout || !o) && setTimeout) + return ((o = setTimeout), setTimeout(s, 0)); + try { + return o(s, 0); + } catch (i) { + try { + return o.call(null, s, 0); + } catch (i) { + return o.call(this, s, 0); + } + } + } + !(function () { + try { + o = 'function' == typeof setTimeout ? setTimeout : defaultSetTimout; + } catch (s) { + o = defaultSetTimout; + } + try { + i = 'function' == typeof clearTimeout ? clearTimeout : defaultClearTimeout; + } catch (s) { + i = defaultClearTimeout; + } + })(); + var _, + w = [], + x = !1, + C = -1; + function cleanUpNextTick() { + x && _ && ((x = !1), _.length ? (w = _.concat(w)) : (C = -1), w.length && drainQueue()); + } + function drainQueue() { + if (!x) { + var s = runTimeout(cleanUpNextTick); + x = !0; + for (var o = w.length; o; ) { + for (_ = w, w = []; ++C < o; ) _ && _[C].run(); + ((C = -1), (o = w.length)); + } + ((_ = null), + (x = !1), + (function runClearTimeout(s) { + if (i === clearTimeout) return clearTimeout(s); + if ((i === defaultClearTimeout || !i) && clearTimeout) + return ((i = clearTimeout), clearTimeout(s)); + try { + return i(s); + } catch (o) { + try { + return i.call(null, s); + } catch (o) { + return i.call(this, s); + } + } + })(s)); + } + } + function Item(s, o) { + ((this.fun = s), (this.array = o)); + } + function noop() {} + ((u.nextTick = function (s) { + var o = new Array(arguments.length - 1); + if (arguments.length > 1) + for (var i = 1; i < arguments.length; i++) o[i - 1] = arguments[i]; + (w.push(new Item(s, o)), 1 !== w.length || x || runTimeout(drainQueue)); + }), + (Item.prototype.run = function () { + this.fun.apply(null, this.array); + }), + (u.title = 'browser'), + (u.browser = !0), + (u.env = {}), + (u.argv = []), + (u.version = ''), + (u.versions = {}), + (u.on = noop), + (u.addListener = noop), + (u.once = noop), + (u.off = noop), + (u.removeListener = noop), + (u.removeAllListeners = noop), + (u.emit = noop), + (u.prependListener = noop), + (u.prependOnceListener = noop), + (u.listeners = function (s) { + return []; + }), + (u.binding = function (s) { + throw new Error('process.binding is not supported'); + }), + (u.cwd = function () { + return '/'; + }), + (u.chdir = function (s) { + throw new Error('process.chdir is not supported'); + }), + (u.umask = function () { + return 0; + })); + }, + 2694: (s, o, i) => { + 'use strict'; + var u = i(6925); + function emptyFunction() {} + function emptyFunctionWithReset() {} + ((emptyFunctionWithReset.resetWarningCache = emptyFunction), + (s.exports = function () { + function shim(s, o, i, _, w, x) { + if (x !== u) { + var C = new Error( + 'Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types' + ); + throw ((C.name = 'Invariant Violation'), C); + } + } + function getShim() { + return shim; + } + shim.isRequired = shim; + var s = { + array: shim, + bigint: shim, + bool: shim, + func: shim, + number: shim, + object: shim, + string: shim, + symbol: shim, + any: shim, + arrayOf: getShim, + element: shim, + elementType: shim, + instanceOf: getShim, + node: shim, + objectOf: getShim, + oneOf: getShim, + oneOfType: getShim, + shape: getShim, + exact: getShim, + checkPropTypes: emptyFunctionWithReset, + resetWarningCache: emptyFunction + }; + return ((s.PropTypes = s), s); + })); + }, + 5556: (s, o, i) => { + s.exports = i(2694)(); + }, + 6925: (s) => { + 'use strict'; + s.exports = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED'; + }, + 73992: (s, o) => { + 'use strict'; + var i = Object.prototype.hasOwnProperty; + function decode(s) { + try { + return decodeURIComponent(s.replace(/\+/g, ' ')); + } catch (s) { + return null; + } + } + function encode(s) { + try { + return encodeURIComponent(s); + } catch (s) { + return null; + } + } + ((o.stringify = function querystringify(s, o) { + o = o || ''; + var u, + _, + w = []; + for (_ in ('string' != typeof o && (o = '?'), s)) + if (i.call(s, _)) { + if ( + ((u = s[_]) || (null != u && !isNaN(u)) || (u = ''), + (_ = encode(_)), + (u = encode(u)), + null === _ || null === u) + ) + continue; + w.push(_ + '=' + u); + } + return w.length ? o + w.join('&') : ''; + }), + (o.parse = function querystring(s) { + for (var o, i = /([^=?#&]+)=?([^&]*)/g, u = {}; (o = i.exec(s)); ) { + var _ = decode(o[1]), + w = decode(o[2]); + null === _ || null === w || _ in u || (u[_] = w); + } + return u; + })); + }, + 41859: (s, o, i) => { + const u = i(27096), + _ = i(78004), + w = u.types; + s.exports = class RandExp { + constructor(s, o) { + if ((this._setDefaults(s), s instanceof RegExp)) + ((this.ignoreCase = s.ignoreCase), (this.multiline = s.multiline), (s = s.source)); + else { + if ('string' != typeof s) throw new Error('Expected a regexp or string'); + ((this.ignoreCase = o && -1 !== o.indexOf('i')), + (this.multiline = o && -1 !== o.indexOf('m'))); + } + this.tokens = u(s); + } + _setDefaults(s) { + ((this.max = + null != s.max + ? s.max + : null != RandExp.prototype.max + ? RandExp.prototype.max + : 100), + (this.defaultRange = s.defaultRange ? s.defaultRange : this.defaultRange.clone()), + s.randInt && (this.randInt = s.randInt)); + } + gen() { + return this._gen(this.tokens, []); + } + _gen(s, o) { + var i, u, _, x, C; + switch (s.type) { + case w.ROOT: + case w.GROUP: + if (s.followedBy || s.notFollowedBy) return ''; + for ( + s.remember && void 0 === s.groupNumber && (s.groupNumber = o.push(null) - 1), + u = '', + x = 0, + C = (i = s.options ? this._randSelect(s.options) : s.stack).length; + x < C; + x++ + ) + u += this._gen(i[x], o); + return (s.remember && (o[s.groupNumber] = u), u); + case w.POSITION: + return ''; + case w.SET: + var j = this._expand(s); + return j.length ? String.fromCharCode(this._randSelect(j)) : ''; + case w.REPETITION: + for ( + _ = this.randInt(s.min, s.max === 1 / 0 ? s.min + this.max : s.max), + u = '', + x = 0; + x < _; + x++ + ) + u += this._gen(s.value, o); + return u; + case w.REFERENCE: + return o[s.value - 1] || ''; + case w.CHAR: + var L = + this.ignoreCase && this._randBool() ? this._toOtherCase(s.value) : s.value; + return String.fromCharCode(L); + } + } + _toOtherCase(s) { + return s + (97 <= s && s <= 122 ? -32 : 65 <= s && s <= 90 ? 32 : 0); + } + _randBool() { + return !this.randInt(0, 1); + } + _randSelect(s) { + return s instanceof _ + ? s.index(this.randInt(0, s.length - 1)) + : s[this.randInt(0, s.length - 1)]; + } + _expand(s) { + if (s.type === u.types.CHAR) return new _(s.value); + if (s.type === u.types.RANGE) return new _(s.from, s.to); + { + let o = new _(); + for (let i = 0; i < s.set.length; i++) { + let u = this._expand(s.set[i]); + if ((o.add(u), this.ignoreCase)) + for (let s = 0; s < u.length; s++) { + let i = u.index(s), + _ = this._toOtherCase(i); + i !== _ && o.add(_); + } + } + return s.not + ? this.defaultRange.clone().subtract(o) + : this.defaultRange.clone().intersect(o); + } + } + randInt(s, o) { + return s + Math.floor(Math.random() * (1 + o - s)); + } + get defaultRange() { + return (this._range = this._range || new _(32, 126)); + } + set defaultRange(s) { + this._range = s; + } + static randexp(s, o) { + var i; + return ( + 'string' == typeof s && (s = new RegExp(s, o)), + void 0 === s._randexp + ? ((i = new RandExp(s, o)), (s._randexp = i)) + : (i = s._randexp)._setDefaults(s), + i.gen() + ); + } + static sugar() { + RegExp.prototype.gen = function () { + return RandExp.randexp(this); + }; + } + }; + }, + 53209: (s, o, i) => { + 'use strict'; + var u = i(65606), + _ = 65536, + w = 4294967295; + var x = i(92861).Buffer, + C = i.g.crypto || i.g.msCrypto; + C && C.getRandomValues + ? (s.exports = function randomBytes(s, o) { + if (s > w) throw new RangeError('requested too many random bytes'); + var i = x.allocUnsafe(s); + if (s > 0) + if (s > _) for (var j = 0; j < s; j += _) C.getRandomValues(i.slice(j, j + _)); + else C.getRandomValues(i); + if ('function' == typeof o) + return u.nextTick(function () { + o(null, i); + }); + return i; + }) + : (s.exports = function oldBrowser() { + throw new Error( + 'Secure random number generation is not supported by this browser.\nUse Chrome, Firefox or Internet Explorer 11' + ); + }); + }, + 25264: (s, o, i) => { + 'use strict'; + function _typeof(s) { + return ( + (_typeof = + 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator + ? function (s) { + return typeof s; + } + : function (s) { + return s && + 'function' == typeof Symbol && + s.constructor === Symbol && + s !== Symbol.prototype + ? 'symbol' + : typeof s; + }), + _typeof(s) + ); + } + (Object.defineProperty(o, '__esModule', { value: !0 }), (o.CopyToClipboard = void 0)); + var u = _interopRequireDefault(i(96540)), + _ = _interopRequireDefault(i(17965)), + w = ['text', 'onCopy', 'options', 'children']; + function _interopRequireDefault(s) { + return s && s.__esModule ? s : { default: s }; + } + function ownKeys(s, o) { + var i = Object.keys(s); + if (Object.getOwnPropertySymbols) { + var u = Object.getOwnPropertySymbols(s); + (o && + (u = u.filter(function (o) { + return Object.getOwnPropertyDescriptor(s, o).enumerable; + })), + i.push.apply(i, u)); + } + return i; + } + function _objectSpread(s) { + for (var o = 1; o < arguments.length; o++) { + var i = null != arguments[o] ? arguments[o] : {}; + o % 2 + ? ownKeys(Object(i), !0).forEach(function (o) { + _defineProperty(s, o, i[o]); + }) + : Object.getOwnPropertyDescriptors + ? Object.defineProperties(s, Object.getOwnPropertyDescriptors(i)) + : ownKeys(Object(i)).forEach(function (o) { + Object.defineProperty(s, o, Object.getOwnPropertyDescriptor(i, o)); + }); + } + return s; + } + function _objectWithoutProperties(s, o) { + if (null == s) return {}; + var i, + u, + _ = (function _objectWithoutPropertiesLoose(s, o) { + if (null == s) return {}; + var i, + u, + _ = {}, + w = Object.keys(s); + for (u = 0; u < w.length; u++) ((i = w[u]), o.indexOf(i) >= 0 || (_[i] = s[i])); + return _; + })(s, o); + if (Object.getOwnPropertySymbols) { + var w = Object.getOwnPropertySymbols(s); + for (u = 0; u < w.length; u++) + ((i = w[u]), + o.indexOf(i) >= 0 || + (Object.prototype.propertyIsEnumerable.call(s, i) && (_[i] = s[i]))); + } + return _; + } + function _defineProperties(s, o) { + for (var i = 0; i < o.length; i++) { + var u = o[i]; + ((u.enumerable = u.enumerable || !1), + (u.configurable = !0), + 'value' in u && (u.writable = !0), + Object.defineProperty(s, u.key, u)); + } + } + function _setPrototypeOf(s, o) { + return ( + (_setPrototypeOf = + Object.setPrototypeOf || + function _setPrototypeOf(s, o) { + return ((s.__proto__ = o), s); + }), + _setPrototypeOf(s, o) + ); + } + function _createSuper(s) { + var o = (function _isNativeReflectConstruct() { + if ('undefined' == typeof Reflect || !Reflect.construct) return !1; + if (Reflect.construct.sham) return !1; + if ('function' == typeof Proxy) return !0; + try { + return ( + Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})), + !0 + ); + } catch (s) { + return !1; + } + })(); + return function _createSuperInternal() { + var i, + u = _getPrototypeOf(s); + if (o) { + var _ = _getPrototypeOf(this).constructor; + i = Reflect.construct(u, arguments, _); + } else i = u.apply(this, arguments); + return (function _possibleConstructorReturn(s, o) { + if (o && ('object' === _typeof(o) || 'function' == typeof o)) return o; + if (void 0 !== o) + throw new TypeError('Derived constructors may only return object or undefined'); + return _assertThisInitialized(s); + })(this, i); + }; + } + function _assertThisInitialized(s) { + if (void 0 === s) + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + return s; + } + function _getPrototypeOf(s) { + return ( + (_getPrototypeOf = Object.setPrototypeOf + ? Object.getPrototypeOf + : function _getPrototypeOf(s) { + return s.__proto__ || Object.getPrototypeOf(s); + }), + _getPrototypeOf(s) + ); + } + function _defineProperty(s, o, i) { + return ( + o in s + ? Object.defineProperty(s, o, { + value: i, + enumerable: !0, + configurable: !0, + writable: !0 + }) + : (s[o] = i), + s + ); + } + var x = (function (s) { + !(function _inherits(s, o) { + if ('function' != typeof o && null !== o) + throw new TypeError('Super expression must either be null or a function'); + ((s.prototype = Object.create(o && o.prototype, { + constructor: { value: s, writable: !0, configurable: !0 } + })), + Object.defineProperty(s, 'prototype', { writable: !1 }), + o && _setPrototypeOf(s, o)); + })(CopyToClipboard, s); + var o = _createSuper(CopyToClipboard); + function CopyToClipboard() { + var s; + !(function _classCallCheck(s, o) { + if (!(s instanceof o)) throw new TypeError('Cannot call a class as a function'); + })(this, CopyToClipboard); + for (var i = arguments.length, w = new Array(i), x = 0; x < i; x++) + w[x] = arguments[x]; + return ( + _defineProperty( + _assertThisInitialized((s = o.call.apply(o, [this].concat(w)))), + 'onClick', + function (o) { + var i = s.props, + w = i.text, + x = i.onCopy, + C = i.children, + j = i.options, + L = u.default.Children.only(C), + B = (0, _.default)(w, j); + (x && x(w, B), + L && L.props && 'function' == typeof L.props.onClick && L.props.onClick(o)); + } + ), + s + ); + } + return ( + (function _createClass(s, o, i) { + return ( + o && _defineProperties(s.prototype, o), + i && _defineProperties(s, i), + Object.defineProperty(s, 'prototype', { writable: !1 }), + s + ); + })(CopyToClipboard, [ + { + key: 'render', + value: function render() { + var s = this.props, + o = (s.text, s.onCopy, s.options, s.children), + i = _objectWithoutProperties(s, w), + _ = u.default.Children.only(o); + return u.default.cloneElement( + _, + _objectSpread(_objectSpread({}, i), {}, { onClick: this.onClick }) + ); + } + } + ]), + CopyToClipboard + ); + })(u.default.PureComponent); + ((o.CopyToClipboard = x), + _defineProperty(x, 'defaultProps', { onCopy: void 0, options: void 0 })); + }, + 59399: (s, o, i) => { + 'use strict'; + var u = i(25264).CopyToClipboard; + ((u.CopyToClipboard = u), (s.exports = u)); + }, + 81214: (s, o, i) => { + 'use strict'; + function _typeof(s) { + return ( + (_typeof = + 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator + ? function (s) { + return typeof s; + } + : function (s) { + return s && + 'function' == typeof Symbol && + s.constructor === Symbol && + s !== Symbol.prototype + ? 'symbol' + : typeof s; + }), + _typeof(s) + ); + } + (Object.defineProperty(o, '__esModule', { value: !0 }), (o.DebounceInput = void 0)); + var u = _interopRequireDefault(i(96540)), + _ = _interopRequireDefault(i(20181)), + w = [ + 'element', + 'onChange', + 'value', + 'minLength', + 'debounceTimeout', + 'forceNotifyByEnter', + 'forceNotifyOnBlur', + 'onKeyDown', + 'onBlur', + 'inputRef' + ]; + function _interopRequireDefault(s) { + return s && s.__esModule ? s : { default: s }; + } + function _objectWithoutProperties(s, o) { + if (null == s) return {}; + var i, + u, + _ = (function _objectWithoutPropertiesLoose(s, o) { + if (null == s) return {}; + var i, + u, + _ = {}, + w = Object.keys(s); + for (u = 0; u < w.length; u++) ((i = w[u]), o.indexOf(i) >= 0 || (_[i] = s[i])); + return _; + })(s, o); + if (Object.getOwnPropertySymbols) { + var w = Object.getOwnPropertySymbols(s); + for (u = 0; u < w.length; u++) + ((i = w[u]), + o.indexOf(i) >= 0 || + (Object.prototype.propertyIsEnumerable.call(s, i) && (_[i] = s[i]))); + } + return _; + } + function ownKeys(s, o) { + var i = Object.keys(s); + if (Object.getOwnPropertySymbols) { + var u = Object.getOwnPropertySymbols(s); + (o && + (u = u.filter(function (o) { + return Object.getOwnPropertyDescriptor(s, o).enumerable; + })), + i.push.apply(i, u)); + } + return i; + } + function _objectSpread(s) { + for (var o = 1; o < arguments.length; o++) { + var i = null != arguments[o] ? arguments[o] : {}; + o % 2 + ? ownKeys(Object(i), !0).forEach(function (o) { + _defineProperty(s, o, i[o]); + }) + : Object.getOwnPropertyDescriptors + ? Object.defineProperties(s, Object.getOwnPropertyDescriptors(i)) + : ownKeys(Object(i)).forEach(function (o) { + Object.defineProperty(s, o, Object.getOwnPropertyDescriptor(i, o)); + }); + } + return s; + } + function _defineProperties(s, o) { + for (var i = 0; i < o.length; i++) { + var u = o[i]; + ((u.enumerable = u.enumerable || !1), + (u.configurable = !0), + 'value' in u && (u.writable = !0), + Object.defineProperty(s, u.key, u)); + } + } + function _setPrototypeOf(s, o) { + return ( + (_setPrototypeOf = + Object.setPrototypeOf || + function _setPrototypeOf(s, o) { + return ((s.__proto__ = o), s); + }), + _setPrototypeOf(s, o) + ); + } + function _createSuper(s) { + var o = (function _isNativeReflectConstruct() { + if ('undefined' == typeof Reflect || !Reflect.construct) return !1; + if (Reflect.construct.sham) return !1; + if ('function' == typeof Proxy) return !0; + try { + return ( + Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})), + !0 + ); + } catch (s) { + return !1; + } + })(); + return function _createSuperInternal() { + var i, + u = _getPrototypeOf(s); + if (o) { + var _ = _getPrototypeOf(this).constructor; + i = Reflect.construct(u, arguments, _); + } else i = u.apply(this, arguments); + return (function _possibleConstructorReturn(s, o) { + if (o && ('object' === _typeof(o) || 'function' == typeof o)) return o; + if (void 0 !== o) + throw new TypeError('Derived constructors may only return object or undefined'); + return _assertThisInitialized(s); + })(this, i); + }; + } + function _assertThisInitialized(s) { + if (void 0 === s) + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + return s; + } + function _getPrototypeOf(s) { + return ( + (_getPrototypeOf = Object.setPrototypeOf + ? Object.getPrototypeOf + : function _getPrototypeOf(s) { + return s.__proto__ || Object.getPrototypeOf(s); + }), + _getPrototypeOf(s) + ); + } + function _defineProperty(s, o, i) { + return ( + o in s + ? Object.defineProperty(s, o, { + value: i, + enumerable: !0, + configurable: !0, + writable: !0 + }) + : (s[o] = i), + s + ); + } + var x = (function (s) { + !(function _inherits(s, o) { + if ('function' != typeof o && null !== o) + throw new TypeError('Super expression must either be null or a function'); + ((s.prototype = Object.create(o && o.prototype, { + constructor: { value: s, writable: !0, configurable: !0 } + })), + Object.defineProperty(s, 'prototype', { writable: !1 }), + o && _setPrototypeOf(s, o)); + })(DebounceInput, s); + var o = _createSuper(DebounceInput); + function DebounceInput(s) { + var i; + (!(function _classCallCheck(s, o) { + if (!(s instanceof o)) throw new TypeError('Cannot call a class as a function'); + })(this, DebounceInput), + _defineProperty( + _assertThisInitialized((i = o.call(this, s))), + 'onChange', + function (s) { + s.persist(); + var o = i.state.value, + u = i.props.minLength; + i.setState({ value: s.target.value }, function () { + var _ = i.state.value; + _.length >= u + ? i.notify(s) + : o.length > _.length && + i.notify( + _objectSpread( + _objectSpread({}, s), + {}, + { + target: _objectSpread( + _objectSpread({}, s.target), + {}, + { value: '' } + ) + } + ) + ); + }); + } + ), + _defineProperty(_assertThisInitialized(i), 'onKeyDown', function (s) { + 'Enter' === s.key && i.forceNotify(s); + var o = i.props.onKeyDown; + o && (s.persist(), o(s)); + }), + _defineProperty(_assertThisInitialized(i), 'onBlur', function (s) { + i.forceNotify(s); + var o = i.props.onBlur; + o && (s.persist(), o(s)); + }), + _defineProperty(_assertThisInitialized(i), 'createNotifier', function (s) { + if (s < 0) + i.notify = function () { + return null; + }; + else if (0 === s) i.notify = i.doNotify; + else { + var o = (0, _.default)(function (s) { + ((i.isDebouncing = !1), i.doNotify(s)); + }, s); + ((i.notify = function (s) { + ((i.isDebouncing = !0), o(s)); + }), + (i.flush = function () { + return o.flush(); + }), + (i.cancel = function () { + ((i.isDebouncing = !1), o.cancel()); + })); + } + }), + _defineProperty(_assertThisInitialized(i), 'doNotify', function () { + i.props.onChange.apply(void 0, arguments); + }), + _defineProperty(_assertThisInitialized(i), 'forceNotify', function (s) { + var o = i.props.debounceTimeout; + if (i.isDebouncing || !(o > 0)) { + i.cancel && i.cancel(); + var u = i.state.value, + _ = i.props.minLength; + u.length >= _ + ? i.doNotify(s) + : i.doNotify( + _objectSpread( + _objectSpread({}, s), + {}, + { target: _objectSpread(_objectSpread({}, s.target), {}, { value: u }) } + ) + ); + } + }), + (i.isDebouncing = !1), + (i.state = { value: void 0 === s.value || null === s.value ? '' : s.value })); + var u = i.props.debounceTimeout; + return (i.createNotifier(u), i); + } + return ( + (function _createClass(s, o, i) { + return ( + o && _defineProperties(s.prototype, o), + i && _defineProperties(s, i), + Object.defineProperty(s, 'prototype', { writable: !1 }), + s + ); + })(DebounceInput, [ + { + key: 'componentDidUpdate', + value: function componentDidUpdate(s) { + if (!this.isDebouncing) { + var o = this.props, + i = o.value, + u = o.debounceTimeout, + _ = s.debounceTimeout, + w = s.value, + x = this.state.value; + (void 0 !== i && w !== i && x !== i && this.setState({ value: i }), + u !== _ && this.createNotifier(u)); + } + } + }, + { + key: 'componentWillUnmount', + value: function componentWillUnmount() { + this.flush && this.flush(); + } + }, + { + key: 'render', + value: function render() { + var s, + o, + i = this.props, + _ = i.element, + x = + (i.onChange, i.value, i.minLength, i.debounceTimeout, i.forceNotifyByEnter), + C = i.forceNotifyOnBlur, + j = i.onKeyDown, + L = i.onBlur, + B = i.inputRef, + $ = _objectWithoutProperties(i, w), + V = this.state.value; + ((s = x ? { onKeyDown: this.onKeyDown } : j ? { onKeyDown: j } : {}), + (o = C ? { onBlur: this.onBlur } : L ? { onBlur: L } : {})); + var U = B ? { ref: B } : {}; + return u.default.createElement( + _, + _objectSpread( + _objectSpread( + _objectSpread( + _objectSpread({}, $), + {}, + { onChange: this.onChange, value: V }, + s + ), + o + ), + U + ) + ); + } + } + ]), + DebounceInput + ); + })(u.default.PureComponent); + ((o.DebounceInput = x), + _defineProperty(x, 'defaultProps', { + element: 'input', + type: 'text', + onKeyDown: void 0, + onBlur: void 0, + value: void 0, + minLength: 0, + debounceTimeout: 100, + forceNotifyByEnter: !0, + forceNotifyOnBlur: !0, + inputRef: void 0 + })); + }, + 24677: (s, o, i) => { + 'use strict'; + var u = i(81214).DebounceInput; + ((u.DebounceInput = u), (s.exports = u)); + }, + 22551: (s, o, i) => { + 'use strict'; + var u = i(96540), + _ = i(69982); + function p(s) { + for ( + var o = 'https://reactjs.org/docs/error-decoder.html?invariant=' + s, i = 1; + i < arguments.length; + i++ + ) + o += '&args[]=' + encodeURIComponent(arguments[i]); + return ( + 'Minified React error #' + + s + + '; visit ' + + o + + ' for the full message or use the non-minified dev environment for full errors and additional helpful warnings.' + ); + } + var w = new Set(), + x = {}; + function fa(s, o) { + (ha(s, o), ha(s + 'Capture', o)); + } + function ha(s, o) { + for (x[s] = o, s = 0; s < o.length; s++) w.add(o[s]); + } + var C = !( + 'undefined' == typeof window || + void 0 === window.document || + void 0 === window.document.createElement + ), + j = Object.prototype.hasOwnProperty, + L = + /^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/, + B = {}, + $ = {}; + function v(s, o, i, u, _, w, x) { + ((this.acceptsBooleans = 2 === o || 3 === o || 4 === o), + (this.attributeName = u), + (this.attributeNamespace = _), + (this.mustUseProperty = i), + (this.propertyName = s), + (this.type = o), + (this.sanitizeURL = w), + (this.removeEmptyString = x)); + } + var V = {}; + ('children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style' + .split(' ') + .forEach(function (s) { + V[s] = new v(s, 0, !1, s, null, !1, !1); + }), + [ + ['acceptCharset', 'accept-charset'], + ['className', 'class'], + ['htmlFor', 'for'], + ['httpEquiv', 'http-equiv'] + ].forEach(function (s) { + var o = s[0]; + V[o] = new v(o, 1, !1, s[1], null, !1, !1); + }), + ['contentEditable', 'draggable', 'spellCheck', 'value'].forEach(function (s) { + V[s] = new v(s, 2, !1, s.toLowerCase(), null, !1, !1); + }), + ['autoReverse', 'externalResourcesRequired', 'focusable', 'preserveAlpha'].forEach( + function (s) { + V[s] = new v(s, 2, !1, s, null, !1, !1); + } + ), + 'allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope' + .split(' ') + .forEach(function (s) { + V[s] = new v(s, 3, !1, s.toLowerCase(), null, !1, !1); + }), + ['checked', 'multiple', 'muted', 'selected'].forEach(function (s) { + V[s] = new v(s, 3, !0, s, null, !1, !1); + }), + ['capture', 'download'].forEach(function (s) { + V[s] = new v(s, 4, !1, s, null, !1, !1); + }), + ['cols', 'rows', 'size', 'span'].forEach(function (s) { + V[s] = new v(s, 6, !1, s, null, !1, !1); + }), + ['rowSpan', 'start'].forEach(function (s) { + V[s] = new v(s, 5, !1, s.toLowerCase(), null, !1, !1); + })); + var U = /[\-:]([a-z])/g; + function sa(s) { + return s[1].toUpperCase(); + } + function ta(s, o, i, u) { + var _ = V.hasOwnProperty(o) ? V[o] : null; + (null !== _ + ? 0 !== _.type + : u || + !(2 < o.length) || + ('o' !== o[0] && 'O' !== o[0]) || + ('n' !== o[1] && 'N' !== o[1])) && + ((function qa(s, o, i, u) { + if ( + null == o || + (function pa(s, o, i, u) { + if (null !== i && 0 === i.type) return !1; + switch (typeof o) { + case 'function': + case 'symbol': + return !0; + case 'boolean': + return ( + !u && + (null !== i + ? !i.acceptsBooleans + : 'data-' !== (s = s.toLowerCase().slice(0, 5)) && 'aria-' !== s) + ); + default: + return !1; + } + })(s, o, i, u) + ) + return !0; + if (u) return !1; + if (null !== i) + switch (i.type) { + case 3: + return !o; + case 4: + return !1 === o; + case 5: + return isNaN(o); + case 6: + return isNaN(o) || 1 > o; + } + return !1; + })(o, i, _, u) && (i = null), + u || null === _ + ? (function oa(s) { + return ( + !!j.call($, s) || + (!j.call(B, s) && (L.test(s) ? ($[s] = !0) : ((B[s] = !0), !1))) + ); + })(o) && (null === i ? s.removeAttribute(o) : s.setAttribute(o, '' + i)) + : _.mustUseProperty + ? (s[_.propertyName] = null === i ? 3 !== _.type && '' : i) + : ((o = _.attributeName), + (u = _.attributeNamespace), + null === i + ? s.removeAttribute(o) + : ((i = 3 === (_ = _.type) || (4 === _ && !0 === i) ? '' : '' + i), + u ? s.setAttributeNS(u, o, i) : s.setAttribute(o, i)))); + } + ('accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height' + .split(' ') + .forEach(function (s) { + var o = s.replace(U, sa); + V[o] = new v(o, 1, !1, s, null, !1, !1); + }), + 'xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type' + .split(' ') + .forEach(function (s) { + var o = s.replace(U, sa); + V[o] = new v(o, 1, !1, s, 'http://www.w3.org/1999/xlink', !1, !1); + }), + ['xml:base', 'xml:lang', 'xml:space'].forEach(function (s) { + var o = s.replace(U, sa); + V[o] = new v(o, 1, !1, s, 'http://www.w3.org/XML/1998/namespace', !1, !1); + }), + ['tabIndex', 'crossOrigin'].forEach(function (s) { + V[s] = new v(s, 1, !1, s.toLowerCase(), null, !1, !1); + }), + (V.xlinkHref = new v( + 'xlinkHref', + 1, + !1, + 'xlink:href', + 'http://www.w3.org/1999/xlink', + !0, + !1 + )), + ['src', 'href', 'action', 'formAction'].forEach(function (s) { + V[s] = new v(s, 1, !1, s.toLowerCase(), null, !0, !0); + })); + var z = u.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + Y = Symbol.for('react.element'), + Z = Symbol.for('react.portal'), + ee = Symbol.for('react.fragment'), + ie = Symbol.for('react.strict_mode'), + ae = Symbol.for('react.profiler'), + le = Symbol.for('react.provider'), + ce = Symbol.for('react.context'), + pe = Symbol.for('react.forward_ref'), + de = Symbol.for('react.suspense'), + fe = Symbol.for('react.suspense_list'), + ye = Symbol.for('react.memo'), + be = Symbol.for('react.lazy'); + (Symbol.for('react.scope'), Symbol.for('react.debug_trace_mode')); + var _e = Symbol.for('react.offscreen'); + (Symbol.for('react.legacy_hidden'), + Symbol.for('react.cache'), + Symbol.for('react.tracing_marker')); + var we = Symbol.iterator; + function Ka(s) { + return null === s || 'object' != typeof s + ? null + : 'function' == typeof (s = (we && s[we]) || s['@@iterator']) + ? s + : null; + } + var Se, + xe = Object.assign; + function Ma(s) { + if (void 0 === Se) + try { + throw Error(); + } catch (s) { + var o = s.stack.trim().match(/\n( *(at )?)/); + Se = (o && o[1]) || ''; + } + return '\n' + Se + s; + } + var Pe = !1; + function Oa(s, o) { + if (!s || Pe) return ''; + Pe = !0; + var i = Error.prepareStackTrace; + Error.prepareStackTrace = void 0; + try { + if (o) + if ( + ((o = function () { + throw Error(); + }), + Object.defineProperty(o.prototype, 'props', { + set: function () { + throw Error(); + } + }), + 'object' == typeof Reflect && Reflect.construct) + ) { + try { + Reflect.construct(o, []); + } catch (s) { + var u = s; + } + Reflect.construct(s, [], o); + } else { + try { + o.call(); + } catch (s) { + u = s; + } + s.call(o.prototype); + } + else { + try { + throw Error(); + } catch (s) { + u = s; + } + s(); + } + } catch (o) { + if (o && u && 'string' == typeof o.stack) { + for ( + var _ = o.stack.split('\n'), + w = u.stack.split('\n'), + x = _.length - 1, + C = w.length - 1; + 1 <= x && 0 <= C && _[x] !== w[C]; + ) + C--; + for (; 1 <= x && 0 <= C; x--, C--) + if (_[x] !== w[C]) { + if (1 !== x || 1 !== C) + do { + if ((x--, 0 > --C || _[x] !== w[C])) { + var j = '\n' + _[x].replace(' at new ', ' at '); + return ( + s.displayName && + j.includes('') && + (j = j.replace('', s.displayName)), + j + ); + } + } while (1 <= x && 0 <= C); + break; + } + } + } finally { + ((Pe = !1), (Error.prepareStackTrace = i)); + } + return (s = s ? s.displayName || s.name : '') ? Ma(s) : ''; + } + function Pa(s) { + switch (s.tag) { + case 5: + return Ma(s.type); + case 16: + return Ma('Lazy'); + case 13: + return Ma('Suspense'); + case 19: + return Ma('SuspenseList'); + case 0: + case 2: + case 15: + return (s = Oa(s.type, !1)); + case 11: + return (s = Oa(s.type.render, !1)); + case 1: + return (s = Oa(s.type, !0)); + default: + return ''; + } + } + function Qa(s) { + if (null == s) return null; + if ('function' == typeof s) return s.displayName || s.name || null; + if ('string' == typeof s) return s; + switch (s) { + case ee: + return 'Fragment'; + case Z: + return 'Portal'; + case ae: + return 'Profiler'; + case ie: + return 'StrictMode'; + case de: + return 'Suspense'; + case fe: + return 'SuspenseList'; + } + if ('object' == typeof s) + switch (s.$$typeof) { + case ce: + return (s.displayName || 'Context') + '.Consumer'; + case le: + return (s._context.displayName || 'Context') + '.Provider'; + case pe: + var o = s.render; + return ( + (s = s.displayName) || + (s = + '' !== (s = o.displayName || o.name || '') + ? 'ForwardRef(' + s + ')' + : 'ForwardRef'), + s + ); + case ye: + return null !== (o = s.displayName || null) ? o : Qa(s.type) || 'Memo'; + case be: + ((o = s._payload), (s = s._init)); + try { + return Qa(s(o)); + } catch (s) {} + } + return null; + } + function Ra(s) { + var o = s.type; + switch (s.tag) { + case 24: + return 'Cache'; + case 9: + return (o.displayName || 'Context') + '.Consumer'; + case 10: + return (o._context.displayName || 'Context') + '.Provider'; + case 18: + return 'DehydratedFragment'; + case 11: + return ( + (s = (s = o.render).displayName || s.name || ''), + o.displayName || ('' !== s ? 'ForwardRef(' + s + ')' : 'ForwardRef') + ); + case 7: + return 'Fragment'; + case 5: + return o; + case 4: + return 'Portal'; + case 3: + return 'Root'; + case 6: + return 'Text'; + case 16: + return Qa(o); + case 8: + return o === ie ? 'StrictMode' : 'Mode'; + case 22: + return 'Offscreen'; + case 12: + return 'Profiler'; + case 21: + return 'Scope'; + case 13: + return 'Suspense'; + case 19: + return 'SuspenseList'; + case 25: + return 'TracingMarker'; + case 1: + case 0: + case 17: + case 2: + case 14: + case 15: + if ('function' == typeof o) return o.displayName || o.name || null; + if ('string' == typeof o) return o; + } + return null; + } + function Sa(s) { + switch (typeof s) { + case 'boolean': + case 'number': + case 'string': + case 'undefined': + case 'object': + return s; + default: + return ''; + } + } + function Ta(s) { + var o = s.type; + return ( + (s = s.nodeName) && 'input' === s.toLowerCase() && ('checkbox' === o || 'radio' === o) + ); + } + function Va(s) { + s._valueTracker || + (s._valueTracker = (function Ua(s) { + var o = Ta(s) ? 'checked' : 'value', + i = Object.getOwnPropertyDescriptor(s.constructor.prototype, o), + u = '' + s[o]; + if ( + !s.hasOwnProperty(o) && + void 0 !== i && + 'function' == typeof i.get && + 'function' == typeof i.set + ) { + var _ = i.get, + w = i.set; + return ( + Object.defineProperty(s, o, { + configurable: !0, + get: function () { + return _.call(this); + }, + set: function (s) { + ((u = '' + s), w.call(this, s)); + } + }), + Object.defineProperty(s, o, { enumerable: i.enumerable }), + { + getValue: function () { + return u; + }, + setValue: function (s) { + u = '' + s; + }, + stopTracking: function () { + ((s._valueTracker = null), delete s[o]); + } + } + ); + } + })(s)); + } + function Wa(s) { + if (!s) return !1; + var o = s._valueTracker; + if (!o) return !0; + var i = o.getValue(), + u = ''; + return ( + s && (u = Ta(s) ? (s.checked ? 'true' : 'false') : s.value), + (s = u) !== i && (o.setValue(s), !0) + ); + } + function Xa(s) { + if (void 0 === (s = s || ('undefined' != typeof document ? document : void 0))) + return null; + try { + return s.activeElement || s.body; + } catch (o) { + return s.body; + } + } + function Ya(s, o) { + var i = o.checked; + return xe({}, o, { + defaultChecked: void 0, + defaultValue: void 0, + value: void 0, + checked: null != i ? i : s._wrapperState.initialChecked + }); + } + function Za(s, o) { + var i = null == o.defaultValue ? '' : o.defaultValue, + u = null != o.checked ? o.checked : o.defaultChecked; + ((i = Sa(null != o.value ? o.value : i)), + (s._wrapperState = { + initialChecked: u, + initialValue: i, + controlled: + 'checkbox' === o.type || 'radio' === o.type ? null != o.checked : null != o.value + })); + } + function ab(s, o) { + null != (o = o.checked) && ta(s, 'checked', o, !1); + } + function bb(s, o) { + ab(s, o); + var i = Sa(o.value), + u = o.type; + if (null != i) + 'number' === u + ? ((0 === i && '' === s.value) || s.value != i) && (s.value = '' + i) + : s.value !== '' + i && (s.value = '' + i); + else if ('submit' === u || 'reset' === u) return void s.removeAttribute('value'); + (o.hasOwnProperty('value') + ? cb(s, o.type, i) + : o.hasOwnProperty('defaultValue') && cb(s, o.type, Sa(o.defaultValue)), + null == o.checked && + null != o.defaultChecked && + (s.defaultChecked = !!o.defaultChecked)); + } + function db(s, o, i) { + if (o.hasOwnProperty('value') || o.hasOwnProperty('defaultValue')) { + var u = o.type; + if (!(('submit' !== u && 'reset' !== u) || (void 0 !== o.value && null !== o.value))) + return; + ((o = '' + s._wrapperState.initialValue), + i || o === s.value || (s.value = o), + (s.defaultValue = o)); + } + ('' !== (i = s.name) && (s.name = ''), + (s.defaultChecked = !!s._wrapperState.initialChecked), + '' !== i && (s.name = i)); + } + function cb(s, o, i) { + ('number' === o && Xa(s.ownerDocument) === s) || + (null == i + ? (s.defaultValue = '' + s._wrapperState.initialValue) + : s.defaultValue !== '' + i && (s.defaultValue = '' + i)); + } + var Te = Array.isArray; + function fb(s, o, i, u) { + if (((s = s.options), o)) { + o = {}; + for (var _ = 0; _ < i.length; _++) o['$' + i[_]] = !0; + for (i = 0; i < s.length; i++) + ((_ = o.hasOwnProperty('$' + s[i].value)), + s[i].selected !== _ && (s[i].selected = _), + _ && u && (s[i].defaultSelected = !0)); + } else { + for (i = '' + Sa(i), o = null, _ = 0; _ < s.length; _++) { + if (s[_].value === i) + return ((s[_].selected = !0), void (u && (s[_].defaultSelected = !0))); + null !== o || s[_].disabled || (o = s[_]); + } + null !== o && (o.selected = !0); + } + } + function gb(s, o) { + if (null != o.dangerouslySetInnerHTML) throw Error(p(91)); + return xe({}, o, { + value: void 0, + defaultValue: void 0, + children: '' + s._wrapperState.initialValue + }); + } + function hb(s, o) { + var i = o.value; + if (null == i) { + if (((i = o.children), (o = o.defaultValue), null != i)) { + if (null != o) throw Error(p(92)); + if (Te(i)) { + if (1 < i.length) throw Error(p(93)); + i = i[0]; + } + o = i; + } + (null == o && (o = ''), (i = o)); + } + s._wrapperState = { initialValue: Sa(i) }; + } + function ib(s, o) { + var i = Sa(o.value), + u = Sa(o.defaultValue); + (null != i && + ((i = '' + i) !== s.value && (s.value = i), + null == o.defaultValue && s.defaultValue !== i && (s.defaultValue = i)), + null != u && (s.defaultValue = '' + u)); + } + function jb(s) { + var o = s.textContent; + o === s._wrapperState.initialValue && '' !== o && null !== o && (s.value = o); + } + function kb(s) { + switch (s) { + case 'svg': + return 'http://www.w3.org/2000/svg'; + case 'math': + return 'http://www.w3.org/1998/Math/MathML'; + default: + return 'http://www.w3.org/1999/xhtml'; + } + } + function lb(s, o) { + return null == s || 'http://www.w3.org/1999/xhtml' === s + ? kb(o) + : 'http://www.w3.org/2000/svg' === s && 'foreignObject' === o + ? 'http://www.w3.org/1999/xhtml' + : s; + } + var Re, + qe, + $e = + ((qe = function (s, o) { + if ('http://www.w3.org/2000/svg' !== s.namespaceURI || 'innerHTML' in s) + s.innerHTML = o; + else { + for ( + (Re = Re || document.createElement('div')).innerHTML = + '' + o.valueOf().toString() + '', + o = Re.firstChild; + s.firstChild; + ) + s.removeChild(s.firstChild); + for (; o.firstChild; ) s.appendChild(o.firstChild); + } + }), + 'undefined' != typeof MSApp && MSApp.execUnsafeLocalFunction + ? function (s, o, i, u) { + MSApp.execUnsafeLocalFunction(function () { + return qe(s, o); + }); + } + : qe); + function ob(s, o) { + if (o) { + var i = s.firstChild; + if (i && i === s.lastChild && 3 === i.nodeType) return void (i.nodeValue = o); + } + s.textContent = o; + } + var ze = { + animationIterationCount: !0, + aspectRatio: !0, + borderImageOutset: !0, + borderImageSlice: !0, + borderImageWidth: !0, + boxFlex: !0, + boxFlexGroup: !0, + boxOrdinalGroup: !0, + columnCount: !0, + columns: !0, + flex: !0, + flexGrow: !0, + flexPositive: !0, + flexShrink: !0, + flexNegative: !0, + flexOrder: !0, + gridArea: !0, + gridRow: !0, + gridRowEnd: !0, + gridRowSpan: !0, + gridRowStart: !0, + gridColumn: !0, + gridColumnEnd: !0, + gridColumnSpan: !0, + gridColumnStart: !0, + fontWeight: !0, + lineClamp: !0, + lineHeight: !0, + opacity: !0, + order: !0, + orphans: !0, + tabSize: !0, + widows: !0, + zIndex: !0, + zoom: !0, + fillOpacity: !0, + floodOpacity: !0, + stopOpacity: !0, + strokeDasharray: !0, + strokeDashoffset: !0, + strokeMiterlimit: !0, + strokeOpacity: !0, + strokeWidth: !0 + }, + We = ['Webkit', 'ms', 'Moz', 'O']; + function rb(s, o, i) { + return null == o || 'boolean' == typeof o || '' === o + ? '' + : i || 'number' != typeof o || 0 === o || (ze.hasOwnProperty(s) && ze[s]) + ? ('' + o).trim() + : o + 'px'; + } + function sb(s, o) { + for (var i in ((s = s.style), o)) + if (o.hasOwnProperty(i)) { + var u = 0 === i.indexOf('--'), + _ = rb(i, o[i], u); + ('float' === i && (i = 'cssFloat'), u ? s.setProperty(i, _) : (s[i] = _)); + } + } + Object.keys(ze).forEach(function (s) { + We.forEach(function (o) { + ((o = o + s.charAt(0).toUpperCase() + s.substring(1)), (ze[o] = ze[s])); + }); + }); + var He = xe( + { menuitem: !0 }, + { + area: !0, + base: !0, + br: !0, + col: !0, + embed: !0, + hr: !0, + img: !0, + input: !0, + keygen: !0, + link: !0, + meta: !0, + param: !0, + source: !0, + track: !0, + wbr: !0 + } + ); + function ub(s, o) { + if (o) { + if (He[s] && (null != o.children || null != o.dangerouslySetInnerHTML)) + throw Error(p(137, s)); + if (null != o.dangerouslySetInnerHTML) { + if (null != o.children) throw Error(p(60)); + if ( + 'object' != typeof o.dangerouslySetInnerHTML || + !('__html' in o.dangerouslySetInnerHTML) + ) + throw Error(p(61)); + } + if (null != o.style && 'object' != typeof o.style) throw Error(p(62)); + } + } + function vb(s, o) { + if (-1 === s.indexOf('-')) return 'string' == typeof o.is; + switch (s) { + case 'annotation-xml': + case 'color-profile': + case 'font-face': + case 'font-face-src': + case 'font-face-uri': + case 'font-face-format': + case 'font-face-name': + case 'missing-glyph': + return !1; + default: + return !0; + } + } + var Ye = null; + function xb(s) { + return ( + (s = s.target || s.srcElement || window).correspondingUseElement && + (s = s.correspondingUseElement), + 3 === s.nodeType ? s.parentNode : s + ); + } + var Xe = null, + Qe = null, + et = null; + function Bb(s) { + if ((s = Cb(s))) { + if ('function' != typeof Xe) throw Error(p(280)); + var o = s.stateNode; + o && ((o = Db(o)), Xe(s.stateNode, s.type, o)); + } + } + function Eb(s) { + Qe ? (et ? et.push(s) : (et = [s])) : (Qe = s); + } + function Fb() { + if (Qe) { + var s = Qe, + o = et; + if (((et = Qe = null), Bb(s), o)) for (s = 0; s < o.length; s++) Bb(o[s]); + } + } + function Gb(s, o) { + return s(o); + } + function Hb() {} + var tt = !1; + function Jb(s, o, i) { + if (tt) return s(o, i); + tt = !0; + try { + return Gb(s, o, i); + } finally { + ((tt = !1), (null !== Qe || null !== et) && (Hb(), Fb())); + } + } + function Kb(s, o) { + var i = s.stateNode; + if (null === i) return null; + var u = Db(i); + if (null === u) return null; + i = u[o]; + e: switch (o) { + case 'onClick': + case 'onClickCapture': + case 'onDoubleClick': + case 'onDoubleClickCapture': + case 'onMouseDown': + case 'onMouseDownCapture': + case 'onMouseMove': + case 'onMouseMoveCapture': + case 'onMouseUp': + case 'onMouseUpCapture': + case 'onMouseEnter': + ((u = !u.disabled) || + (u = !( + 'button' === (s = s.type) || + 'input' === s || + 'select' === s || + 'textarea' === s + )), + (s = !u)); + break e; + default: + s = !1; + } + if (s) return null; + if (i && 'function' != typeof i) throw Error(p(231, o, typeof i)); + return i; + } + var rt = !1; + if (C) + try { + var nt = {}; + (Object.defineProperty(nt, 'passive', { + get: function () { + rt = !0; + } + }), + window.addEventListener('test', nt, nt), + window.removeEventListener('test', nt, nt)); + } catch (qe) { + rt = !1; + } + function Nb(s, o, i, u, _, w, x, C, j) { + var L = Array.prototype.slice.call(arguments, 3); + try { + o.apply(i, L); + } catch (s) { + this.onError(s); + } + } + var st = !1, + ot = null, + it = !1, + at = null, + lt = { + onError: function (s) { + ((st = !0), (ot = s)); + } + }; + function Tb(s, o, i, u, _, w, x, C, j) { + ((st = !1), (ot = null), Nb.apply(lt, arguments)); + } + function Vb(s) { + var o = s, + i = s; + if (s.alternate) for (; o.return; ) o = o.return; + else { + s = o; + do { + (!!(4098 & (o = s).flags) && (i = o.return), (s = o.return)); + } while (s); + } + return 3 === o.tag ? i : null; + } + function Wb(s) { + if (13 === s.tag) { + var o = s.memoizedState; + if ((null === o && null !== (s = s.alternate) && (o = s.memoizedState), null !== o)) + return o.dehydrated; + } + return null; + } + function Xb(s) { + if (Vb(s) !== s) throw Error(p(188)); + } + function Zb(s) { + return null !== + (s = (function Yb(s) { + var o = s.alternate; + if (!o) { + if (null === (o = Vb(s))) throw Error(p(188)); + return o !== s ? null : s; + } + for (var i = s, u = o; ; ) { + var _ = i.return; + if (null === _) break; + var w = _.alternate; + if (null === w) { + if (null !== (u = _.return)) { + i = u; + continue; + } + break; + } + if (_.child === w.child) { + for (w = _.child; w; ) { + if (w === i) return (Xb(_), s); + if (w === u) return (Xb(_), o); + w = w.sibling; + } + throw Error(p(188)); + } + if (i.return !== u.return) ((i = _), (u = w)); + else { + for (var x = !1, C = _.child; C; ) { + if (C === i) { + ((x = !0), (i = _), (u = w)); + break; + } + if (C === u) { + ((x = !0), (u = _), (i = w)); + break; + } + C = C.sibling; + } + if (!x) { + for (C = w.child; C; ) { + if (C === i) { + ((x = !0), (i = w), (u = _)); + break; + } + if (C === u) { + ((x = !0), (u = w), (i = _)); + break; + } + C = C.sibling; + } + if (!x) throw Error(p(189)); + } + } + if (i.alternate !== u) throw Error(p(190)); + } + if (3 !== i.tag) throw Error(p(188)); + return i.stateNode.current === i ? s : o; + })(s)) + ? $b(s) + : null; + } + function $b(s) { + if (5 === s.tag || 6 === s.tag) return s; + for (s = s.child; null !== s; ) { + var o = $b(s); + if (null !== o) return o; + s = s.sibling; + } + return null; + } + var ct = _.unstable_scheduleCallback, + ut = _.unstable_cancelCallback, + pt = _.unstable_shouldYield, + ht = _.unstable_requestPaint, + dt = _.unstable_now, + mt = _.unstable_getCurrentPriorityLevel, + gt = _.unstable_ImmediatePriority, + yt = _.unstable_UserBlockingPriority, + vt = _.unstable_NormalPriority, + bt = _.unstable_LowPriority, + _t = _.unstable_IdlePriority, + Et = null, + wt = null; + var St = Math.clz32 + ? Math.clz32 + : function nc(s) { + return ((s >>>= 0), 0 === s ? 32 : (31 - ((xt(s) / kt) | 0)) | 0); + }, + xt = Math.log, + kt = Math.LN2; + var Ct = 64, + Ot = 4194304; + function tc(s) { + switch (s & -s) { + case 1: + return 1; + case 2: + return 2; + case 4: + return 4; + case 8: + return 8; + case 16: + return 16; + case 32: + return 32; + case 64: + case 128: + case 256: + case 512: + case 1024: + case 2048: + case 4096: + case 8192: + case 16384: + case 32768: + case 65536: + case 131072: + case 262144: + case 524288: + case 1048576: + case 2097152: + return 4194240 & s; + case 4194304: + case 8388608: + case 16777216: + case 33554432: + case 67108864: + return 130023424 & s; + case 134217728: + return 134217728; + case 268435456: + return 268435456; + case 536870912: + return 536870912; + case 1073741824: + return 1073741824; + default: + return s; + } + } + function uc(s, o) { + var i = s.pendingLanes; + if (0 === i) return 0; + var u = 0, + _ = s.suspendedLanes, + w = s.pingedLanes, + x = 268435455 & i; + if (0 !== x) { + var C = x & ~_; + 0 !== C ? (u = tc(C)) : 0 !== (w &= x) && (u = tc(w)); + } else 0 !== (x = i & ~_) ? (u = tc(x)) : 0 !== w && (u = tc(w)); + if (0 === u) return 0; + if ( + 0 !== o && + o !== u && + !(o & _) && + ((_ = u & -u) >= (w = o & -o) || (16 === _ && 4194240 & w)) + ) + return o; + if ((4 & u && (u |= 16 & i), 0 !== (o = s.entangledLanes))) + for (s = s.entanglements, o &= u; 0 < o; ) + ((_ = 1 << (i = 31 - St(o))), (u |= s[i]), (o &= ~_)); + return u; + } + function vc(s, o) { + switch (s) { + case 1: + case 2: + case 4: + return o + 250; + case 8: + case 16: + case 32: + case 64: + case 128: + case 256: + case 512: + case 1024: + case 2048: + case 4096: + case 8192: + case 16384: + case 32768: + case 65536: + case 131072: + case 262144: + case 524288: + case 1048576: + case 2097152: + return o + 5e3; + default: + return -1; + } + } + function xc(s) { + return 0 !== (s = -1073741825 & s.pendingLanes) ? s : 1073741824 & s ? 1073741824 : 0; + } + function yc() { + var s = Ct; + return (!(4194240 & (Ct <<= 1)) && (Ct = 64), s); + } + function zc(s) { + for (var o = [], i = 0; 31 > i; i++) o.push(s); + return o; + } + function Ac(s, o, i) { + ((s.pendingLanes |= o), + 536870912 !== o && ((s.suspendedLanes = 0), (s.pingedLanes = 0)), + ((s = s.eventTimes)[(o = 31 - St(o))] = i)); + } + function Cc(s, o) { + var i = (s.entangledLanes |= o); + for (s = s.entanglements; i; ) { + var u = 31 - St(i), + _ = 1 << u; + ((_ & o) | (s[u] & o) && (s[u] |= o), (i &= ~_)); + } + } + var At = 0; + function Dc(s) { + return 1 < (s &= -s) ? (4 < s ? (268435455 & s ? 16 : 536870912) : 4) : 1; + } + var jt, + It, + Pt, + Mt, + Tt, + Nt = !1, + Rt = [], + Dt = null, + Lt = null, + Bt = null, + Ft = new Map(), + qt = new Map(), + $t = [], + Vt = + 'mousedown mouseup touchcancel touchend touchstart auxclick dblclick pointercancel pointerdown pointerup dragend dragstart drop compositionend compositionstart keydown keypress keyup input textInput copy cut paste click change contextmenu reset submit'.split( + ' ' + ); + function Sc(s, o) { + switch (s) { + case 'focusin': + case 'focusout': + Dt = null; + break; + case 'dragenter': + case 'dragleave': + Lt = null; + break; + case 'mouseover': + case 'mouseout': + Bt = null; + break; + case 'pointerover': + case 'pointerout': + Ft.delete(o.pointerId); + break; + case 'gotpointercapture': + case 'lostpointercapture': + qt.delete(o.pointerId); + } + } + function Tc(s, o, i, u, _, w) { + return null === s || s.nativeEvent !== w + ? ((s = { + blockedOn: o, + domEventName: i, + eventSystemFlags: u, + nativeEvent: w, + targetContainers: [_] + }), + null !== o && null !== (o = Cb(o)) && It(o), + s) + : ((s.eventSystemFlags |= u), + (o = s.targetContainers), + null !== _ && -1 === o.indexOf(_) && o.push(_), + s); + } + function Vc(s) { + var o = Wc(s.target); + if (null !== o) { + var i = Vb(o); + if (null !== i) + if (13 === (o = i.tag)) { + if (null !== (o = Wb(i))) + return ( + (s.blockedOn = o), + void Tt(s.priority, function () { + Pt(i); + }) + ); + } else if (3 === o && i.stateNode.current.memoizedState.isDehydrated) + return void (s.blockedOn = 3 === i.tag ? i.stateNode.containerInfo : null); + } + s.blockedOn = null; + } + function Xc(s) { + if (null !== s.blockedOn) return !1; + for (var o = s.targetContainers; 0 < o.length; ) { + var i = Yc(s.domEventName, s.eventSystemFlags, o[0], s.nativeEvent); + if (null !== i) return (null !== (o = Cb(i)) && It(o), (s.blockedOn = i), !1); + var u = new (i = s.nativeEvent).constructor(i.type, i); + ((Ye = u), i.target.dispatchEvent(u), (Ye = null), o.shift()); + } + return !0; + } + function Zc(s, o, i) { + Xc(s) && i.delete(o); + } + function $c() { + ((Nt = !1), + null !== Dt && Xc(Dt) && (Dt = null), + null !== Lt && Xc(Lt) && (Lt = null), + null !== Bt && Xc(Bt) && (Bt = null), + Ft.forEach(Zc), + qt.forEach(Zc)); + } + function ad(s, o) { + s.blockedOn === o && + ((s.blockedOn = null), + Nt || ((Nt = !0), _.unstable_scheduleCallback(_.unstable_NormalPriority, $c))); + } + function bd(s) { + function b(o) { + return ad(o, s); + } + if (0 < Rt.length) { + ad(Rt[0], s); + for (var o = 1; o < Rt.length; o++) { + var i = Rt[o]; + i.blockedOn === s && (i.blockedOn = null); + } + } + for ( + null !== Dt && ad(Dt, s), + null !== Lt && ad(Lt, s), + null !== Bt && ad(Bt, s), + Ft.forEach(b), + qt.forEach(b), + o = 0; + o < $t.length; + o++ + ) + (i = $t[o]).blockedOn === s && (i.blockedOn = null); + for (; 0 < $t.length && null === (o = $t[0]).blockedOn; ) + (Vc(o), null === o.blockedOn && $t.shift()); + } + var Ut = z.ReactCurrentBatchConfig, + zt = !0; + function ed(s, o, i, u) { + var _ = At, + w = Ut.transition; + Ut.transition = null; + try { + ((At = 1), fd(s, o, i, u)); + } finally { + ((At = _), (Ut.transition = w)); + } + } + function gd(s, o, i, u) { + var _ = At, + w = Ut.transition; + Ut.transition = null; + try { + ((At = 4), fd(s, o, i, u)); + } finally { + ((At = _), (Ut.transition = w)); + } + } + function fd(s, o, i, u) { + if (zt) { + var _ = Yc(s, o, i, u); + if (null === _) (hd(s, o, u, Wt, i), Sc(s, u)); + else if ( + (function Uc(s, o, i, u, _) { + switch (o) { + case 'focusin': + return ((Dt = Tc(Dt, s, o, i, u, _)), !0); + case 'dragenter': + return ((Lt = Tc(Lt, s, o, i, u, _)), !0); + case 'mouseover': + return ((Bt = Tc(Bt, s, o, i, u, _)), !0); + case 'pointerover': + var w = _.pointerId; + return (Ft.set(w, Tc(Ft.get(w) || null, s, o, i, u, _)), !0); + case 'gotpointercapture': + return ( + (w = _.pointerId), + qt.set(w, Tc(qt.get(w) || null, s, o, i, u, _)), + !0 + ); + } + return !1; + })(_, s, o, i, u) + ) + u.stopPropagation(); + else if ((Sc(s, u), 4 & o && -1 < Vt.indexOf(s))) { + for (; null !== _; ) { + var w = Cb(_); + if ( + (null !== w && jt(w), + null === (w = Yc(s, o, i, u)) && hd(s, o, u, Wt, i), + w === _) + ) + break; + _ = w; + } + null !== _ && u.stopPropagation(); + } else hd(s, o, u, null, i); + } + } + var Wt = null; + function Yc(s, o, i, u) { + if (((Wt = null), null !== (s = Wc((s = xb(u)))))) + if (null === (o = Vb(s))) s = null; + else if (13 === (i = o.tag)) { + if (null !== (s = Wb(o))) return s; + s = null; + } else if (3 === i) { + if (o.stateNode.current.memoizedState.isDehydrated) + return 3 === o.tag ? o.stateNode.containerInfo : null; + s = null; + } else o !== s && (s = null); + return ((Wt = s), null); + } + function jd(s) { + switch (s) { + case 'cancel': + case 'click': + case 'close': + case 'contextmenu': + case 'copy': + case 'cut': + case 'auxclick': + case 'dblclick': + case 'dragend': + case 'dragstart': + case 'drop': + case 'focusin': + case 'focusout': + case 'input': + case 'invalid': + case 'keydown': + case 'keypress': + case 'keyup': + case 'mousedown': + case 'mouseup': + case 'paste': + case 'pause': + case 'play': + case 'pointercancel': + case 'pointerdown': + case 'pointerup': + case 'ratechange': + case 'reset': + case 'resize': + case 'seeked': + case 'submit': + case 'touchcancel': + case 'touchend': + case 'touchstart': + case 'volumechange': + case 'change': + case 'selectionchange': + case 'textInput': + case 'compositionstart': + case 'compositionend': + case 'compositionupdate': + case 'beforeblur': + case 'afterblur': + case 'beforeinput': + case 'blur': + case 'fullscreenchange': + case 'focus': + case 'hashchange': + case 'popstate': + case 'select': + case 'selectstart': + return 1; + case 'drag': + case 'dragenter': + case 'dragexit': + case 'dragleave': + case 'dragover': + case 'mousemove': + case 'mouseout': + case 'mouseover': + case 'pointermove': + case 'pointerout': + case 'pointerover': + case 'scroll': + case 'toggle': + case 'touchmove': + case 'wheel': + case 'mouseenter': + case 'mouseleave': + case 'pointerenter': + case 'pointerleave': + return 4; + case 'message': + switch (mt()) { + case gt: + return 1; + case yt: + return 4; + case vt: + case bt: + return 16; + case _t: + return 536870912; + default: + return 16; + } + default: + return 16; + } + } + var Kt = null, + Ht = null, + Jt = null; + function nd() { + if (Jt) return Jt; + var s, + o, + i = Ht, + u = i.length, + _ = 'value' in Kt ? Kt.value : Kt.textContent, + w = _.length; + for (s = 0; s < u && i[s] === _[s]; s++); + var x = u - s; + for (o = 1; o <= x && i[u - o] === _[w - o]; o++); + return (Jt = _.slice(s, 1 < o ? 1 - o : void 0)); + } + function od(s) { + var o = s.keyCode; + return ( + 'charCode' in s ? 0 === (s = s.charCode) && 13 === o && (s = 13) : (s = o), + 10 === s && (s = 13), + 32 <= s || 13 === s ? s : 0 + ); + } + function pd() { + return !0; + } + function qd() { + return !1; + } + function rd(s) { + function b(o, i, u, _, w) { + for (var x in ((this._reactName = o), + (this._targetInst = u), + (this.type = i), + (this.nativeEvent = _), + (this.target = w), + (this.currentTarget = null), + s)) + s.hasOwnProperty(x) && ((o = s[x]), (this[x] = o ? o(_) : _[x])); + return ( + (this.isDefaultPrevented = ( + null != _.defaultPrevented ? _.defaultPrevented : !1 === _.returnValue + ) + ? pd + : qd), + (this.isPropagationStopped = qd), + this + ); + } + return ( + xe(b.prototype, { + preventDefault: function () { + this.defaultPrevented = !0; + var s = this.nativeEvent; + s && + (s.preventDefault + ? s.preventDefault() + : 'unknown' != typeof s.returnValue && (s.returnValue = !1), + (this.isDefaultPrevented = pd)); + }, + stopPropagation: function () { + var s = this.nativeEvent; + s && + (s.stopPropagation + ? s.stopPropagation() + : 'unknown' != typeof s.cancelBubble && (s.cancelBubble = !0), + (this.isPropagationStopped = pd)); + }, + persist: function () {}, + isPersistent: pd + }), + b + ); + } + var Gt, + Yt, + Xt, + Zt = { + eventPhase: 0, + bubbles: 0, + cancelable: 0, + timeStamp: function (s) { + return s.timeStamp || Date.now(); + }, + defaultPrevented: 0, + isTrusted: 0 + }, + Qt = rd(Zt), + er = xe({}, Zt, { view: 0, detail: 0 }), + tr = rd(er), + rr = xe({}, er, { + screenX: 0, + screenY: 0, + clientX: 0, + clientY: 0, + pageX: 0, + pageY: 0, + ctrlKey: 0, + shiftKey: 0, + altKey: 0, + metaKey: 0, + getModifierState: zd, + button: 0, + buttons: 0, + relatedTarget: function (s) { + return void 0 === s.relatedTarget + ? s.fromElement === s.srcElement + ? s.toElement + : s.fromElement + : s.relatedTarget; + }, + movementX: function (s) { + return 'movementX' in s + ? s.movementX + : (s !== Xt && + (Xt && 'mousemove' === s.type + ? ((Gt = s.screenX - Xt.screenX), (Yt = s.screenY - Xt.screenY)) + : (Yt = Gt = 0), + (Xt = s)), + Gt); + }, + movementY: function (s) { + return 'movementY' in s ? s.movementY : Yt; + } + }), + nr = rd(rr), + sr = rd(xe({}, rr, { dataTransfer: 0 })), + ir = rd(xe({}, er, { relatedTarget: 0 })), + ar = rd(xe({}, Zt, { animationName: 0, elapsedTime: 0, pseudoElement: 0 })), + lr = xe({}, Zt, { + clipboardData: function (s) { + return 'clipboardData' in s ? s.clipboardData : window.clipboardData; + } + }), + cr = rd(lr), + ur = rd(xe({}, Zt, { data: 0 })), + pr = { + Esc: 'Escape', + Spacebar: ' ', + Left: 'ArrowLeft', + Up: 'ArrowUp', + Right: 'ArrowRight', + Down: 'ArrowDown', + Del: 'Delete', + Win: 'OS', + Menu: 'ContextMenu', + Apps: 'ContextMenu', + Scroll: 'ScrollLock', + MozPrintableKey: 'Unidentified' + }, + dr = { + 8: 'Backspace', + 9: 'Tab', + 12: 'Clear', + 13: 'Enter', + 16: 'Shift', + 17: 'Control', + 18: 'Alt', + 19: 'Pause', + 20: 'CapsLock', + 27: 'Escape', + 32: ' ', + 33: 'PageUp', + 34: 'PageDown', + 35: 'End', + 36: 'Home', + 37: 'ArrowLeft', + 38: 'ArrowUp', + 39: 'ArrowRight', + 40: 'ArrowDown', + 45: 'Insert', + 46: 'Delete', + 112: 'F1', + 113: 'F2', + 114: 'F3', + 115: 'F4', + 116: 'F5', + 117: 'F6', + 118: 'F7', + 119: 'F8', + 120: 'F9', + 121: 'F10', + 122: 'F11', + 123: 'F12', + 144: 'NumLock', + 145: 'ScrollLock', + 224: 'Meta' + }, + fr = { Alt: 'altKey', Control: 'ctrlKey', Meta: 'metaKey', Shift: 'shiftKey' }; + function Pd(s) { + var o = this.nativeEvent; + return o.getModifierState ? o.getModifierState(s) : !!(s = fr[s]) && !!o[s]; + } + function zd() { + return Pd; + } + var mr = xe({}, er, { + key: function (s) { + if (s.key) { + var o = pr[s.key] || s.key; + if ('Unidentified' !== o) return o; + } + return 'keypress' === s.type + ? 13 === (s = od(s)) + ? 'Enter' + : String.fromCharCode(s) + : 'keydown' === s.type || 'keyup' === s.type + ? dr[s.keyCode] || 'Unidentified' + : ''; + }, + code: 0, + location: 0, + ctrlKey: 0, + shiftKey: 0, + altKey: 0, + metaKey: 0, + repeat: 0, + locale: 0, + getModifierState: zd, + charCode: function (s) { + return 'keypress' === s.type ? od(s) : 0; + }, + keyCode: function (s) { + return 'keydown' === s.type || 'keyup' === s.type ? s.keyCode : 0; + }, + which: function (s) { + return 'keypress' === s.type + ? od(s) + : 'keydown' === s.type || 'keyup' === s.type + ? s.keyCode + : 0; + } + }), + gr = rd(mr), + yr = rd( + xe({}, rr, { + pointerId: 0, + width: 0, + height: 0, + pressure: 0, + tangentialPressure: 0, + tiltX: 0, + tiltY: 0, + twist: 0, + pointerType: 0, + isPrimary: 0 + }) + ), + vr = rd( + xe({}, er, { + touches: 0, + targetTouches: 0, + changedTouches: 0, + altKey: 0, + metaKey: 0, + ctrlKey: 0, + shiftKey: 0, + getModifierState: zd + }) + ), + br = rd(xe({}, Zt, { propertyName: 0, elapsedTime: 0, pseudoElement: 0 })), + _r = xe({}, rr, { + deltaX: function (s) { + return 'deltaX' in s ? s.deltaX : 'wheelDeltaX' in s ? -s.wheelDeltaX : 0; + }, + deltaY: function (s) { + return 'deltaY' in s + ? s.deltaY + : 'wheelDeltaY' in s + ? -s.wheelDeltaY + : 'wheelDelta' in s + ? -s.wheelDelta + : 0; + }, + deltaZ: 0, + deltaMode: 0 + }), + Er = rd(_r), + wr = [9, 13, 27, 32], + Sr = C && 'CompositionEvent' in window, + xr = null; + C && 'documentMode' in document && (xr = document.documentMode); + var kr = C && 'TextEvent' in window && !xr, + Cr = C && (!Sr || (xr && 8 < xr && 11 >= xr)), + Or = String.fromCharCode(32), + Ar = !1; + function ge(s, o) { + switch (s) { + case 'keyup': + return -1 !== wr.indexOf(o.keyCode); + case 'keydown': + return 229 !== o.keyCode; + case 'keypress': + case 'mousedown': + case 'focusout': + return !0; + default: + return !1; + } + } + function he(s) { + return 'object' == typeof (s = s.detail) && 'data' in s ? s.data : null; + } + var jr = !1; + var Ir = { + color: !0, + date: !0, + datetime: !0, + 'datetime-local': !0, + email: !0, + month: !0, + number: !0, + password: !0, + range: !0, + search: !0, + tel: !0, + text: !0, + time: !0, + url: !0, + week: !0 + }; + function me(s) { + var o = s && s.nodeName && s.nodeName.toLowerCase(); + return 'input' === o ? !!Ir[s.type] : 'textarea' === o; + } + function ne(s, o, i, u) { + (Eb(u), + 0 < (o = oe(o, 'onChange')).length && + ((i = new Qt('onChange', 'change', null, i, u)), + s.push({ event: i, listeners: o }))); + } + var Pr = null, + Mr = null; + function re(s) { + se(s, 0); + } + function te(s) { + if (Wa(ue(s))) return s; + } + function ve(s, o) { + if ('change' === s) return o; + } + var Tr = !1; + if (C) { + var Nr; + if (C) { + var Rr = 'oninput' in document; + if (!Rr) { + var Dr = document.createElement('div'); + (Dr.setAttribute('oninput', 'return;'), (Rr = 'function' == typeof Dr.oninput)); + } + Nr = Rr; + } else Nr = !1; + Tr = Nr && (!document.documentMode || 9 < document.documentMode); + } + function Ae() { + Pr && (Pr.detachEvent('onpropertychange', Be), (Mr = Pr = null)); + } + function Be(s) { + if ('value' === s.propertyName && te(Mr)) { + var o = []; + (ne(o, Mr, s, xb(s)), Jb(re, o)); + } + } + function Ce(s, o, i) { + 'focusin' === s + ? (Ae(), (Mr = i), (Pr = o).attachEvent('onpropertychange', Be)) + : 'focusout' === s && Ae(); + } + function De(s) { + if ('selectionchange' === s || 'keyup' === s || 'keydown' === s) return te(Mr); + } + function Ee(s, o) { + if ('click' === s) return te(o); + } + function Fe(s, o) { + if ('input' === s || 'change' === s) return te(o); + } + var Lr = + 'function' == typeof Object.is + ? Object.is + : function Ge(s, o) { + return (s === o && (0 !== s || 1 / s == 1 / o)) || (s != s && o != o); + }; + function Ie(s, o) { + if (Lr(s, o)) return !0; + if ('object' != typeof s || null === s || 'object' != typeof o || null === o) return !1; + var i = Object.keys(s), + u = Object.keys(o); + if (i.length !== u.length) return !1; + for (u = 0; u < i.length; u++) { + var _ = i[u]; + if (!j.call(o, _) || !Lr(s[_], o[_])) return !1; + } + return !0; + } + function Je(s) { + for (; s && s.firstChild; ) s = s.firstChild; + return s; + } + function Ke(s, o) { + var i, + u = Je(s); + for (s = 0; u; ) { + if (3 === u.nodeType) { + if (((i = s + u.textContent.length), s <= o && i >= o)) + return { node: u, offset: o - s }; + s = i; + } + e: { + for (; u; ) { + if (u.nextSibling) { + u = u.nextSibling; + break e; + } + u = u.parentNode; + } + u = void 0; + } + u = Je(u); + } + } + function Le(s, o) { + return ( + !(!s || !o) && + (s === o || + ((!s || 3 !== s.nodeType) && + (o && 3 === o.nodeType + ? Le(s, o.parentNode) + : 'contains' in s + ? s.contains(o) + : !!s.compareDocumentPosition && !!(16 & s.compareDocumentPosition(o))))) + ); + } + function Me() { + for (var s = window, o = Xa(); o instanceof s.HTMLIFrameElement; ) { + try { + var i = 'string' == typeof o.contentWindow.location.href; + } catch (s) { + i = !1; + } + if (!i) break; + o = Xa((s = o.contentWindow).document); + } + return o; + } + function Ne(s) { + var o = s && s.nodeName && s.nodeName.toLowerCase(); + return ( + o && + (('input' === o && + ('text' === s.type || + 'search' === s.type || + 'tel' === s.type || + 'url' === s.type || + 'password' === s.type)) || + 'textarea' === o || + 'true' === s.contentEditable) + ); + } + function Oe(s) { + var o = Me(), + i = s.focusedElem, + u = s.selectionRange; + if (o !== i && i && i.ownerDocument && Le(i.ownerDocument.documentElement, i)) { + if (null !== u && Ne(i)) + if (((o = u.start), void 0 === (s = u.end) && (s = o), 'selectionStart' in i)) + ((i.selectionStart = o), (i.selectionEnd = Math.min(s, i.value.length))); + else if ( + (s = ((o = i.ownerDocument || document) && o.defaultView) || window).getSelection + ) { + s = s.getSelection(); + var _ = i.textContent.length, + w = Math.min(u.start, _); + ((u = void 0 === u.end ? w : Math.min(u.end, _)), + !s.extend && w > u && ((_ = u), (u = w), (w = _)), + (_ = Ke(i, w))); + var x = Ke(i, u); + _ && + x && + (1 !== s.rangeCount || + s.anchorNode !== _.node || + s.anchorOffset !== _.offset || + s.focusNode !== x.node || + s.focusOffset !== x.offset) && + ((o = o.createRange()).setStart(_.node, _.offset), + s.removeAllRanges(), + w > u + ? (s.addRange(o), s.extend(x.node, x.offset)) + : (o.setEnd(x.node, x.offset), s.addRange(o))); + } + for (o = [], s = i; (s = s.parentNode); ) + 1 === s.nodeType && o.push({ element: s, left: s.scrollLeft, top: s.scrollTop }); + for ('function' == typeof i.focus && i.focus(), i = 0; i < o.length; i++) + (((s = o[i]).element.scrollLeft = s.left), (s.element.scrollTop = s.top)); + } + } + var Br = C && 'documentMode' in document && 11 >= document.documentMode, + Fr = null, + qr = null, + $r = null, + Vr = !1; + function Ue(s, o, i) { + var u = i.window === i ? i.document : 9 === i.nodeType ? i : i.ownerDocument; + Vr || + null == Fr || + Fr !== Xa(u) || + ('selectionStart' in (u = Fr) && Ne(u) + ? (u = { start: u.selectionStart, end: u.selectionEnd }) + : (u = { + anchorNode: (u = ( + (u.ownerDocument && u.ownerDocument.defaultView) || + window + ).getSelection()).anchorNode, + anchorOffset: u.anchorOffset, + focusNode: u.focusNode, + focusOffset: u.focusOffset + }), + ($r && Ie($r, u)) || + (($r = u), + 0 < (u = oe(qr, 'onSelect')).length && + ((o = new Qt('onSelect', 'select', null, o, i)), + s.push({ event: o, listeners: u }), + (o.target = Fr)))); + } + function Ve(s, o) { + var i = {}; + return ( + (i[s.toLowerCase()] = o.toLowerCase()), + (i['Webkit' + s] = 'webkit' + o), + (i['Moz' + s] = 'moz' + o), + i + ); + } + var Ur = { + animationend: Ve('Animation', 'AnimationEnd'), + animationiteration: Ve('Animation', 'AnimationIteration'), + animationstart: Ve('Animation', 'AnimationStart'), + transitionend: Ve('Transition', 'TransitionEnd') + }, + zr = {}, + Wr = {}; + function Ze(s) { + if (zr[s]) return zr[s]; + if (!Ur[s]) return s; + var o, + i = Ur[s]; + for (o in i) if (i.hasOwnProperty(o) && o in Wr) return (zr[s] = i[o]); + return s; + } + C && + ((Wr = document.createElement('div').style), + 'AnimationEvent' in window || + (delete Ur.animationend.animation, + delete Ur.animationiteration.animation, + delete Ur.animationstart.animation), + 'TransitionEvent' in window || delete Ur.transitionend.transition); + var Kr = Ze('animationend'), + Hr = Ze('animationiteration'), + Jr = Ze('animationstart'), + Gr = Ze('transitionend'), + Yr = new Map(), + Xr = + 'abort auxClick cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel'.split( + ' ' + ); + function ff(s, o) { + (Yr.set(s, o), fa(o, [s])); + } + for (var Zr = 0; Zr < Xr.length; Zr++) { + var Qr = Xr[Zr]; + ff(Qr.toLowerCase(), 'on' + (Qr[0].toUpperCase() + Qr.slice(1))); + } + (ff(Kr, 'onAnimationEnd'), + ff(Hr, 'onAnimationIteration'), + ff(Jr, 'onAnimationStart'), + ff('dblclick', 'onDoubleClick'), + ff('focusin', 'onFocus'), + ff('focusout', 'onBlur'), + ff(Gr, 'onTransitionEnd'), + ha('onMouseEnter', ['mouseout', 'mouseover']), + ha('onMouseLeave', ['mouseout', 'mouseover']), + ha('onPointerEnter', ['pointerout', 'pointerover']), + ha('onPointerLeave', ['pointerout', 'pointerover']), + fa( + 'onChange', + 'change click focusin focusout input keydown keyup selectionchange'.split(' ') + ), + fa( + 'onSelect', + 'focusout contextmenu dragend focusin keydown keyup mousedown mouseup selectionchange'.split( + ' ' + ) + ), + fa('onBeforeInput', ['compositionend', 'keypress', 'textInput', 'paste']), + fa( + 'onCompositionEnd', + 'compositionend focusout keydown keypress keyup mousedown'.split(' ') + ), + fa( + 'onCompositionStart', + 'compositionstart focusout keydown keypress keyup mousedown'.split(' ') + ), + fa( + 'onCompositionUpdate', + 'compositionupdate focusout keydown keypress keyup mousedown'.split(' ') + )); + var en = + 'abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange resize seeked seeking stalled suspend timeupdate volumechange waiting'.split( + ' ' + ), + tn = new Set('cancel close invalid load scroll toggle'.split(' ').concat(en)); + function nf(s, o, i) { + var u = s.type || 'unknown-event'; + ((s.currentTarget = i), + (function Ub(s, o, i, u, _, w, x, C, j) { + if ((Tb.apply(this, arguments), st)) { + if (!st) throw Error(p(198)); + var L = ot; + ((st = !1), (ot = null), it || ((it = !0), (at = L))); + } + })(u, o, void 0, s), + (s.currentTarget = null)); + } + function se(s, o) { + o = !!(4 & o); + for (var i = 0; i < s.length; i++) { + var u = s[i], + _ = u.event; + u = u.listeners; + e: { + var w = void 0; + if (o) + for (var x = u.length - 1; 0 <= x; x--) { + var C = u[x], + j = C.instance, + L = C.currentTarget; + if (((C = C.listener), j !== w && _.isPropagationStopped())) break e; + (nf(_, C, L), (w = j)); + } + else + for (x = 0; x < u.length; x++) { + if ( + ((j = (C = u[x]).instance), + (L = C.currentTarget), + (C = C.listener), + j !== w && _.isPropagationStopped()) + ) + break e; + (nf(_, C, L), (w = j)); + } + } + } + if (it) throw ((s = at), (it = !1), (at = null), s); + } + function D(s, o) { + var i = o[gn]; + void 0 === i && (i = o[gn] = new Set()); + var u = s + '__bubble'; + i.has(u) || (pf(o, s, 2, !1), i.add(u)); + } + function qf(s, o, i) { + var u = 0; + (o && (u |= 4), pf(i, s, u, o)); + } + var rn = '_reactListening' + Math.random().toString(36).slice(2); + function sf(s) { + if (!s[rn]) { + ((s[rn] = !0), + w.forEach(function (o) { + 'selectionchange' !== o && (tn.has(o) || qf(o, !1, s), qf(o, !0, s)); + })); + var o = 9 === s.nodeType ? s : s.ownerDocument; + null === o || o[rn] || ((o[rn] = !0), qf('selectionchange', !1, o)); + } + } + function pf(s, o, i, u) { + switch (jd(o)) { + case 1: + var _ = ed; + break; + case 4: + _ = gd; + break; + default: + _ = fd; + } + ((i = _.bind(null, o, i, s)), + (_ = void 0), + !rt || ('touchstart' !== o && 'touchmove' !== o && 'wheel' !== o) || (_ = !0), + u + ? void 0 !== _ + ? s.addEventListener(o, i, { capture: !0, passive: _ }) + : s.addEventListener(o, i, !0) + : void 0 !== _ + ? s.addEventListener(o, i, { passive: _ }) + : s.addEventListener(o, i, !1)); + } + function hd(s, o, i, u, _) { + var w = u; + if (!(1 & o || 2 & o || null === u)) + e: for (;;) { + if (null === u) return; + var x = u.tag; + if (3 === x || 4 === x) { + var C = u.stateNode.containerInfo; + if (C === _ || (8 === C.nodeType && C.parentNode === _)) break; + if (4 === x) + for (x = u.return; null !== x; ) { + var j = x.tag; + if ( + (3 === j || 4 === j) && + ((j = x.stateNode.containerInfo) === _ || + (8 === j.nodeType && j.parentNode === _)) + ) + return; + x = x.return; + } + for (; null !== C; ) { + if (null === (x = Wc(C))) return; + if (5 === (j = x.tag) || 6 === j) { + u = w = x; + continue e; + } + C = C.parentNode; + } + } + u = u.return; + } + Jb(function () { + var u = w, + _ = xb(i), + x = []; + e: { + var C = Yr.get(s); + if (void 0 !== C) { + var j = Qt, + L = s; + switch (s) { + case 'keypress': + if (0 === od(i)) break e; + case 'keydown': + case 'keyup': + j = gr; + break; + case 'focusin': + ((L = 'focus'), (j = ir)); + break; + case 'focusout': + ((L = 'blur'), (j = ir)); + break; + case 'beforeblur': + case 'afterblur': + j = ir; + break; + case 'click': + if (2 === i.button) break e; + case 'auxclick': + case 'dblclick': + case 'mousedown': + case 'mousemove': + case 'mouseup': + case 'mouseout': + case 'mouseover': + case 'contextmenu': + j = nr; + break; + case 'drag': + case 'dragend': + case 'dragenter': + case 'dragexit': + case 'dragleave': + case 'dragover': + case 'dragstart': + case 'drop': + j = sr; + break; + case 'touchcancel': + case 'touchend': + case 'touchmove': + case 'touchstart': + j = vr; + break; + case Kr: + case Hr: + case Jr: + j = ar; + break; + case Gr: + j = br; + break; + case 'scroll': + j = tr; + break; + case 'wheel': + j = Er; + break; + case 'copy': + case 'cut': + case 'paste': + j = cr; + break; + case 'gotpointercapture': + case 'lostpointercapture': + case 'pointercancel': + case 'pointerdown': + case 'pointermove': + case 'pointerout': + case 'pointerover': + case 'pointerup': + j = yr; + } + var B = !!(4 & o), + $ = !B && 'scroll' === s, + V = B ? (null !== C ? C + 'Capture' : null) : C; + B = []; + for (var U, z = u; null !== z; ) { + var Y = (U = z).stateNode; + if ( + (5 === U.tag && + null !== Y && + ((U = Y), null !== V && null != (Y = Kb(z, V)) && B.push(tf(z, Y, U))), + $) + ) + break; + z = z.return; + } + 0 < B.length && + ((C = new j(C, L, null, i, _)), x.push({ event: C, listeners: B })); + } + } + if (!(7 & o)) { + if ( + ((j = 'mouseout' === s || 'pointerout' === s), + (!(C = 'mouseover' === s || 'pointerover' === s) || + i === Ye || + !(L = i.relatedTarget || i.fromElement) || + (!Wc(L) && !L[mn])) && + (j || C) && + ((C = + _.window === _ + ? _ + : (C = _.ownerDocument) + ? C.defaultView || C.parentWindow + : window), + j + ? ((j = u), + null !== (L = (L = i.relatedTarget || i.toElement) ? Wc(L) : null) && + (L !== ($ = Vb(L)) || (5 !== L.tag && 6 !== L.tag)) && + (L = null)) + : ((j = null), (L = u)), + j !== L)) + ) { + if ( + ((B = nr), + (Y = 'onMouseLeave'), + (V = 'onMouseEnter'), + (z = 'mouse'), + ('pointerout' !== s && 'pointerover' !== s) || + ((B = yr), (Y = 'onPointerLeave'), (V = 'onPointerEnter'), (z = 'pointer')), + ($ = null == j ? C : ue(j)), + (U = null == L ? C : ue(L)), + ((C = new B(Y, z + 'leave', j, i, _)).target = $), + (C.relatedTarget = U), + (Y = null), + Wc(_) === u && + (((B = new B(V, z + 'enter', L, i, _)).target = U), + (B.relatedTarget = $), + (Y = B)), + ($ = Y), + j && L) + ) + e: { + for (V = L, z = 0, U = B = j; U; U = vf(U)) z++; + for (U = 0, Y = V; Y; Y = vf(Y)) U++; + for (; 0 < z - U; ) ((B = vf(B)), z--); + for (; 0 < U - z; ) ((V = vf(V)), U--); + for (; z--; ) { + if (B === V || (null !== V && B === V.alternate)) break e; + ((B = vf(B)), (V = vf(V))); + } + B = null; + } + else B = null; + (null !== j && wf(x, C, j, B, !1), + null !== L && null !== $ && wf(x, $, L, B, !0)); + } + if ( + 'select' === + (j = (C = u ? ue(u) : window).nodeName && C.nodeName.toLowerCase()) || + ('input' === j && 'file' === C.type) + ) + var Z = ve; + else if (me(C)) + if (Tr) Z = Fe; + else { + Z = De; + var ee = Ce; + } + else + (j = C.nodeName) && + 'input' === j.toLowerCase() && + ('checkbox' === C.type || 'radio' === C.type) && + (Z = Ee); + switch ( + (Z && (Z = Z(s, u)) + ? ne(x, Z, i, _) + : (ee && ee(s, C, u), + 'focusout' === s && + (ee = C._wrapperState) && + ee.controlled && + 'number' === C.type && + cb(C, 'number', C.value)), + (ee = u ? ue(u) : window), + s) + ) { + case 'focusin': + (me(ee) || 'true' === ee.contentEditable) && ((Fr = ee), (qr = u), ($r = null)); + break; + case 'focusout': + $r = qr = Fr = null; + break; + case 'mousedown': + Vr = !0; + break; + case 'contextmenu': + case 'mouseup': + case 'dragend': + ((Vr = !1), Ue(x, i, _)); + break; + case 'selectionchange': + if (Br) break; + case 'keydown': + case 'keyup': + Ue(x, i, _); + } + var ie; + if (Sr) + e: { + switch (s) { + case 'compositionstart': + var ae = 'onCompositionStart'; + break e; + case 'compositionend': + ae = 'onCompositionEnd'; + break e; + case 'compositionupdate': + ae = 'onCompositionUpdate'; + break e; + } + ae = void 0; + } + else + jr + ? ge(s, i) && (ae = 'onCompositionEnd') + : 'keydown' === s && 229 === i.keyCode && (ae = 'onCompositionStart'); + (ae && + (Cr && + 'ko' !== i.locale && + (jr || 'onCompositionStart' !== ae + ? 'onCompositionEnd' === ae && jr && (ie = nd()) + : ((Ht = 'value' in (Kt = _) ? Kt.value : Kt.textContent), (jr = !0))), + 0 < (ee = oe(u, ae)).length && + ((ae = new ur(ae, s, null, i, _)), + x.push({ event: ae, listeners: ee }), + ie ? (ae.data = ie) : null !== (ie = he(i)) && (ae.data = ie))), + (ie = kr + ? (function je(s, o) { + switch (s) { + case 'compositionend': + return he(o); + case 'keypress': + return 32 !== o.which ? null : ((Ar = !0), Or); + case 'textInput': + return (s = o.data) === Or && Ar ? null : s; + default: + return null; + } + })(s, i) + : (function ke(s, o) { + if (jr) + return 'compositionend' === s || (!Sr && ge(s, o)) + ? ((s = nd()), (Jt = Ht = Kt = null), (jr = !1), s) + : null; + switch (s) { + case 'paste': + default: + return null; + case 'keypress': + if (!(o.ctrlKey || o.altKey || o.metaKey) || (o.ctrlKey && o.altKey)) { + if (o.char && 1 < o.char.length) return o.char; + if (o.which) return String.fromCharCode(o.which); + } + return null; + case 'compositionend': + return Cr && 'ko' !== o.locale ? null : o.data; + } + })(s, i)) && + 0 < (u = oe(u, 'onBeforeInput')).length && + ((_ = new ur('onBeforeInput', 'beforeinput', null, i, _)), + x.push({ event: _, listeners: u }), + (_.data = ie))); + } + se(x, o); + }); + } + function tf(s, o, i) { + return { instance: s, listener: o, currentTarget: i }; + } + function oe(s, o) { + for (var i = o + 'Capture', u = []; null !== s; ) { + var _ = s, + w = _.stateNode; + (5 === _.tag && + null !== w && + ((_ = w), + null != (w = Kb(s, i)) && u.unshift(tf(s, w, _)), + null != (w = Kb(s, o)) && u.push(tf(s, w, _))), + (s = s.return)); + } + return u; + } + function vf(s) { + if (null === s) return null; + do { + s = s.return; + } while (s && 5 !== s.tag); + return s || null; + } + function wf(s, o, i, u, _) { + for (var w = o._reactName, x = []; null !== i && i !== u; ) { + var C = i, + j = C.alternate, + L = C.stateNode; + if (null !== j && j === u) break; + (5 === C.tag && + null !== L && + ((C = L), + _ + ? null != (j = Kb(i, w)) && x.unshift(tf(i, j, C)) + : _ || (null != (j = Kb(i, w)) && x.push(tf(i, j, C)))), + (i = i.return)); + } + 0 !== x.length && s.push({ event: o, listeners: x }); + } + var nn = /\r\n?/g, + sn = /\u0000|\uFFFD/g; + function zf(s) { + return ('string' == typeof s ? s : '' + s).replace(nn, '\n').replace(sn, ''); + } + function Af(s, o, i) { + if (((o = zf(o)), zf(s) !== o && i)) throw Error(p(425)); + } + function Bf() {} + var on = null, + an = null; + function Ef(s, o) { + return ( + 'textarea' === s || + 'noscript' === s || + 'string' == typeof o.children || + 'number' == typeof o.children || + ('object' == typeof o.dangerouslySetInnerHTML && + null !== o.dangerouslySetInnerHTML && + null != o.dangerouslySetInnerHTML.__html) + ); + } + var ln = 'function' == typeof setTimeout ? setTimeout : void 0, + cn = 'function' == typeof clearTimeout ? clearTimeout : void 0, + un = 'function' == typeof Promise ? Promise : void 0, + pn = + 'function' == typeof queueMicrotask + ? queueMicrotask + : void 0 !== un + ? function (s) { + return un.resolve(null).then(s).catch(If); + } + : ln; + function If(s) { + setTimeout(function () { + throw s; + }); + } + function Kf(s, o) { + var i = o, + u = 0; + do { + var _ = i.nextSibling; + if ((s.removeChild(i), _ && 8 === _.nodeType)) + if ('/$' === (i = _.data)) { + if (0 === u) return (s.removeChild(_), void bd(o)); + u--; + } else ('$' !== i && '$?' !== i && '$!' !== i) || u++; + i = _; + } while (i); + bd(o); + } + function Lf(s) { + for (; null != s; s = s.nextSibling) { + var o = s.nodeType; + if (1 === o || 3 === o) break; + if (8 === o) { + if ('$' === (o = s.data) || '$!' === o || '$?' === o) break; + if ('/$' === o) return null; + } + } + return s; + } + function Mf(s) { + s = s.previousSibling; + for (var o = 0; s; ) { + if (8 === s.nodeType) { + var i = s.data; + if ('$' === i || '$!' === i || '$?' === i) { + if (0 === o) return s; + o--; + } else '/$' === i && o++; + } + s = s.previousSibling; + } + return null; + } + var hn = Math.random().toString(36).slice(2), + dn = '__reactFiber$' + hn, + fn = '__reactProps$' + hn, + mn = '__reactContainer$' + hn, + gn = '__reactEvents$' + hn, + yn = '__reactListeners$' + hn, + vn = '__reactHandles$' + hn; + function Wc(s) { + var o = s[dn]; + if (o) return o; + for (var i = s.parentNode; i; ) { + if ((o = i[mn] || i[dn])) { + if (((i = o.alternate), null !== o.child || (null !== i && null !== i.child))) + for (s = Mf(s); null !== s; ) { + if ((i = s[dn])) return i; + s = Mf(s); + } + return o; + } + i = (s = i).parentNode; + } + return null; + } + function Cb(s) { + return !(s = s[dn] || s[mn]) || + (5 !== s.tag && 6 !== s.tag && 13 !== s.tag && 3 !== s.tag) + ? null + : s; + } + function ue(s) { + if (5 === s.tag || 6 === s.tag) return s.stateNode; + throw Error(p(33)); + } + function Db(s) { + return s[fn] || null; + } + var bn = [], + _n = -1; + function Uf(s) { + return { current: s }; + } + function E(s) { + 0 > _n || ((s.current = bn[_n]), (bn[_n] = null), _n--); + } + function G(s, o) { + (_n++, (bn[_n] = s.current), (s.current = o)); + } + var En = {}, + wn = Uf(En), + Sn = Uf(!1), + xn = En; + function Yf(s, o) { + var i = s.type.contextTypes; + if (!i) return En; + var u = s.stateNode; + if (u && u.__reactInternalMemoizedUnmaskedChildContext === o) + return u.__reactInternalMemoizedMaskedChildContext; + var _, + w = {}; + for (_ in i) w[_] = o[_]; + return ( + u && + (((s = s.stateNode).__reactInternalMemoizedUnmaskedChildContext = o), + (s.__reactInternalMemoizedMaskedChildContext = w)), + w + ); + } + function Zf(s) { + return null != (s = s.childContextTypes); + } + function $f() { + (E(Sn), E(wn)); + } + function ag(s, o, i) { + if (wn.current !== En) throw Error(p(168)); + (G(wn, o), G(Sn, i)); + } + function bg(s, o, i) { + var u = s.stateNode; + if (((o = o.childContextTypes), 'function' != typeof u.getChildContext)) return i; + for (var _ in (u = u.getChildContext())) + if (!(_ in o)) throw Error(p(108, Ra(s) || 'Unknown', _)); + return xe({}, i, u); + } + function cg(s) { + return ( + (s = ((s = s.stateNode) && s.__reactInternalMemoizedMergedChildContext) || En), + (xn = wn.current), + G(wn, s), + G(Sn, Sn.current), + !0 + ); + } + function dg(s, o, i) { + var u = s.stateNode; + if (!u) throw Error(p(169)); + (i + ? ((s = bg(s, o, xn)), + (u.__reactInternalMemoizedMergedChildContext = s), + E(Sn), + E(wn), + G(wn, s)) + : E(Sn), + G(Sn, i)); + } + var kn = null, + Cn = !1, + On = !1; + function hg(s) { + null === kn ? (kn = [s]) : kn.push(s); + } + function jg() { + if (!On && null !== kn) { + On = !0; + var s = 0, + o = At; + try { + var i = kn; + for (At = 1; s < i.length; s++) { + var u = i[s]; + do { + u = u(!0); + } while (null !== u); + } + ((kn = null), (Cn = !1)); + } catch (o) { + throw (null !== kn && (kn = kn.slice(s + 1)), ct(gt, jg), o); + } finally { + ((At = o), (On = !1)); + } + } + return null; + } + var An = [], + jn = 0, + In = null, + Pn = 0, + Mn = [], + Tn = 0, + Nn = null, + Rn = 1, + Dn = ''; + function tg(s, o) { + ((An[jn++] = Pn), (An[jn++] = In), (In = s), (Pn = o)); + } + function ug(s, o, i) { + ((Mn[Tn++] = Rn), (Mn[Tn++] = Dn), (Mn[Tn++] = Nn), (Nn = s)); + var u = Rn; + s = Dn; + var _ = 32 - St(u) - 1; + ((u &= ~(1 << _)), (i += 1)); + var w = 32 - St(o) + _; + if (30 < w) { + var x = _ - (_ % 5); + ((w = (u & ((1 << x) - 1)).toString(32)), + (u >>= x), + (_ -= x), + (Rn = (1 << (32 - St(o) + _)) | (i << _) | u), + (Dn = w + s)); + } else ((Rn = (1 << w) | (i << _) | u), (Dn = s)); + } + function vg(s) { + null !== s.return && (tg(s, 1), ug(s, 1, 0)); + } + function wg(s) { + for (; s === In; ) ((In = An[--jn]), (An[jn] = null), (Pn = An[--jn]), (An[jn] = null)); + for (; s === Nn; ) + ((Nn = Mn[--Tn]), + (Mn[Tn] = null), + (Dn = Mn[--Tn]), + (Mn[Tn] = null), + (Rn = Mn[--Tn]), + (Mn[Tn] = null)); + } + var Ln = null, + Bn = null, + Fn = !1, + qn = null; + function Ag(s, o) { + var i = Bg(5, null, null, 0); + ((i.elementType = 'DELETED'), + (i.stateNode = o), + (i.return = s), + null === (o = s.deletions) ? ((s.deletions = [i]), (s.flags |= 16)) : o.push(i)); + } + function Cg(s, o) { + switch (s.tag) { + case 5: + var i = s.type; + return ( + null !== + (o = + 1 !== o.nodeType || i.toLowerCase() !== o.nodeName.toLowerCase() + ? null + : o) && ((s.stateNode = o), (Ln = s), (Bn = Lf(o.firstChild)), !0) + ); + case 6: + return ( + null !== (o = '' === s.pendingProps || 3 !== o.nodeType ? null : o) && + ((s.stateNode = o), (Ln = s), (Bn = null), !0) + ); + case 13: + return ( + null !== (o = 8 !== o.nodeType ? null : o) && + ((i = null !== Nn ? { id: Rn, overflow: Dn } : null), + (s.memoizedState = { dehydrated: o, treeContext: i, retryLane: 1073741824 }), + ((i = Bg(18, null, null, 0)).stateNode = o), + (i.return = s), + (s.child = i), + (Ln = s), + (Bn = null), + !0) + ); + default: + return !1; + } + } + function Dg(s) { + return !(!(1 & s.mode) || 128 & s.flags); + } + function Eg(s) { + if (Fn) { + var o = Bn; + if (o) { + var i = o; + if (!Cg(s, o)) { + if (Dg(s)) throw Error(p(418)); + o = Lf(i.nextSibling); + var u = Ln; + o && Cg(s, o) + ? Ag(u, i) + : ((s.flags = (-4097 & s.flags) | 2), (Fn = !1), (Ln = s)); + } + } else { + if (Dg(s)) throw Error(p(418)); + ((s.flags = (-4097 & s.flags) | 2), (Fn = !1), (Ln = s)); + } + } + } + function Fg(s) { + for (s = s.return; null !== s && 5 !== s.tag && 3 !== s.tag && 13 !== s.tag; ) + s = s.return; + Ln = s; + } + function Gg(s) { + if (s !== Ln) return !1; + if (!Fn) return (Fg(s), (Fn = !0), !1); + var o; + if ( + ((o = 3 !== s.tag) && + !(o = 5 !== s.tag) && + (o = 'head' !== (o = s.type) && 'body' !== o && !Ef(s.type, s.memoizedProps)), + o && (o = Bn)) + ) { + if (Dg(s)) throw (Hg(), Error(p(418))); + for (; o; ) (Ag(s, o), (o = Lf(o.nextSibling))); + } + if ((Fg(s), 13 === s.tag)) { + if (!(s = null !== (s = s.memoizedState) ? s.dehydrated : null)) throw Error(p(317)); + e: { + for (s = s.nextSibling, o = 0; s; ) { + if (8 === s.nodeType) { + var i = s.data; + if ('/$' === i) { + if (0 === o) { + Bn = Lf(s.nextSibling); + break e; + } + o--; + } else ('$' !== i && '$!' !== i && '$?' !== i) || o++; + } + s = s.nextSibling; + } + Bn = null; + } + } else Bn = Ln ? Lf(s.stateNode.nextSibling) : null; + return !0; + } + function Hg() { + for (var s = Bn; s; ) s = Lf(s.nextSibling); + } + function Ig() { + ((Bn = Ln = null), (Fn = !1)); + } + function Jg(s) { + null === qn ? (qn = [s]) : qn.push(s); + } + var $n = z.ReactCurrentBatchConfig; + function Lg(s, o, i) { + if (null !== (s = i.ref) && 'function' != typeof s && 'object' != typeof s) { + if (i._owner) { + if ((i = i._owner)) { + if (1 !== i.tag) throw Error(p(309)); + var u = i.stateNode; + } + if (!u) throw Error(p(147, s)); + var _ = u, + w = '' + s; + return null !== o && + null !== o.ref && + 'function' == typeof o.ref && + o.ref._stringRef === w + ? o.ref + : ((o = function (s) { + var o = _.refs; + null === s ? delete o[w] : (o[w] = s); + }), + (o._stringRef = w), + o); + } + if ('string' != typeof s) throw Error(p(284)); + if (!i._owner) throw Error(p(290, s)); + } + return s; + } + function Mg(s, o) { + throw ( + (s = Object.prototype.toString.call(o)), + Error( + p( + 31, + '[object Object]' === s + ? 'object with keys {' + Object.keys(o).join(', ') + '}' + : s + ) + ) + ); + } + function Ng(s) { + return (0, s._init)(s._payload); + } + function Og(s) { + function b(o, i) { + if (s) { + var u = o.deletions; + null === u ? ((o.deletions = [i]), (o.flags |= 16)) : u.push(i); + } + } + function c(o, i) { + if (!s) return null; + for (; null !== i; ) (b(o, i), (i = i.sibling)); + return null; + } + function d(s, o) { + for (s = new Map(); null !== o; ) + (null !== o.key ? s.set(o.key, o) : s.set(o.index, o), (o = o.sibling)); + return s; + } + function e(s, o) { + return (((s = Pg(s, o)).index = 0), (s.sibling = null), s); + } + function f(o, i, u) { + return ( + (o.index = u), + s + ? null !== (u = o.alternate) + ? (u = u.index) < i + ? ((o.flags |= 2), i) + : u + : ((o.flags |= 2), i) + : ((o.flags |= 1048576), i) + ); + } + function g(o) { + return (s && null === o.alternate && (o.flags |= 2), o); + } + function h(s, o, i, u) { + return null === o || 6 !== o.tag + ? (((o = Qg(i, s.mode, u)).return = s), o) + : (((o = e(o, i)).return = s), o); + } + function k(s, o, i, u) { + var _ = i.type; + return _ === ee + ? m(s, o, i.props.children, u, i.key) + : null !== o && + (o.elementType === _ || + ('object' == typeof _ && null !== _ && _.$$typeof === be && Ng(_) === o.type)) + ? (((u = e(o, i.props)).ref = Lg(s, o, i)), (u.return = s), u) + : (((u = Rg(i.type, i.key, i.props, null, s.mode, u)).ref = Lg(s, o, i)), + (u.return = s), + u); + } + function l(s, o, i, u) { + return null === o || + 4 !== o.tag || + o.stateNode.containerInfo !== i.containerInfo || + o.stateNode.implementation !== i.implementation + ? (((o = Sg(i, s.mode, u)).return = s), o) + : (((o = e(o, i.children || [])).return = s), o); + } + function m(s, o, i, u, _) { + return null === o || 7 !== o.tag + ? (((o = Tg(i, s.mode, u, _)).return = s), o) + : (((o = e(o, i)).return = s), o); + } + function q(s, o, i) { + if (('string' == typeof o && '' !== o) || 'number' == typeof o) + return (((o = Qg('' + o, s.mode, i)).return = s), o); + if ('object' == typeof o && null !== o) { + switch (o.$$typeof) { + case Y: + return ( + ((i = Rg(o.type, o.key, o.props, null, s.mode, i)).ref = Lg(s, null, o)), + (i.return = s), + i + ); + case Z: + return (((o = Sg(o, s.mode, i)).return = s), o); + case be: + return q(s, (0, o._init)(o._payload), i); + } + if (Te(o) || Ka(o)) return (((o = Tg(o, s.mode, i, null)).return = s), o); + Mg(s, o); + } + return null; + } + function r(s, o, i, u) { + var _ = null !== o ? o.key : null; + if (('string' == typeof i && '' !== i) || 'number' == typeof i) + return null !== _ ? null : h(s, o, '' + i, u); + if ('object' == typeof i && null !== i) { + switch (i.$$typeof) { + case Y: + return i.key === _ ? k(s, o, i, u) : null; + case Z: + return i.key === _ ? l(s, o, i, u) : null; + case be: + return r(s, o, (_ = i._init)(i._payload), u); + } + if (Te(i) || Ka(i)) return null !== _ ? null : m(s, o, i, u, null); + Mg(s, i); + } + return null; + } + function y(s, o, i, u, _) { + if (('string' == typeof u && '' !== u) || 'number' == typeof u) + return h(o, (s = s.get(i) || null), '' + u, _); + if ('object' == typeof u && null !== u) { + switch (u.$$typeof) { + case Y: + return k(o, (s = s.get(null === u.key ? i : u.key) || null), u, _); + case Z: + return l(o, (s = s.get(null === u.key ? i : u.key) || null), u, _); + case be: + return y(s, o, i, (0, u._init)(u._payload), _); + } + if (Te(u) || Ka(u)) return m(o, (s = s.get(i) || null), u, _, null); + Mg(o, u); + } + return null; + } + function n(o, i, u, _) { + for ( + var w = null, x = null, C = i, j = (i = 0), L = null; + null !== C && j < u.length; + j++ + ) { + C.index > j ? ((L = C), (C = null)) : (L = C.sibling); + var B = r(o, C, u[j], _); + if (null === B) { + null === C && (C = L); + break; + } + (s && C && null === B.alternate && b(o, C), + (i = f(B, i, j)), + null === x ? (w = B) : (x.sibling = B), + (x = B), + (C = L)); + } + if (j === u.length) return (c(o, C), Fn && tg(o, j), w); + if (null === C) { + for (; j < u.length; j++) + null !== (C = q(o, u[j], _)) && + ((i = f(C, i, j)), null === x ? (w = C) : (x.sibling = C), (x = C)); + return (Fn && tg(o, j), w); + } + for (C = d(o, C); j < u.length; j++) + null !== (L = y(C, o, j, u[j], _)) && + (s && null !== L.alternate && C.delete(null === L.key ? j : L.key), + (i = f(L, i, j)), + null === x ? (w = L) : (x.sibling = L), + (x = L)); + return ( + s && + C.forEach(function (s) { + return b(o, s); + }), + Fn && tg(o, j), + w + ); + } + function t(o, i, u, _) { + var w = Ka(u); + if ('function' != typeof w) throw Error(p(150)); + if (null == (u = w.call(u))) throw Error(p(151)); + for ( + var x = (w = null), C = i, j = (i = 0), L = null, B = u.next(); + null !== C && !B.done; + j++, B = u.next() + ) { + C.index > j ? ((L = C), (C = null)) : (L = C.sibling); + var $ = r(o, C, B.value, _); + if (null === $) { + null === C && (C = L); + break; + } + (s && C && null === $.alternate && b(o, C), + (i = f($, i, j)), + null === x ? (w = $) : (x.sibling = $), + (x = $), + (C = L)); + } + if (B.done) return (c(o, C), Fn && tg(o, j), w); + if (null === C) { + for (; !B.done; j++, B = u.next()) + null !== (B = q(o, B.value, _)) && + ((i = f(B, i, j)), null === x ? (w = B) : (x.sibling = B), (x = B)); + return (Fn && tg(o, j), w); + } + for (C = d(o, C); !B.done; j++, B = u.next()) + null !== (B = y(C, o, j, B.value, _)) && + (s && null !== B.alternate && C.delete(null === B.key ? j : B.key), + (i = f(B, i, j)), + null === x ? (w = B) : (x.sibling = B), + (x = B)); + return ( + s && + C.forEach(function (s) { + return b(o, s); + }), + Fn && tg(o, j), + w + ); + } + return function J(s, o, i, u) { + if ( + ('object' == typeof i && + null !== i && + i.type === ee && + null === i.key && + (i = i.props.children), + 'object' == typeof i && null !== i) + ) { + switch (i.$$typeof) { + case Y: + e: { + for (var _ = i.key, w = o; null !== w; ) { + if (w.key === _) { + if ((_ = i.type) === ee) { + if (7 === w.tag) { + (c(s, w.sibling), ((o = e(w, i.props.children)).return = s), (s = o)); + break e; + } + } else if ( + w.elementType === _ || + ('object' == typeof _ && + null !== _ && + _.$$typeof === be && + Ng(_) === w.type) + ) { + (c(s, w.sibling), + ((o = e(w, i.props)).ref = Lg(s, w, i)), + (o.return = s), + (s = o)); + break e; + } + c(s, w); + break; + } + (b(s, w), (w = w.sibling)); + } + i.type === ee + ? (((o = Tg(i.props.children, s.mode, u, i.key)).return = s), (s = o)) + : (((u = Rg(i.type, i.key, i.props, null, s.mode, u)).ref = Lg(s, o, i)), + (u.return = s), + (s = u)); + } + return g(s); + case Z: + e: { + for (w = i.key; null !== o; ) { + if (o.key === w) { + if ( + 4 === o.tag && + o.stateNode.containerInfo === i.containerInfo && + o.stateNode.implementation === i.implementation + ) { + (c(s, o.sibling), ((o = e(o, i.children || [])).return = s), (s = o)); + break e; + } + c(s, o); + break; + } + (b(s, o), (o = o.sibling)); + } + (((o = Sg(i, s.mode, u)).return = s), (s = o)); + } + return g(s); + case be: + return J(s, o, (w = i._init)(i._payload), u); + } + if (Te(i)) return n(s, o, i, u); + if (Ka(i)) return t(s, o, i, u); + Mg(s, i); + } + return ('string' == typeof i && '' !== i) || 'number' == typeof i + ? ((i = '' + i), + null !== o && 6 === o.tag + ? (c(s, o.sibling), ((o = e(o, i)).return = s), (s = o)) + : (c(s, o), ((o = Qg(i, s.mode, u)).return = s), (s = o)), + g(s)) + : c(s, o); + }; + } + var Vn = Og(!0), + Un = Og(!1), + zn = Uf(null), + Wn = null, + Kn = null, + Hn = null; + function $g() { + Hn = Kn = Wn = null; + } + function ah(s) { + var o = zn.current; + (E(zn), (s._currentValue = o)); + } + function bh(s, o, i) { + for (; null !== s; ) { + var u = s.alternate; + if ( + ((s.childLanes & o) !== o + ? ((s.childLanes |= o), null !== u && (u.childLanes |= o)) + : null !== u && (u.childLanes & o) !== o && (u.childLanes |= o), + s === i) + ) + break; + s = s.return; + } + } + function ch(s, o) { + ((Wn = s), + (Hn = Kn = null), + null !== (s = s.dependencies) && + null !== s.firstContext && + (!!(s.lanes & o) && (_s = !0), (s.firstContext = null))); + } + function eh(s) { + var o = s._currentValue; + if (Hn !== s) + if (((s = { context: s, memoizedValue: o, next: null }), null === Kn)) { + if (null === Wn) throw Error(p(308)); + ((Kn = s), (Wn.dependencies = { lanes: 0, firstContext: s })); + } else Kn = Kn.next = s; + return o; + } + var Jn = null; + function gh(s) { + null === Jn ? (Jn = [s]) : Jn.push(s); + } + function hh(s, o, i, u) { + var _ = o.interleaved; + return ( + null === _ ? ((i.next = i), gh(o)) : ((i.next = _.next), (_.next = i)), + (o.interleaved = i), + ih(s, u) + ); + } + function ih(s, o) { + s.lanes |= o; + var i = s.alternate; + for (null !== i && (i.lanes |= o), i = s, s = s.return; null !== s; ) + ((s.childLanes |= o), + null !== (i = s.alternate) && (i.childLanes |= o), + (i = s), + (s = s.return)); + return 3 === i.tag ? i.stateNode : null; + } + var Gn = !1; + function kh(s) { + s.updateQueue = { + baseState: s.memoizedState, + firstBaseUpdate: null, + lastBaseUpdate: null, + shared: { pending: null, interleaved: null, lanes: 0 }, + effects: null + }; + } + function lh(s, o) { + ((s = s.updateQueue), + o.updateQueue === s && + (o.updateQueue = { + baseState: s.baseState, + firstBaseUpdate: s.firstBaseUpdate, + lastBaseUpdate: s.lastBaseUpdate, + shared: s.shared, + effects: s.effects + })); + } + function mh(s, o) { + return { eventTime: s, lane: o, tag: 0, payload: null, callback: null, next: null }; + } + function nh(s, o, i) { + var u = s.updateQueue; + if (null === u) return null; + if (((u = u.shared), 2 & Bs)) { + var _ = u.pending; + return ( + null === _ ? (o.next = o) : ((o.next = _.next), (_.next = o)), + (u.pending = o), + ih(s, i) + ); + } + return ( + null === (_ = u.interleaved) + ? ((o.next = o), gh(u)) + : ((o.next = _.next), (_.next = o)), + (u.interleaved = o), + ih(s, i) + ); + } + function oh(s, o, i) { + if (null !== (o = o.updateQueue) && ((o = o.shared), 4194240 & i)) { + var u = o.lanes; + ((i |= u &= s.pendingLanes), (o.lanes = i), Cc(s, i)); + } + } + function ph(s, o) { + var i = s.updateQueue, + u = s.alternate; + if (null !== u && i === (u = u.updateQueue)) { + var _ = null, + w = null; + if (null !== (i = i.firstBaseUpdate)) { + do { + var x = { + eventTime: i.eventTime, + lane: i.lane, + tag: i.tag, + payload: i.payload, + callback: i.callback, + next: null + }; + (null === w ? (_ = w = x) : (w = w.next = x), (i = i.next)); + } while (null !== i); + null === w ? (_ = w = o) : (w = w.next = o); + } else _ = w = o; + return ( + (i = { + baseState: u.baseState, + firstBaseUpdate: _, + lastBaseUpdate: w, + shared: u.shared, + effects: u.effects + }), + void (s.updateQueue = i) + ); + } + (null === (s = i.lastBaseUpdate) ? (i.firstBaseUpdate = o) : (s.next = o), + (i.lastBaseUpdate = o)); + } + function qh(s, o, i, u) { + var _ = s.updateQueue; + Gn = !1; + var w = _.firstBaseUpdate, + x = _.lastBaseUpdate, + C = _.shared.pending; + if (null !== C) { + _.shared.pending = null; + var j = C, + L = j.next; + ((j.next = null), null === x ? (w = L) : (x.next = L), (x = j)); + var B = s.alternate; + null !== B && + (C = (B = B.updateQueue).lastBaseUpdate) !== x && + (null === C ? (B.firstBaseUpdate = L) : (C.next = L), (B.lastBaseUpdate = j)); + } + if (null !== w) { + var $ = _.baseState; + for (x = 0, B = L = j = null, C = w; ; ) { + var V = C.lane, + U = C.eventTime; + if ((u & V) === V) { + null !== B && + (B = B.next = + { + eventTime: U, + lane: 0, + tag: C.tag, + payload: C.payload, + callback: C.callback, + next: null + }); + e: { + var z = s, + Y = C; + switch (((V = o), (U = i), Y.tag)) { + case 1: + if ('function' == typeof (z = Y.payload)) { + $ = z.call(U, $, V); + break e; + } + $ = z; + break e; + case 3: + z.flags = (-65537 & z.flags) | 128; + case 0: + if ( + null == (V = 'function' == typeof (z = Y.payload) ? z.call(U, $, V) : z) + ) + break e; + $ = xe({}, $, V); + break e; + case 2: + Gn = !0; + } + } + null !== C.callback && + 0 !== C.lane && + ((s.flags |= 64), null === (V = _.effects) ? (_.effects = [C]) : V.push(C)); + } else + ((U = { + eventTime: U, + lane: V, + tag: C.tag, + payload: C.payload, + callback: C.callback, + next: null + }), + null === B ? ((L = B = U), (j = $)) : (B = B.next = U), + (x |= V)); + if (null === (C = C.next)) { + if (null === (C = _.shared.pending)) break; + ((C = (V = C).next), + (V.next = null), + (_.lastBaseUpdate = V), + (_.shared.pending = null)); + } + } + if ( + (null === B && (j = $), + (_.baseState = j), + (_.firstBaseUpdate = L), + (_.lastBaseUpdate = B), + null !== (o = _.shared.interleaved)) + ) { + _ = o; + do { + ((x |= _.lane), (_ = _.next)); + } while (_ !== o); + } else null === w && (_.shared.lanes = 0); + ((Ks |= x), (s.lanes = x), (s.memoizedState = $)); + } + } + function sh(s, o, i) { + if (((s = o.effects), (o.effects = null), null !== s)) + for (o = 0; o < s.length; o++) { + var u = s[o], + _ = u.callback; + if (null !== _) { + if (((u.callback = null), (u = i), 'function' != typeof _)) + throw Error(p(191, _)); + _.call(u); + } + } + } + var Yn = {}, + Xn = Uf(Yn), + Zn = Uf(Yn), + Qn = Uf(Yn); + function xh(s) { + if (s === Yn) throw Error(p(174)); + return s; + } + function yh(s, o) { + switch ((G(Qn, o), G(Zn, s), G(Xn, Yn), (s = o.nodeType))) { + case 9: + case 11: + o = (o = o.documentElement) ? o.namespaceURI : lb(null, ''); + break; + default: + o = lb( + (o = (s = 8 === s ? o.parentNode : o).namespaceURI || null), + (s = s.tagName) + ); + } + (E(Xn), G(Xn, o)); + } + function zh() { + (E(Xn), E(Zn), E(Qn)); + } + function Ah(s) { + xh(Qn.current); + var o = xh(Xn.current), + i = lb(o, s.type); + o !== i && (G(Zn, s), G(Xn, i)); + } + function Bh(s) { + Zn.current === s && (E(Xn), E(Zn)); + } + var es = Uf(0); + function Ch(s) { + for (var o = s; null !== o; ) { + if (13 === o.tag) { + var i = o.memoizedState; + if ( + null !== i && + (null === (i = i.dehydrated) || '$?' === i.data || '$!' === i.data) + ) + return o; + } else if (19 === o.tag && void 0 !== o.memoizedProps.revealOrder) { + if (128 & o.flags) return o; + } else if (null !== o.child) { + ((o.child.return = o), (o = o.child)); + continue; + } + if (o === s) break; + for (; null === o.sibling; ) { + if (null === o.return || o.return === s) return null; + o = o.return; + } + ((o.sibling.return = o.return), (o = o.sibling)); + } + return null; + } + var ts = []; + function Eh() { + for (var s = 0; s < ts.length; s++) ts[s]._workInProgressVersionPrimary = null; + ts.length = 0; + } + var rs = z.ReactCurrentDispatcher, + ns = z.ReactCurrentBatchConfig, + ss = 0, + os = null, + as = null, + ls = null, + cs = !1, + us = !1, + ps = 0, + hs = 0; + function P() { + throw Error(p(321)); + } + function Mh(s, o) { + if (null === o) return !1; + for (var i = 0; i < o.length && i < s.length; i++) if (!Lr(s[i], o[i])) return !1; + return !0; + } + function Nh(s, o, i, u, _, w) { + if ( + ((ss = w), + (os = o), + (o.memoizedState = null), + (o.updateQueue = null), + (o.lanes = 0), + (rs.current = null === s || null === s.memoizedState ? fs : ms), + (s = i(u, _)), + us) + ) { + w = 0; + do { + if (((us = !1), (ps = 0), 25 <= w)) throw Error(p(301)); + ((w += 1), + (ls = as = null), + (o.updateQueue = null), + (rs.current = gs), + (s = i(u, _))); + } while (us); + } + if ( + ((rs.current = ds), + (o = null !== as && null !== as.next), + (ss = 0), + (ls = as = os = null), + (cs = !1), + o) + ) + throw Error(p(300)); + return s; + } + function Sh() { + var s = 0 !== ps; + return ((ps = 0), s); + } + function Th() { + var s = { + memoizedState: null, + baseState: null, + baseQueue: null, + queue: null, + next: null + }; + return (null === ls ? (os.memoizedState = ls = s) : (ls = ls.next = s), ls); + } + function Uh() { + if (null === as) { + var s = os.alternate; + s = null !== s ? s.memoizedState : null; + } else s = as.next; + var o = null === ls ? os.memoizedState : ls.next; + if (null !== o) ((ls = o), (as = s)); + else { + if (null === s) throw Error(p(310)); + ((s = { + memoizedState: (as = s).memoizedState, + baseState: as.baseState, + baseQueue: as.baseQueue, + queue: as.queue, + next: null + }), + null === ls ? (os.memoizedState = ls = s) : (ls = ls.next = s)); + } + return ls; + } + function Vh(s, o) { + return 'function' == typeof o ? o(s) : o; + } + function Wh(s) { + var o = Uh(), + i = o.queue; + if (null === i) throw Error(p(311)); + i.lastRenderedReducer = s; + var u = as, + _ = u.baseQueue, + w = i.pending; + if (null !== w) { + if (null !== _) { + var x = _.next; + ((_.next = w.next), (w.next = x)); + } + ((u.baseQueue = _ = w), (i.pending = null)); + } + if (null !== _) { + ((w = _.next), (u = u.baseState)); + var C = (x = null), + j = null, + L = w; + do { + var B = L.lane; + if ((ss & B) === B) + (null !== j && + (j = j.next = + { + lane: 0, + action: L.action, + hasEagerState: L.hasEagerState, + eagerState: L.eagerState, + next: null + }), + (u = L.hasEagerState ? L.eagerState : s(u, L.action))); + else { + var $ = { + lane: B, + action: L.action, + hasEagerState: L.hasEagerState, + eagerState: L.eagerState, + next: null + }; + (null === j ? ((C = j = $), (x = u)) : (j = j.next = $), + (os.lanes |= B), + (Ks |= B)); + } + L = L.next; + } while (null !== L && L !== w); + (null === j ? (x = u) : (j.next = C), + Lr(u, o.memoizedState) || (_s = !0), + (o.memoizedState = u), + (o.baseState = x), + (o.baseQueue = j), + (i.lastRenderedState = u)); + } + if (null !== (s = i.interleaved)) { + _ = s; + do { + ((w = _.lane), (os.lanes |= w), (Ks |= w), (_ = _.next)); + } while (_ !== s); + } else null === _ && (i.lanes = 0); + return [o.memoizedState, i.dispatch]; + } + function Xh(s) { + var o = Uh(), + i = o.queue; + if (null === i) throw Error(p(311)); + i.lastRenderedReducer = s; + var u = i.dispatch, + _ = i.pending, + w = o.memoizedState; + if (null !== _) { + i.pending = null; + var x = (_ = _.next); + do { + ((w = s(w, x.action)), (x = x.next)); + } while (x !== _); + (Lr(w, o.memoizedState) || (_s = !0), + (o.memoizedState = w), + null === o.baseQueue && (o.baseState = w), + (i.lastRenderedState = w)); + } + return [w, u]; + } + function Yh() {} + function Zh(s, o) { + var i = os, + u = Uh(), + _ = o(), + w = !Lr(u.memoizedState, _); + if ( + (w && ((u.memoizedState = _), (_s = !0)), + (u = u.queue), + $h(ai.bind(null, i, u, s), [s]), + u.getSnapshot !== o || w || (null !== ls && 1 & ls.memoizedState.tag)) + ) { + if (((i.flags |= 2048), bi(9, ci.bind(null, i, u, _, o), void 0, null), null === Fs)) + throw Error(p(349)); + 30 & ss || di(i, o, _); + } + return _; + } + function di(s, o, i) { + ((s.flags |= 16384), + (s = { getSnapshot: o, value: i }), + null === (o = os.updateQueue) + ? ((o = { lastEffect: null, stores: null }), (os.updateQueue = o), (o.stores = [s])) + : null === (i = o.stores) + ? (o.stores = [s]) + : i.push(s)); + } + function ci(s, o, i, u) { + ((o.value = i), (o.getSnapshot = u), ei(o) && fi(s)); + } + function ai(s, o, i) { + return i(function () { + ei(o) && fi(s); + }); + } + function ei(s) { + var o = s.getSnapshot; + s = s.value; + try { + var i = o(); + return !Lr(s, i); + } catch (s) { + return !0; + } + } + function fi(s) { + var o = ih(s, 1); + null !== o && gi(o, s, 1, -1); + } + function hi(s) { + var o = Th(); + return ( + 'function' == typeof s && (s = s()), + (o.memoizedState = o.baseState = s), + (s = { + pending: null, + interleaved: null, + lanes: 0, + dispatch: null, + lastRenderedReducer: Vh, + lastRenderedState: s + }), + (o.queue = s), + (s = s.dispatch = ii.bind(null, os, s)), + [o.memoizedState, s] + ); + } + function bi(s, o, i, u) { + return ( + (s = { tag: s, create: o, destroy: i, deps: u, next: null }), + null === (o = os.updateQueue) + ? ((o = { lastEffect: null, stores: null }), + (os.updateQueue = o), + (o.lastEffect = s.next = s)) + : null === (i = o.lastEffect) + ? (o.lastEffect = s.next = s) + : ((u = i.next), (i.next = s), (s.next = u), (o.lastEffect = s)), + s + ); + } + function ji() { + return Uh().memoizedState; + } + function ki(s, o, i, u) { + var _ = Th(); + ((os.flags |= s), (_.memoizedState = bi(1 | o, i, void 0, void 0 === u ? null : u))); + } + function li(s, o, i, u) { + var _ = Uh(); + u = void 0 === u ? null : u; + var w = void 0; + if (null !== as) { + var x = as.memoizedState; + if (((w = x.destroy), null !== u && Mh(u, x.deps))) + return void (_.memoizedState = bi(o, i, w, u)); + } + ((os.flags |= s), (_.memoizedState = bi(1 | o, i, w, u))); + } + function mi(s, o) { + return ki(8390656, 8, s, o); + } + function $h(s, o) { + return li(2048, 8, s, o); + } + function ni(s, o) { + return li(4, 2, s, o); + } + function oi(s, o) { + return li(4, 4, s, o); + } + function pi(s, o) { + return 'function' == typeof o + ? ((s = s()), + o(s), + function () { + o(null); + }) + : null != o + ? ((s = s()), + (o.current = s), + function () { + o.current = null; + }) + : void 0; + } + function qi(s, o, i) { + return ((i = null != i ? i.concat([s]) : null), li(4, 4, pi.bind(null, o, s), i)); + } + function ri() {} + function si(s, o) { + var i = Uh(); + o = void 0 === o ? null : o; + var u = i.memoizedState; + return null !== u && null !== o && Mh(o, u[1]) ? u[0] : ((i.memoizedState = [s, o]), s); + } + function ti(s, o) { + var i = Uh(); + o = void 0 === o ? null : o; + var u = i.memoizedState; + return null !== u && null !== o && Mh(o, u[1]) + ? u[0] + : ((s = s()), (i.memoizedState = [s, o]), s); + } + function ui(s, o, i) { + return 21 & ss + ? (Lr(i, o) || ((i = yc()), (os.lanes |= i), (Ks |= i), (s.baseState = !0)), o) + : (s.baseState && ((s.baseState = !1), (_s = !0)), (s.memoizedState = i)); + } + function vi(s, o) { + var i = At; + ((At = 0 !== i && 4 > i ? i : 4), s(!0)); + var u = ns.transition; + ns.transition = {}; + try { + (s(!1), o()); + } finally { + ((At = i), (ns.transition = u)); + } + } + function wi() { + return Uh().memoizedState; + } + function xi(s, o, i) { + var u = yi(s); + if ( + ((i = { lane: u, action: i, hasEagerState: !1, eagerState: null, next: null }), zi(s)) + ) + Ai(o, i); + else if (null !== (i = hh(s, o, i, u))) { + (gi(i, s, u, R()), Bi(i, o, u)); + } + } + function ii(s, o, i) { + var u = yi(s), + _ = { lane: u, action: i, hasEagerState: !1, eagerState: null, next: null }; + if (zi(s)) Ai(o, _); + else { + var w = s.alternate; + if ( + 0 === s.lanes && + (null === w || 0 === w.lanes) && + null !== (w = o.lastRenderedReducer) + ) + try { + var x = o.lastRenderedState, + C = w(x, i); + if (((_.hasEagerState = !0), (_.eagerState = C), Lr(C, x))) { + var j = o.interleaved; + return ( + null === j ? ((_.next = _), gh(o)) : ((_.next = j.next), (j.next = _)), + void (o.interleaved = _) + ); + } + } catch (s) {} + null !== (i = hh(s, o, _, u)) && (gi(i, s, u, (_ = R())), Bi(i, o, u)); + } + } + function zi(s) { + var o = s.alternate; + return s === os || (null !== o && o === os); + } + function Ai(s, o) { + us = cs = !0; + var i = s.pending; + (null === i ? (o.next = o) : ((o.next = i.next), (i.next = o)), (s.pending = o)); + } + function Bi(s, o, i) { + if (4194240 & i) { + var u = o.lanes; + ((i |= u &= s.pendingLanes), (o.lanes = i), Cc(s, i)); + } + } + var ds = { + readContext: eh, + useCallback: P, + useContext: P, + useEffect: P, + useImperativeHandle: P, + useInsertionEffect: P, + useLayoutEffect: P, + useMemo: P, + useReducer: P, + useRef: P, + useState: P, + useDebugValue: P, + useDeferredValue: P, + useTransition: P, + useMutableSource: P, + useSyncExternalStore: P, + useId: P, + unstable_isNewReconciler: !1 + }, + fs = { + readContext: eh, + useCallback: function (s, o) { + return ((Th().memoizedState = [s, void 0 === o ? null : o]), s); + }, + useContext: eh, + useEffect: mi, + useImperativeHandle: function (s, o, i) { + return ( + (i = null != i ? i.concat([s]) : null), + ki(4194308, 4, pi.bind(null, o, s), i) + ); + }, + useLayoutEffect: function (s, o) { + return ki(4194308, 4, s, o); + }, + useInsertionEffect: function (s, o) { + return ki(4, 2, s, o); + }, + useMemo: function (s, o) { + var i = Th(); + return ((o = void 0 === o ? null : o), (s = s()), (i.memoizedState = [s, o]), s); + }, + useReducer: function (s, o, i) { + var u = Th(); + return ( + (o = void 0 !== i ? i(o) : o), + (u.memoizedState = u.baseState = o), + (s = { + pending: null, + interleaved: null, + lanes: 0, + dispatch: null, + lastRenderedReducer: s, + lastRenderedState: o + }), + (u.queue = s), + (s = s.dispatch = xi.bind(null, os, s)), + [u.memoizedState, s] + ); + }, + useRef: function (s) { + return ((s = { current: s }), (Th().memoizedState = s)); + }, + useState: hi, + useDebugValue: ri, + useDeferredValue: function (s) { + return (Th().memoizedState = s); + }, + useTransition: function () { + var s = hi(!1), + o = s[0]; + return ((s = vi.bind(null, s[1])), (Th().memoizedState = s), [o, s]); + }, + useMutableSource: function () {}, + useSyncExternalStore: function (s, o, i) { + var u = os, + _ = Th(); + if (Fn) { + if (void 0 === i) throw Error(p(407)); + i = i(); + } else { + if (((i = o()), null === Fs)) throw Error(p(349)); + 30 & ss || di(u, o, i); + } + _.memoizedState = i; + var w = { value: i, getSnapshot: o }; + return ( + (_.queue = w), + mi(ai.bind(null, u, w, s), [s]), + (u.flags |= 2048), + bi(9, ci.bind(null, u, w, i, o), void 0, null), + i + ); + }, + useId: function () { + var s = Th(), + o = Fs.identifierPrefix; + if (Fn) { + var i = Dn; + ((o = ':' + o + 'R' + (i = (Rn & ~(1 << (32 - St(Rn) - 1))).toString(32) + i)), + 0 < (i = ps++) && (o += 'H' + i.toString(32)), + (o += ':')); + } else o = ':' + o + 'r' + (i = hs++).toString(32) + ':'; + return (s.memoizedState = o); + }, + unstable_isNewReconciler: !1 + }, + ms = { + readContext: eh, + useCallback: si, + useContext: eh, + useEffect: $h, + useImperativeHandle: qi, + useInsertionEffect: ni, + useLayoutEffect: oi, + useMemo: ti, + useReducer: Wh, + useRef: ji, + useState: function () { + return Wh(Vh); + }, + useDebugValue: ri, + useDeferredValue: function (s) { + return ui(Uh(), as.memoizedState, s); + }, + useTransition: function () { + return [Wh(Vh)[0], Uh().memoizedState]; + }, + useMutableSource: Yh, + useSyncExternalStore: Zh, + useId: wi, + unstable_isNewReconciler: !1 + }, + gs = { + readContext: eh, + useCallback: si, + useContext: eh, + useEffect: $h, + useImperativeHandle: qi, + useInsertionEffect: ni, + useLayoutEffect: oi, + useMemo: ti, + useReducer: Xh, + useRef: ji, + useState: function () { + return Xh(Vh); + }, + useDebugValue: ri, + useDeferredValue: function (s) { + var o = Uh(); + return null === as ? (o.memoizedState = s) : ui(o, as.memoizedState, s); + }, + useTransition: function () { + return [Xh(Vh)[0], Uh().memoizedState]; + }, + useMutableSource: Yh, + useSyncExternalStore: Zh, + useId: wi, + unstable_isNewReconciler: !1 + }; + function Ci(s, o) { + if (s && s.defaultProps) { + for (var i in ((o = xe({}, o)), (s = s.defaultProps))) + void 0 === o[i] && (o[i] = s[i]); + return o; + } + return o; + } + function Di(s, o, i, u) { + ((i = null == (i = i(u, (o = s.memoizedState))) ? o : xe({}, o, i)), + (s.memoizedState = i), + 0 === s.lanes && (s.updateQueue.baseState = i)); + } + var ys = { + isMounted: function (s) { + return !!(s = s._reactInternals) && Vb(s) === s; + }, + enqueueSetState: function (s, o, i) { + s = s._reactInternals; + var u = R(), + _ = yi(s), + w = mh(u, _); + ((w.payload = o), + null != i && (w.callback = i), + null !== (o = nh(s, w, _)) && (gi(o, s, _, u), oh(o, s, _))); + }, + enqueueReplaceState: function (s, o, i) { + s = s._reactInternals; + var u = R(), + _ = yi(s), + w = mh(u, _); + ((w.tag = 1), + (w.payload = o), + null != i && (w.callback = i), + null !== (o = nh(s, w, _)) && (gi(o, s, _, u), oh(o, s, _))); + }, + enqueueForceUpdate: function (s, o) { + s = s._reactInternals; + var i = R(), + u = yi(s), + _ = mh(i, u); + ((_.tag = 2), + null != o && (_.callback = o), + null !== (o = nh(s, _, u)) && (gi(o, s, u, i), oh(o, s, u))); + } + }; + function Fi(s, o, i, u, _, w, x) { + return 'function' == typeof (s = s.stateNode).shouldComponentUpdate + ? s.shouldComponentUpdate(u, w, x) + : !o.prototype || !o.prototype.isPureReactComponent || !Ie(i, u) || !Ie(_, w); + } + function Gi(s, o, i) { + var u = !1, + _ = En, + w = o.contextType; + return ( + 'object' == typeof w && null !== w + ? (w = eh(w)) + : ((_ = Zf(o) ? xn : wn.current), + (w = (u = null != (u = o.contextTypes)) ? Yf(s, _) : En)), + (o = new o(i, w)), + (s.memoizedState = null !== o.state && void 0 !== o.state ? o.state : null), + (o.updater = ys), + (s.stateNode = o), + (o._reactInternals = s), + u && + (((s = s.stateNode).__reactInternalMemoizedUnmaskedChildContext = _), + (s.__reactInternalMemoizedMaskedChildContext = w)), + o + ); + } + function Hi(s, o, i, u) { + ((s = o.state), + 'function' == typeof o.componentWillReceiveProps && o.componentWillReceiveProps(i, u), + 'function' == typeof o.UNSAFE_componentWillReceiveProps && + o.UNSAFE_componentWillReceiveProps(i, u), + o.state !== s && ys.enqueueReplaceState(o, o.state, null)); + } + function Ii(s, o, i, u) { + var _ = s.stateNode; + ((_.props = i), (_.state = s.memoizedState), (_.refs = {}), kh(s)); + var w = o.contextType; + ('object' == typeof w && null !== w + ? (_.context = eh(w)) + : ((w = Zf(o) ? xn : wn.current), (_.context = Yf(s, w))), + (_.state = s.memoizedState), + 'function' == typeof (w = o.getDerivedStateFromProps) && + (Di(s, o, w, i), (_.state = s.memoizedState)), + 'function' == typeof o.getDerivedStateFromProps || + 'function' == typeof _.getSnapshotBeforeUpdate || + ('function' != typeof _.UNSAFE_componentWillMount && + 'function' != typeof _.componentWillMount) || + ((o = _.state), + 'function' == typeof _.componentWillMount && _.componentWillMount(), + 'function' == typeof _.UNSAFE_componentWillMount && _.UNSAFE_componentWillMount(), + o !== _.state && ys.enqueueReplaceState(_, _.state, null), + qh(s, i, _, u), + (_.state = s.memoizedState)), + 'function' == typeof _.componentDidMount && (s.flags |= 4194308)); + } + function Ji(s, o) { + try { + var i = '', + u = o; + do { + ((i += Pa(u)), (u = u.return)); + } while (u); + var _ = i; + } catch (s) { + _ = '\nError generating stack: ' + s.message + '\n' + s.stack; + } + return { value: s, source: o, stack: _, digest: null }; + } + function Ki(s, o, i) { + return { + value: s, + source: null, + stack: null != i ? i : null, + digest: null != o ? o : null + }; + } + function Li(s, o) { + try { + console.error(o.value); + } catch (s) { + setTimeout(function () { + throw s; + }); + } + } + var vs = 'function' == typeof WeakMap ? WeakMap : Map; + function Ni(s, o, i) { + (((i = mh(-1, i)).tag = 3), (i.payload = { element: null })); + var u = o.value; + return ( + (i.callback = function () { + (eo || ((eo = !0), (to = u)), Li(0, o)); + }), + i + ); + } + function Qi(s, o, i) { + (i = mh(-1, i)).tag = 3; + var u = s.type.getDerivedStateFromError; + if ('function' == typeof u) { + var _ = o.value; + ((i.payload = function () { + return u(_); + }), + (i.callback = function () { + Li(0, o); + })); + } + var w = s.stateNode; + return ( + null !== w && + 'function' == typeof w.componentDidCatch && + (i.callback = function () { + (Li(0, o), + 'function' != typeof u && + (null === ro ? (ro = new Set([this])) : ro.add(this))); + var s = o.stack; + this.componentDidCatch(o.value, { componentStack: null !== s ? s : '' }); + }), + i + ); + } + function Si(s, o, i) { + var u = s.pingCache; + if (null === u) { + u = s.pingCache = new vs(); + var _ = new Set(); + u.set(o, _); + } else void 0 === (_ = u.get(o)) && ((_ = new Set()), u.set(o, _)); + _.has(i) || (_.add(i), (s = Ti.bind(null, s, o, i)), o.then(s, s)); + } + function Ui(s) { + do { + var o; + if ( + ((o = 13 === s.tag) && + (o = null === (o = s.memoizedState) || null !== o.dehydrated), + o) + ) + return s; + s = s.return; + } while (null !== s); + return null; + } + function Vi(s, o, i, u, _) { + return 1 & s.mode + ? ((s.flags |= 65536), (s.lanes = _), s) + : (s === o + ? (s.flags |= 65536) + : ((s.flags |= 128), + (i.flags |= 131072), + (i.flags &= -52805), + 1 === i.tag && + (null === i.alternate + ? (i.tag = 17) + : (((o = mh(-1, 1)).tag = 2), nh(i, o, 1))), + (i.lanes |= 1)), + s); + } + var bs = z.ReactCurrentOwner, + _s = !1; + function Xi(s, o, i, u) { + o.child = null === s ? Un(o, null, i, u) : Vn(o, s.child, i, u); + } + function Yi(s, o, i, u, _) { + i = i.render; + var w = o.ref; + return ( + ch(o, _), + (u = Nh(s, o, i, u, w, _)), + (i = Sh()), + null === s || _s + ? (Fn && i && vg(o), (o.flags |= 1), Xi(s, o, u, _), o.child) + : ((o.updateQueue = s.updateQueue), + (o.flags &= -2053), + (s.lanes &= ~_), + Zi(s, o, _)) + ); + } + function $i(s, o, i, u, _) { + if (null === s) { + var w = i.type; + return 'function' != typeof w || + aj(w) || + void 0 !== w.defaultProps || + null !== i.compare || + void 0 !== i.defaultProps + ? (((s = Rg(i.type, null, u, o, o.mode, _)).ref = o.ref), + (s.return = o), + (o.child = s)) + : ((o.tag = 15), (o.type = w), bj(s, o, w, u, _)); + } + if (((w = s.child), !(s.lanes & _))) { + var x = w.memoizedProps; + if ((i = null !== (i = i.compare) ? i : Ie)(x, u) && s.ref === o.ref) + return Zi(s, o, _); + } + return ((o.flags |= 1), ((s = Pg(w, u)).ref = o.ref), (s.return = o), (o.child = s)); + } + function bj(s, o, i, u, _) { + if (null !== s) { + var w = s.memoizedProps; + if (Ie(w, u) && s.ref === o.ref) { + if (((_s = !1), (o.pendingProps = u = w), !(s.lanes & _))) + return ((o.lanes = s.lanes), Zi(s, o, _)); + 131072 & s.flags && (_s = !0); + } + } + return cj(s, o, i, u, _); + } + function dj(s, o, i) { + var u = o.pendingProps, + _ = u.children, + w = null !== s ? s.memoizedState : null; + if ('hidden' === u.mode) + if (1 & o.mode) { + if (!(1073741824 & i)) + return ( + (s = null !== w ? w.baseLanes | i : i), + (o.lanes = o.childLanes = 1073741824), + (o.memoizedState = { baseLanes: s, cachePool: null, transitions: null }), + (o.updateQueue = null), + G(Us, Vs), + (Vs |= s), + null + ); + ((o.memoizedState = { baseLanes: 0, cachePool: null, transitions: null }), + (u = null !== w ? w.baseLanes : i), + G(Us, Vs), + (Vs |= u)); + } else + ((o.memoizedState = { baseLanes: 0, cachePool: null, transitions: null }), + G(Us, Vs), + (Vs |= i)); + else + (null !== w ? ((u = w.baseLanes | i), (o.memoizedState = null)) : (u = i), + G(Us, Vs), + (Vs |= u)); + return (Xi(s, o, _, i), o.child); + } + function gj(s, o) { + var i = o.ref; + ((null === s && null !== i) || (null !== s && s.ref !== i)) && + ((o.flags |= 512), (o.flags |= 2097152)); + } + function cj(s, o, i, u, _) { + var w = Zf(i) ? xn : wn.current; + return ( + (w = Yf(o, w)), + ch(o, _), + (i = Nh(s, o, i, u, w, _)), + (u = Sh()), + null === s || _s + ? (Fn && u && vg(o), (o.flags |= 1), Xi(s, o, i, _), o.child) + : ((o.updateQueue = s.updateQueue), + (o.flags &= -2053), + (s.lanes &= ~_), + Zi(s, o, _)) + ); + } + function hj(s, o, i, u, _) { + if (Zf(i)) { + var w = !0; + cg(o); + } else w = !1; + if ((ch(o, _), null === o.stateNode)) (ij(s, o), Gi(o, i, u), Ii(o, i, u, _), (u = !0)); + else if (null === s) { + var x = o.stateNode, + C = o.memoizedProps; + x.props = C; + var j = x.context, + L = i.contextType; + 'object' == typeof L && null !== L + ? (L = eh(L)) + : (L = Yf(o, (L = Zf(i) ? xn : wn.current))); + var B = i.getDerivedStateFromProps, + $ = 'function' == typeof B || 'function' == typeof x.getSnapshotBeforeUpdate; + ($ || + ('function' != typeof x.UNSAFE_componentWillReceiveProps && + 'function' != typeof x.componentWillReceiveProps) || + ((C !== u || j !== L) && Hi(o, x, u, L)), + (Gn = !1)); + var V = o.memoizedState; + ((x.state = V), + qh(o, u, x, _), + (j = o.memoizedState), + C !== u || V !== j || Sn.current || Gn + ? ('function' == typeof B && (Di(o, i, B, u), (j = o.memoizedState)), + (C = Gn || Fi(o, i, C, u, V, j, L)) + ? ($ || + ('function' != typeof x.UNSAFE_componentWillMount && + 'function' != typeof x.componentWillMount) || + ('function' == typeof x.componentWillMount && x.componentWillMount(), + 'function' == typeof x.UNSAFE_componentWillMount && + x.UNSAFE_componentWillMount()), + 'function' == typeof x.componentDidMount && (o.flags |= 4194308)) + : ('function' == typeof x.componentDidMount && (o.flags |= 4194308), + (o.memoizedProps = u), + (o.memoizedState = j)), + (x.props = u), + (x.state = j), + (x.context = L), + (u = C)) + : ('function' == typeof x.componentDidMount && (o.flags |= 4194308), (u = !1))); + } else { + ((x = o.stateNode), + lh(s, o), + (C = o.memoizedProps), + (L = o.type === o.elementType ? C : Ci(o.type, C)), + (x.props = L), + ($ = o.pendingProps), + (V = x.context), + 'object' == typeof (j = i.contextType) && null !== j + ? (j = eh(j)) + : (j = Yf(o, (j = Zf(i) ? xn : wn.current)))); + var U = i.getDerivedStateFromProps; + ((B = 'function' == typeof U || 'function' == typeof x.getSnapshotBeforeUpdate) || + ('function' != typeof x.UNSAFE_componentWillReceiveProps && + 'function' != typeof x.componentWillReceiveProps) || + ((C !== $ || V !== j) && Hi(o, x, u, j)), + (Gn = !1), + (V = o.memoizedState), + (x.state = V), + qh(o, u, x, _)); + var z = o.memoizedState; + C !== $ || V !== z || Sn.current || Gn + ? ('function' == typeof U && (Di(o, i, U, u), (z = o.memoizedState)), + (L = Gn || Fi(o, i, L, u, V, z, j) || !1) + ? (B || + ('function' != typeof x.UNSAFE_componentWillUpdate && + 'function' != typeof x.componentWillUpdate) || + ('function' == typeof x.componentWillUpdate && + x.componentWillUpdate(u, z, j), + 'function' == typeof x.UNSAFE_componentWillUpdate && + x.UNSAFE_componentWillUpdate(u, z, j)), + 'function' == typeof x.componentDidUpdate && (o.flags |= 4), + 'function' == typeof x.getSnapshotBeforeUpdate && (o.flags |= 1024)) + : ('function' != typeof x.componentDidUpdate || + (C === s.memoizedProps && V === s.memoizedState) || + (o.flags |= 4), + 'function' != typeof x.getSnapshotBeforeUpdate || + (C === s.memoizedProps && V === s.memoizedState) || + (o.flags |= 1024), + (o.memoizedProps = u), + (o.memoizedState = z)), + (x.props = u), + (x.state = z), + (x.context = j), + (u = L)) + : ('function' != typeof x.componentDidUpdate || + (C === s.memoizedProps && V === s.memoizedState) || + (o.flags |= 4), + 'function' != typeof x.getSnapshotBeforeUpdate || + (C === s.memoizedProps && V === s.memoizedState) || + (o.flags |= 1024), + (u = !1)); + } + return jj(s, o, i, u, w, _); + } + function jj(s, o, i, u, _, w) { + gj(s, o); + var x = !!(128 & o.flags); + if (!u && !x) return (_ && dg(o, i, !1), Zi(s, o, w)); + ((u = o.stateNode), (bs.current = o)); + var C = x && 'function' != typeof i.getDerivedStateFromError ? null : u.render(); + return ( + (o.flags |= 1), + null !== s && x + ? ((o.child = Vn(o, s.child, null, w)), (o.child = Vn(o, null, C, w))) + : Xi(s, o, C, w), + (o.memoizedState = u.state), + _ && dg(o, i, !0), + o.child + ); + } + function kj(s) { + var o = s.stateNode; + (o.pendingContext + ? ag(0, o.pendingContext, o.pendingContext !== o.context) + : o.context && ag(0, o.context, !1), + yh(s, o.containerInfo)); + } + function lj(s, o, i, u, _) { + return (Ig(), Jg(_), (o.flags |= 256), Xi(s, o, i, u), o.child); + } + var Es, + ws, + Ss, + xs, + ks = { dehydrated: null, treeContext: null, retryLane: 0 }; + function nj(s) { + return { baseLanes: s, cachePool: null, transitions: null }; + } + function oj(s, o, i) { + var u, + _ = o.pendingProps, + w = es.current, + x = !1, + C = !!(128 & o.flags); + if ( + ((u = C) || (u = (null === s || null !== s.memoizedState) && !!(2 & w)), + u + ? ((x = !0), (o.flags &= -129)) + : (null !== s && null === s.memoizedState) || (w |= 1), + G(es, 1 & w), + null === s) + ) + return ( + Eg(o), + null !== (s = o.memoizedState) && null !== (s = s.dehydrated) + ? (1 & o.mode + ? '$!' === s.data + ? (o.lanes = 8) + : (o.lanes = 1073741824) + : (o.lanes = 1), + null) + : ((C = _.children), + (s = _.fallback), + x + ? ((_ = o.mode), + (x = o.child), + (C = { mode: 'hidden', children: C }), + 1 & _ || null === x + ? (x = pj(C, _, 0, null)) + : ((x.childLanes = 0), (x.pendingProps = C)), + (s = Tg(s, _, i, null)), + (x.return = o), + (s.return = o), + (x.sibling = s), + (o.child = x), + (o.child.memoizedState = nj(i)), + (o.memoizedState = ks), + s) + : qj(o, C)) + ); + if (null !== (w = s.memoizedState) && null !== (u = w.dehydrated)) + return (function rj(s, o, i, u, _, w, x) { + if (i) + return 256 & o.flags + ? ((o.flags &= -257), sj(s, o, x, (u = Ki(Error(p(422)))))) + : null !== o.memoizedState + ? ((o.child = s.child), (o.flags |= 128), null) + : ((w = u.fallback), + (_ = o.mode), + (u = pj({ mode: 'visible', children: u.children }, _, 0, null)), + ((w = Tg(w, _, x, null)).flags |= 2), + (u.return = o), + (w.return = o), + (u.sibling = w), + (o.child = u), + 1 & o.mode && Vn(o, s.child, null, x), + (o.child.memoizedState = nj(x)), + (o.memoizedState = ks), + w); + if (!(1 & o.mode)) return sj(s, o, x, null); + if ('$!' === _.data) { + if ((u = _.nextSibling && _.nextSibling.dataset)) var C = u.dgst; + return ((u = C), sj(s, o, x, (u = Ki((w = Error(p(419))), u, void 0)))); + } + if (((C = !!(x & s.childLanes)), _s || C)) { + if (null !== (u = Fs)) { + switch (x & -x) { + case 4: + _ = 2; + break; + case 16: + _ = 8; + break; + case 64: + case 128: + case 256: + case 512: + case 1024: + case 2048: + case 4096: + case 8192: + case 16384: + case 32768: + case 65536: + case 131072: + case 262144: + case 524288: + case 1048576: + case 2097152: + case 4194304: + case 8388608: + case 16777216: + case 33554432: + case 67108864: + _ = 32; + break; + case 536870912: + _ = 268435456; + break; + default: + _ = 0; + } + 0 !== (_ = _ & (u.suspendedLanes | x) ? 0 : _) && + _ !== w.retryLane && + ((w.retryLane = _), ih(s, _), gi(u, s, _, -1)); + } + return (tj(), sj(s, o, x, (u = Ki(Error(p(421)))))); + } + return '$?' === _.data + ? ((o.flags |= 128), + (o.child = s.child), + (o = uj.bind(null, s)), + (_._reactRetry = o), + null) + : ((s = w.treeContext), + (Bn = Lf(_.nextSibling)), + (Ln = o), + (Fn = !0), + (qn = null), + null !== s && + ((Mn[Tn++] = Rn), + (Mn[Tn++] = Dn), + (Mn[Tn++] = Nn), + (Rn = s.id), + (Dn = s.overflow), + (Nn = o)), + (o = qj(o, u.children)), + (o.flags |= 4096), + o); + })(s, o, C, _, u, w, i); + if (x) { + ((x = _.fallback), (C = o.mode), (u = (w = s.child).sibling)); + var j = { mode: 'hidden', children: _.children }; + return ( + 1 & C || o.child === w + ? ((_ = Pg(w, j)).subtreeFlags = 14680064 & w.subtreeFlags) + : (((_ = o.child).childLanes = 0), (_.pendingProps = j), (o.deletions = null)), + null !== u ? (x = Pg(u, x)) : ((x = Tg(x, C, i, null)).flags |= 2), + (x.return = o), + (_.return = o), + (_.sibling = x), + (o.child = _), + (_ = x), + (x = o.child), + (C = + null === (C = s.child.memoizedState) + ? nj(i) + : { baseLanes: C.baseLanes | i, cachePool: null, transitions: C.transitions }), + (x.memoizedState = C), + (x.childLanes = s.childLanes & ~i), + (o.memoizedState = ks), + _ + ); + } + return ( + (s = (x = s.child).sibling), + (_ = Pg(x, { mode: 'visible', children: _.children })), + !(1 & o.mode) && (_.lanes = i), + (_.return = o), + (_.sibling = null), + null !== s && + (null === (i = o.deletions) ? ((o.deletions = [s]), (o.flags |= 16)) : i.push(s)), + (o.child = _), + (o.memoizedState = null), + _ + ); + } + function qj(s, o) { + return ( + ((o = pj({ mode: 'visible', children: o }, s.mode, 0, null)).return = s), + (s.child = o) + ); + } + function sj(s, o, i, u) { + return ( + null !== u && Jg(u), + Vn(o, s.child, null, i), + ((s = qj(o, o.pendingProps.children)).flags |= 2), + (o.memoizedState = null), + s + ); + } + function vj(s, o, i) { + s.lanes |= o; + var u = s.alternate; + (null !== u && (u.lanes |= o), bh(s.return, o, i)); + } + function wj(s, o, i, u, _) { + var w = s.memoizedState; + null === w + ? (s.memoizedState = { + isBackwards: o, + rendering: null, + renderingStartTime: 0, + last: u, + tail: i, + tailMode: _ + }) + : ((w.isBackwards = o), + (w.rendering = null), + (w.renderingStartTime = 0), + (w.last = u), + (w.tail = i), + (w.tailMode = _)); + } + function xj(s, o, i) { + var u = o.pendingProps, + _ = u.revealOrder, + w = u.tail; + if ((Xi(s, o, u.children, i), 2 & (u = es.current))) + ((u = (1 & u) | 2), (o.flags |= 128)); + else { + if (null !== s && 128 & s.flags) + e: for (s = o.child; null !== s; ) { + if (13 === s.tag) null !== s.memoizedState && vj(s, i, o); + else if (19 === s.tag) vj(s, i, o); + else if (null !== s.child) { + ((s.child.return = s), (s = s.child)); + continue; + } + if (s === o) break e; + for (; null === s.sibling; ) { + if (null === s.return || s.return === o) break e; + s = s.return; + } + ((s.sibling.return = s.return), (s = s.sibling)); + } + u &= 1; + } + if ((G(es, u), 1 & o.mode)) + switch (_) { + case 'forwards': + for (i = o.child, _ = null; null !== i; ) + (null !== (s = i.alternate) && null === Ch(s) && (_ = i), (i = i.sibling)); + (null === (i = _) + ? ((_ = o.child), (o.child = null)) + : ((_ = i.sibling), (i.sibling = null)), + wj(o, !1, _, i, w)); + break; + case 'backwards': + for (i = null, _ = o.child, o.child = null; null !== _; ) { + if (null !== (s = _.alternate) && null === Ch(s)) { + o.child = _; + break; + } + ((s = _.sibling), (_.sibling = i), (i = _), (_ = s)); + } + wj(o, !0, i, null, w); + break; + case 'together': + wj(o, !1, null, null, void 0); + break; + default: + o.memoizedState = null; + } + else o.memoizedState = null; + return o.child; + } + function ij(s, o) { + !(1 & o.mode) && + null !== s && + ((s.alternate = null), (o.alternate = null), (o.flags |= 2)); + } + function Zi(s, o, i) { + if ( + (null !== s && (o.dependencies = s.dependencies), + (Ks |= o.lanes), + !(i & o.childLanes)) + ) + return null; + if (null !== s && o.child !== s.child) throw Error(p(153)); + if (null !== o.child) { + for ( + i = Pg((s = o.child), s.pendingProps), o.child = i, i.return = o; + null !== s.sibling; + ) + ((s = s.sibling), ((i = i.sibling = Pg(s, s.pendingProps)).return = o)); + i.sibling = null; + } + return o.child; + } + function Dj(s, o) { + if (!Fn) + switch (s.tailMode) { + case 'hidden': + o = s.tail; + for (var i = null; null !== o; ) + (null !== o.alternate && (i = o), (o = o.sibling)); + null === i ? (s.tail = null) : (i.sibling = null); + break; + case 'collapsed': + i = s.tail; + for (var u = null; null !== i; ) + (null !== i.alternate && (u = i), (i = i.sibling)); + null === u + ? o || null === s.tail + ? (s.tail = null) + : (s.tail.sibling = null) + : (u.sibling = null); + } + } + function S(s) { + var o = null !== s.alternate && s.alternate.child === s.child, + i = 0, + u = 0; + if (o) + for (var _ = s.child; null !== _; ) + ((i |= _.lanes | _.childLanes), + (u |= 14680064 & _.subtreeFlags), + (u |= 14680064 & _.flags), + (_.return = s), + (_ = _.sibling)); + else + for (_ = s.child; null !== _; ) + ((i |= _.lanes | _.childLanes), + (u |= _.subtreeFlags), + (u |= _.flags), + (_.return = s), + (_ = _.sibling)); + return ((s.subtreeFlags |= u), (s.childLanes = i), o); + } + function Ej(s, o, i) { + var u = o.pendingProps; + switch ((wg(o), o.tag)) { + case 2: + case 16: + case 15: + case 0: + case 11: + case 7: + case 8: + case 12: + case 9: + case 14: + return (S(o), null); + case 1: + case 17: + return (Zf(o.type) && $f(), S(o), null); + case 3: + return ( + (u = o.stateNode), + zh(), + E(Sn), + E(wn), + Eh(), + u.pendingContext && ((u.context = u.pendingContext), (u.pendingContext = null)), + (null !== s && null !== s.child) || + (Gg(o) + ? (o.flags |= 4) + : null === s || + (s.memoizedState.isDehydrated && !(256 & o.flags)) || + ((o.flags |= 1024), null !== qn && (Fj(qn), (qn = null)))), + ws(s, o), + S(o), + null + ); + case 5: + Bh(o); + var _ = xh(Qn.current); + if (((i = o.type), null !== s && null != o.stateNode)) + (Ss(s, o, i, u, _), s.ref !== o.ref && ((o.flags |= 512), (o.flags |= 2097152))); + else { + if (!u) { + if (null === o.stateNode) throw Error(p(166)); + return (S(o), null); + } + if (((s = xh(Xn.current)), Gg(o))) { + ((u = o.stateNode), (i = o.type)); + var w = o.memoizedProps; + switch (((u[dn] = o), (u[fn] = w), (s = !!(1 & o.mode)), i)) { + case 'dialog': + (D('cancel', u), D('close', u)); + break; + case 'iframe': + case 'object': + case 'embed': + D('load', u); + break; + case 'video': + case 'audio': + for (_ = 0; _ < en.length; _++) D(en[_], u); + break; + case 'source': + D('error', u); + break; + case 'img': + case 'image': + case 'link': + (D('error', u), D('load', u)); + break; + case 'details': + D('toggle', u); + break; + case 'input': + (Za(u, w), D('invalid', u)); + break; + case 'select': + ((u._wrapperState = { wasMultiple: !!w.multiple }), D('invalid', u)); + break; + case 'textarea': + (hb(u, w), D('invalid', u)); + } + for (var C in (ub(i, w), (_ = null), w)) + if (w.hasOwnProperty(C)) { + var j = w[C]; + 'children' === C + ? 'string' == typeof j + ? u.textContent !== j && + (!0 !== w.suppressHydrationWarning && Af(u.textContent, j, s), + (_ = ['children', j])) + : 'number' == typeof j && + u.textContent !== '' + j && + (!0 !== w.suppressHydrationWarning && Af(u.textContent, j, s), + (_ = ['children', '' + j])) + : x.hasOwnProperty(C) && null != j && 'onScroll' === C && D('scroll', u); + } + switch (i) { + case 'input': + (Va(u), db(u, w, !0)); + break; + case 'textarea': + (Va(u), jb(u)); + break; + case 'select': + case 'option': + break; + default: + 'function' == typeof w.onClick && (u.onclick = Bf); + } + ((u = _), (o.updateQueue = u), null !== u && (o.flags |= 4)); + } else { + ((C = 9 === _.nodeType ? _ : _.ownerDocument), + 'http://www.w3.org/1999/xhtml' === s && (s = kb(i)), + 'http://www.w3.org/1999/xhtml' === s + ? 'script' === i + ? (((s = C.createElement('div')).innerHTML = ''), + (s = s.removeChild(s.firstChild))) + : 'string' == typeof u.is + ? (s = C.createElement(i, { is: u.is })) + : ((s = C.createElement(i)), + 'select' === i && + ((C = s), + u.multiple ? (C.multiple = !0) : u.size && (C.size = u.size))) + : (s = C.createElementNS(s, i)), + (s[dn] = o), + (s[fn] = u), + Es(s, o, !1, !1), + (o.stateNode = s)); + e: { + switch (((C = vb(i, u)), i)) { + case 'dialog': + (D('cancel', s), D('close', s), (_ = u)); + break; + case 'iframe': + case 'object': + case 'embed': + (D('load', s), (_ = u)); + break; + case 'video': + case 'audio': + for (_ = 0; _ < en.length; _++) D(en[_], s); + _ = u; + break; + case 'source': + (D('error', s), (_ = u)); + break; + case 'img': + case 'image': + case 'link': + (D('error', s), D('load', s), (_ = u)); + break; + case 'details': + (D('toggle', s), (_ = u)); + break; + case 'input': + (Za(s, u), (_ = Ya(s, u)), D('invalid', s)); + break; + case 'option': + default: + _ = u; + break; + case 'select': + ((s._wrapperState = { wasMultiple: !!u.multiple }), + (_ = xe({}, u, { value: void 0 })), + D('invalid', s)); + break; + case 'textarea': + (hb(s, u), (_ = gb(s, u)), D('invalid', s)); + } + for (w in (ub(i, _), (j = _))) + if (j.hasOwnProperty(w)) { + var L = j[w]; + 'style' === w + ? sb(s, L) + : 'dangerouslySetInnerHTML' === w + ? null != (L = L ? L.__html : void 0) && $e(s, L) + : 'children' === w + ? 'string' == typeof L + ? ('textarea' !== i || '' !== L) && ob(s, L) + : 'number' == typeof L && ob(s, '' + L) + : 'suppressContentEditableWarning' !== w && + 'suppressHydrationWarning' !== w && + 'autoFocus' !== w && + (x.hasOwnProperty(w) + ? null != L && 'onScroll' === w && D('scroll', s) + : null != L && ta(s, w, L, C)); + } + switch (i) { + case 'input': + (Va(s), db(s, u, !1)); + break; + case 'textarea': + (Va(s), jb(s)); + break; + case 'option': + null != u.value && s.setAttribute('value', '' + Sa(u.value)); + break; + case 'select': + ((s.multiple = !!u.multiple), + null != (w = u.value) + ? fb(s, !!u.multiple, w, !1) + : null != u.defaultValue && fb(s, !!u.multiple, u.defaultValue, !0)); + break; + default: + 'function' == typeof _.onClick && (s.onclick = Bf); + } + switch (i) { + case 'button': + case 'input': + case 'select': + case 'textarea': + u = !!u.autoFocus; + break e; + case 'img': + u = !0; + break e; + default: + u = !1; + } + } + u && (o.flags |= 4); + } + null !== o.ref && ((o.flags |= 512), (o.flags |= 2097152)); + } + return (S(o), null); + case 6: + if (s && null != o.stateNode) xs(s, o, s.memoizedProps, u); + else { + if ('string' != typeof u && null === o.stateNode) throw Error(p(166)); + if (((i = xh(Qn.current)), xh(Xn.current), Gg(o))) { + if ( + ((u = o.stateNode), + (i = o.memoizedProps), + (u[dn] = o), + (w = u.nodeValue !== i) && null !== (s = Ln)) + ) + switch (s.tag) { + case 3: + Af(u.nodeValue, i, !!(1 & s.mode)); + break; + case 5: + !0 !== s.memoizedProps.suppressHydrationWarning && + Af(u.nodeValue, i, !!(1 & s.mode)); + } + w && (o.flags |= 4); + } else + (((u = (9 === i.nodeType ? i : i.ownerDocument).createTextNode(u))[dn] = o), + (o.stateNode = u)); + } + return (S(o), null); + case 13: + if ( + (E(es), + (u = o.memoizedState), + null === s || (null !== s.memoizedState && null !== s.memoizedState.dehydrated)) + ) { + if (Fn && null !== Bn && 1 & o.mode && !(128 & o.flags)) + (Hg(), Ig(), (o.flags |= 98560), (w = !1)); + else if (((w = Gg(o)), null !== u && null !== u.dehydrated)) { + if (null === s) { + if (!w) throw Error(p(318)); + if (!(w = null !== (w = o.memoizedState) ? w.dehydrated : null)) + throw Error(p(317)); + w[dn] = o; + } else (Ig(), !(128 & o.flags) && (o.memoizedState = null), (o.flags |= 4)); + (S(o), (w = !1)); + } else (null !== qn && (Fj(qn), (qn = null)), (w = !0)); + if (!w) return 65536 & o.flags ? o : null; + } + return 128 & o.flags + ? ((o.lanes = i), o) + : ((u = null !== u) !== (null !== s && null !== s.memoizedState) && + u && + ((o.child.flags |= 8192), + 1 & o.mode && (null === s || 1 & es.current ? 0 === zs && (zs = 3) : tj())), + null !== o.updateQueue && (o.flags |= 4), + S(o), + null); + case 4: + return (zh(), ws(s, o), null === s && sf(o.stateNode.containerInfo), S(o), null); + case 10: + return (ah(o.type._context), S(o), null); + case 19: + if ((E(es), null === (w = o.memoizedState))) return (S(o), null); + if (((u = !!(128 & o.flags)), null === (C = w.rendering))) + if (u) Dj(w, !1); + else { + if (0 !== zs || (null !== s && 128 & s.flags)) + for (s = o.child; null !== s; ) { + if (null !== (C = Ch(s))) { + for ( + o.flags |= 128, + Dj(w, !1), + null !== (u = C.updateQueue) && ((o.updateQueue = u), (o.flags |= 4)), + o.subtreeFlags = 0, + u = i, + i = o.child; + null !== i; + ) + ((s = u), + ((w = i).flags &= 14680066), + null === (C = w.alternate) + ? ((w.childLanes = 0), + (w.lanes = s), + (w.child = null), + (w.subtreeFlags = 0), + (w.memoizedProps = null), + (w.memoizedState = null), + (w.updateQueue = null), + (w.dependencies = null), + (w.stateNode = null)) + : ((w.childLanes = C.childLanes), + (w.lanes = C.lanes), + (w.child = C.child), + (w.subtreeFlags = 0), + (w.deletions = null), + (w.memoizedProps = C.memoizedProps), + (w.memoizedState = C.memoizedState), + (w.updateQueue = C.updateQueue), + (w.type = C.type), + (s = C.dependencies), + (w.dependencies = + null === s + ? null + : { lanes: s.lanes, firstContext: s.firstContext })), + (i = i.sibling)); + return (G(es, (1 & es.current) | 2), o.child); + } + s = s.sibling; + } + null !== w.tail && + dt() > Zs && + ((o.flags |= 128), (u = !0), Dj(w, !1), (o.lanes = 4194304)); + } + else { + if (!u) + if (null !== (s = Ch(C))) { + if ( + ((o.flags |= 128), + (u = !0), + null !== (i = s.updateQueue) && ((o.updateQueue = i), (o.flags |= 4)), + Dj(w, !0), + null === w.tail && 'hidden' === w.tailMode && !C.alternate && !Fn) + ) + return (S(o), null); + } else + 2 * dt() - w.renderingStartTime > Zs && + 1073741824 !== i && + ((o.flags |= 128), (u = !0), Dj(w, !1), (o.lanes = 4194304)); + w.isBackwards + ? ((C.sibling = o.child), (o.child = C)) + : (null !== (i = w.last) ? (i.sibling = C) : (o.child = C), (w.last = C)); + } + return null !== w.tail + ? ((o = w.tail), + (w.rendering = o), + (w.tail = o.sibling), + (w.renderingStartTime = dt()), + (o.sibling = null), + (i = es.current), + G(es, u ? (1 & i) | 2 : 1 & i), + o) + : (S(o), null); + case 22: + case 23: + return ( + Hj(), + (u = null !== o.memoizedState), + null !== s && (null !== s.memoizedState) !== u && (o.flags |= 8192), + u && 1 & o.mode + ? !!(1073741824 & Vs) && (S(o), 6 & o.subtreeFlags && (o.flags |= 8192)) + : S(o), + null + ); + case 24: + case 25: + return null; + } + throw Error(p(156, o.tag)); + } + function Ij(s, o) { + switch ((wg(o), o.tag)) { + case 1: + return ( + Zf(o.type) && $f(), + 65536 & (s = o.flags) ? ((o.flags = (-65537 & s) | 128), o) : null + ); + case 3: + return ( + zh(), + E(Sn), + E(wn), + Eh(), + 65536 & (s = o.flags) && !(128 & s) ? ((o.flags = (-65537 & s) | 128), o) : null + ); + case 5: + return (Bh(o), null); + case 13: + if ((E(es), null !== (s = o.memoizedState) && null !== s.dehydrated)) { + if (null === o.alternate) throw Error(p(340)); + Ig(); + } + return 65536 & (s = o.flags) ? ((o.flags = (-65537 & s) | 128), o) : null; + case 19: + return (E(es), null); + case 4: + return (zh(), null); + case 10: + return (ah(o.type._context), null); + case 22: + case 23: + return (Hj(), null); + default: + return null; + } + } + ((Es = function (s, o) { + for (var i = o.child; null !== i; ) { + if (5 === i.tag || 6 === i.tag) s.appendChild(i.stateNode); + else if (4 !== i.tag && null !== i.child) { + ((i.child.return = i), (i = i.child)); + continue; + } + if (i === o) break; + for (; null === i.sibling; ) { + if (null === i.return || i.return === o) return; + i = i.return; + } + ((i.sibling.return = i.return), (i = i.sibling)); + } + }), + (ws = function () {}), + (Ss = function (s, o, i, u) { + var _ = s.memoizedProps; + if (_ !== u) { + ((s = o.stateNode), xh(Xn.current)); + var w, + C = null; + switch (i) { + case 'input': + ((_ = Ya(s, _)), (u = Ya(s, u)), (C = [])); + break; + case 'select': + ((_ = xe({}, _, { value: void 0 })), + (u = xe({}, u, { value: void 0 })), + (C = [])); + break; + case 'textarea': + ((_ = gb(s, _)), (u = gb(s, u)), (C = [])); + break; + default: + 'function' != typeof _.onClick && + 'function' == typeof u.onClick && + (s.onclick = Bf); + } + for (B in (ub(i, u), (i = null), _)) + if (!u.hasOwnProperty(B) && _.hasOwnProperty(B) && null != _[B]) + if ('style' === B) { + var j = _[B]; + for (w in j) j.hasOwnProperty(w) && (i || (i = {}), (i[w] = '')); + } else + 'dangerouslySetInnerHTML' !== B && + 'children' !== B && + 'suppressContentEditableWarning' !== B && + 'suppressHydrationWarning' !== B && + 'autoFocus' !== B && + (x.hasOwnProperty(B) ? C || (C = []) : (C = C || []).push(B, null)); + for (B in u) { + var L = u[B]; + if ( + ((j = null != _ ? _[B] : void 0), + u.hasOwnProperty(B) && L !== j && (null != L || null != j)) + ) + if ('style' === B) + if (j) { + for (w in j) + !j.hasOwnProperty(w) || + (L && L.hasOwnProperty(w)) || + (i || (i = {}), (i[w] = '')); + for (w in L) + L.hasOwnProperty(w) && j[w] !== L[w] && (i || (i = {}), (i[w] = L[w])); + } else (i || (C || (C = []), C.push(B, i)), (i = L)); + else + 'dangerouslySetInnerHTML' === B + ? ((L = L ? L.__html : void 0), + (j = j ? j.__html : void 0), + null != L && j !== L && (C = C || []).push(B, L)) + : 'children' === B + ? ('string' != typeof L && 'number' != typeof L) || + (C = C || []).push(B, '' + L) + : 'suppressContentEditableWarning' !== B && + 'suppressHydrationWarning' !== B && + (x.hasOwnProperty(B) + ? (null != L && 'onScroll' === B && D('scroll', s), + C || j === L || (C = [])) + : (C = C || []).push(B, L)); + } + i && (C = C || []).push('style', i); + var B = C; + (o.updateQueue = B) && (o.flags |= 4); + } + }), + (xs = function (s, o, i, u) { + i !== u && (o.flags |= 4); + })); + var Cs = !1, + Os = !1, + As = 'function' == typeof WeakSet ? WeakSet : Set, + js = null; + function Lj(s, o) { + var i = s.ref; + if (null !== i) + if ('function' == typeof i) + try { + i(null); + } catch (i) { + W(s, o, i); + } + else i.current = null; + } + function Mj(s, o, i) { + try { + i(); + } catch (i) { + W(s, o, i); + } + } + var Is = !1; + function Pj(s, o, i) { + var u = o.updateQueue; + if (null !== (u = null !== u ? u.lastEffect : null)) { + var _ = (u = u.next); + do { + if ((_.tag & s) === s) { + var w = _.destroy; + ((_.destroy = void 0), void 0 !== w && Mj(o, i, w)); + } + _ = _.next; + } while (_ !== u); + } + } + function Qj(s, o) { + if (null !== (o = null !== (o = o.updateQueue) ? o.lastEffect : null)) { + var i = (o = o.next); + do { + if ((i.tag & s) === s) { + var u = i.create; + i.destroy = u(); + } + i = i.next; + } while (i !== o); + } + } + function Rj(s) { + var o = s.ref; + if (null !== o) { + var i = s.stateNode; + (s.tag, (s = i), 'function' == typeof o ? o(s) : (o.current = s)); + } + } + function Sj(s) { + var o = s.alternate; + (null !== o && ((s.alternate = null), Sj(o)), + (s.child = null), + (s.deletions = null), + (s.sibling = null), + 5 === s.tag && + null !== (o = s.stateNode) && + (delete o[dn], delete o[fn], delete o[gn], delete o[yn], delete o[vn]), + (s.stateNode = null), + (s.return = null), + (s.dependencies = null), + (s.memoizedProps = null), + (s.memoizedState = null), + (s.pendingProps = null), + (s.stateNode = null), + (s.updateQueue = null)); + } + function Tj(s) { + return 5 === s.tag || 3 === s.tag || 4 === s.tag; + } + function Uj(s) { + e: for (;;) { + for (; null === s.sibling; ) { + if (null === s.return || Tj(s.return)) return null; + s = s.return; + } + for ( + s.sibling.return = s.return, s = s.sibling; + 5 !== s.tag && 6 !== s.tag && 18 !== s.tag; + ) { + if (2 & s.flags) continue e; + if (null === s.child || 4 === s.tag) continue e; + ((s.child.return = s), (s = s.child)); + } + if (!(2 & s.flags)) return s.stateNode; + } + } + function Vj(s, o, i) { + var u = s.tag; + if (5 === u || 6 === u) + ((s = s.stateNode), + o + ? 8 === i.nodeType + ? i.parentNode.insertBefore(s, o) + : i.insertBefore(s, o) + : (8 === i.nodeType + ? (o = i.parentNode).insertBefore(s, i) + : (o = i).appendChild(s), + null != (i = i._reactRootContainer) || null !== o.onclick || (o.onclick = Bf))); + else if (4 !== u && null !== (s = s.child)) + for (Vj(s, o, i), s = s.sibling; null !== s; ) (Vj(s, o, i), (s = s.sibling)); + } + function Wj(s, o, i) { + var u = s.tag; + if (5 === u || 6 === u) + ((s = s.stateNode), o ? i.insertBefore(s, o) : i.appendChild(s)); + else if (4 !== u && null !== (s = s.child)) + for (Wj(s, o, i), s = s.sibling; null !== s; ) (Wj(s, o, i), (s = s.sibling)); + } + var Ps = null, + Ms = !1; + function Yj(s, o, i) { + for (i = i.child; null !== i; ) (Zj(s, o, i), (i = i.sibling)); + } + function Zj(s, o, i) { + if (wt && 'function' == typeof wt.onCommitFiberUnmount) + try { + wt.onCommitFiberUnmount(Et, i); + } catch (s) {} + switch (i.tag) { + case 5: + Os || Lj(i, o); + case 6: + var u = Ps, + _ = Ms; + ((Ps = null), + Yj(s, o, i), + (Ms = _), + null !== (Ps = u) && + (Ms + ? ((s = Ps), + (i = i.stateNode), + 8 === s.nodeType ? s.parentNode.removeChild(i) : s.removeChild(i)) + : Ps.removeChild(i.stateNode))); + break; + case 18: + null !== Ps && + (Ms + ? ((s = Ps), + (i = i.stateNode), + 8 === s.nodeType ? Kf(s.parentNode, i) : 1 === s.nodeType && Kf(s, i), + bd(s)) + : Kf(Ps, i.stateNode)); + break; + case 4: + ((u = Ps), + (_ = Ms), + (Ps = i.stateNode.containerInfo), + (Ms = !0), + Yj(s, o, i), + (Ps = u), + (Ms = _)); + break; + case 0: + case 11: + case 14: + case 15: + if (!Os && null !== (u = i.updateQueue) && null !== (u = u.lastEffect)) { + _ = u = u.next; + do { + var w = _, + x = w.destroy; + ((w = w.tag), void 0 !== x && (2 & w || 4 & w) && Mj(i, o, x), (_ = _.next)); + } while (_ !== u); + } + Yj(s, o, i); + break; + case 1: + if (!Os && (Lj(i, o), 'function' == typeof (u = i.stateNode).componentWillUnmount)) + try { + ((u.props = i.memoizedProps), + (u.state = i.memoizedState), + u.componentWillUnmount()); + } catch (s) { + W(i, o, s); + } + Yj(s, o, i); + break; + case 21: + Yj(s, o, i); + break; + case 22: + 1 & i.mode + ? ((Os = (u = Os) || null !== i.memoizedState), Yj(s, o, i), (Os = u)) + : Yj(s, o, i); + break; + default: + Yj(s, o, i); + } + } + function ak(s) { + var o = s.updateQueue; + if (null !== o) { + s.updateQueue = null; + var i = s.stateNode; + (null === i && (i = s.stateNode = new As()), + o.forEach(function (o) { + var u = bk.bind(null, s, o); + i.has(o) || (i.add(o), o.then(u, u)); + })); + } + } + function ck(s, o) { + var i = o.deletions; + if (null !== i) + for (var u = 0; u < i.length; u++) { + var _ = i[u]; + try { + var w = s, + x = o, + C = x; + e: for (; null !== C; ) { + switch (C.tag) { + case 5: + ((Ps = C.stateNode), (Ms = !1)); + break e; + case 3: + case 4: + ((Ps = C.stateNode.containerInfo), (Ms = !0)); + break e; + } + C = C.return; + } + if (null === Ps) throw Error(p(160)); + (Zj(w, x, _), (Ps = null), (Ms = !1)); + var j = _.alternate; + (null !== j && (j.return = null), (_.return = null)); + } catch (s) { + W(_, o, s); + } + } + if (12854 & o.subtreeFlags) for (o = o.child; null !== o; ) (dk(o, s), (o = o.sibling)); + } + function dk(s, o) { + var i = s.alternate, + u = s.flags; + switch (s.tag) { + case 0: + case 11: + case 14: + case 15: + if ((ck(o, s), ek(s), 4 & u)) { + try { + (Pj(3, s, s.return), Qj(3, s)); + } catch (o) { + W(s, s.return, o); + } + try { + Pj(5, s, s.return); + } catch (o) { + W(s, s.return, o); + } + } + break; + case 1: + (ck(o, s), ek(s), 512 & u && null !== i && Lj(i, i.return)); + break; + case 5: + if ((ck(o, s), ek(s), 512 & u && null !== i && Lj(i, i.return), 32 & s.flags)) { + var _ = s.stateNode; + try { + ob(_, ''); + } catch (o) { + W(s, s.return, o); + } + } + if (4 & u && null != (_ = s.stateNode)) { + var w = s.memoizedProps, + x = null !== i ? i.memoizedProps : w, + C = s.type, + j = s.updateQueue; + if (((s.updateQueue = null), null !== j)) + try { + ('input' === C && 'radio' === w.type && null != w.name && ab(_, w), vb(C, x)); + var L = vb(C, w); + for (x = 0; x < j.length; x += 2) { + var B = j[x], + $ = j[x + 1]; + 'style' === B + ? sb(_, $) + : 'dangerouslySetInnerHTML' === B + ? $e(_, $) + : 'children' === B + ? ob(_, $) + : ta(_, B, $, L); + } + switch (C) { + case 'input': + bb(_, w); + break; + case 'textarea': + ib(_, w); + break; + case 'select': + var V = _._wrapperState.wasMultiple; + _._wrapperState.wasMultiple = !!w.multiple; + var U = w.value; + null != U + ? fb(_, !!w.multiple, U, !1) + : V !== !!w.multiple && + (null != w.defaultValue + ? fb(_, !!w.multiple, w.defaultValue, !0) + : fb(_, !!w.multiple, w.multiple ? [] : '', !1)); + } + _[fn] = w; + } catch (o) { + W(s, s.return, o); + } + } + break; + case 6: + if ((ck(o, s), ek(s), 4 & u)) { + if (null === s.stateNode) throw Error(p(162)); + ((_ = s.stateNode), (w = s.memoizedProps)); + try { + _.nodeValue = w; + } catch (o) { + W(s, s.return, o); + } + } + break; + case 3: + if ((ck(o, s), ek(s), 4 & u && null !== i && i.memoizedState.isDehydrated)) + try { + bd(o.containerInfo); + } catch (o) { + W(s, s.return, o); + } + break; + case 4: + default: + (ck(o, s), ek(s)); + break; + case 13: + (ck(o, s), + ek(s), + 8192 & (_ = s.child).flags && + ((w = null !== _.memoizedState), + (_.stateNode.isHidden = w), + !w || + (null !== _.alternate && null !== _.alternate.memoizedState) || + (Xs = dt())), + 4 & u && ak(s)); + break; + case 22: + if ( + ((B = null !== i && null !== i.memoizedState), + 1 & s.mode ? ((Os = (L = Os) || B), ck(o, s), (Os = L)) : ck(o, s), + ek(s), + 8192 & u) + ) { + if ( + ((L = null !== s.memoizedState), (s.stateNode.isHidden = L) && !B && 1 & s.mode) + ) + for (js = s, B = s.child; null !== B; ) { + for ($ = js = B; null !== js; ) { + switch (((U = (V = js).child), V.tag)) { + case 0: + case 11: + case 14: + case 15: + Pj(4, V, V.return); + break; + case 1: + Lj(V, V.return); + var z = V.stateNode; + if ('function' == typeof z.componentWillUnmount) { + ((u = V), (i = V.return)); + try { + ((o = u), + (z.props = o.memoizedProps), + (z.state = o.memoizedState), + z.componentWillUnmount()); + } catch (s) { + W(u, i, s); + } + } + break; + case 5: + Lj(V, V.return); + break; + case 22: + if (null !== V.memoizedState) { + gk($); + continue; + } + } + null !== U ? ((U.return = V), (js = U)) : gk($); + } + B = B.sibling; + } + e: for (B = null, $ = s; ; ) { + if (5 === $.tag) { + if (null === B) { + B = $; + try { + ((_ = $.stateNode), + L + ? 'function' == typeof (w = _.style).setProperty + ? w.setProperty('display', 'none', 'important') + : (w.display = 'none') + : ((C = $.stateNode), + (x = + null != (j = $.memoizedProps.style) && j.hasOwnProperty('display') + ? j.display + : null), + (C.style.display = rb('display', x)))); + } catch (o) { + W(s, s.return, o); + } + } + } else if (6 === $.tag) { + if (null === B) + try { + $.stateNode.nodeValue = L ? '' : $.memoizedProps; + } catch (o) { + W(s, s.return, o); + } + } else if ( + ((22 !== $.tag && 23 !== $.tag) || null === $.memoizedState || $ === s) && + null !== $.child + ) { + (($.child.return = $), ($ = $.child)); + continue; + } + if ($ === s) break e; + for (; null === $.sibling; ) { + if (null === $.return || $.return === s) break e; + (B === $ && (B = null), ($ = $.return)); + } + (B === $ && (B = null), ($.sibling.return = $.return), ($ = $.sibling)); + } + } + break; + case 19: + (ck(o, s), ek(s), 4 & u && ak(s)); + case 21: + } + } + function ek(s) { + var o = s.flags; + if (2 & o) { + try { + e: { + for (var i = s.return; null !== i; ) { + if (Tj(i)) { + var u = i; + break e; + } + i = i.return; + } + throw Error(p(160)); + } + switch (u.tag) { + case 5: + var _ = u.stateNode; + (32 & u.flags && (ob(_, ''), (u.flags &= -33)), Wj(s, Uj(s), _)); + break; + case 3: + case 4: + var w = u.stateNode.containerInfo; + Vj(s, Uj(s), w); + break; + default: + throw Error(p(161)); + } + } catch (o) { + W(s, s.return, o); + } + s.flags &= -3; + } + 4096 & o && (s.flags &= -4097); + } + function hk(s, o, i) { + ((js = s), ik(s, o, i)); + } + function ik(s, o, i) { + for (var u = !!(1 & s.mode); null !== js; ) { + var _ = js, + w = _.child; + if (22 === _.tag && u) { + var x = null !== _.memoizedState || Cs; + if (!x) { + var C = _.alternate, + j = (null !== C && null !== C.memoizedState) || Os; + C = Cs; + var L = Os; + if (((Cs = x), (Os = j) && !L)) + for (js = _; null !== js; ) + ((j = (x = js).child), + 22 === x.tag && null !== x.memoizedState + ? jk(_) + : null !== j + ? ((j.return = x), (js = j)) + : jk(_)); + for (; null !== w; ) ((js = w), ik(w, o, i), (w = w.sibling)); + ((js = _), (Cs = C), (Os = L)); + } + kk(s); + } else 8772 & _.subtreeFlags && null !== w ? ((w.return = _), (js = w)) : kk(s); + } + } + function kk(s) { + for (; null !== js; ) { + var o = js; + if (8772 & o.flags) { + var i = o.alternate; + try { + if (8772 & o.flags) + switch (o.tag) { + case 0: + case 11: + case 15: + Os || Qj(5, o); + break; + case 1: + var u = o.stateNode; + if (4 & o.flags && !Os) + if (null === i) u.componentDidMount(); + else { + var _ = + o.elementType === o.type + ? i.memoizedProps + : Ci(o.type, i.memoizedProps); + u.componentDidUpdate( + _, + i.memoizedState, + u.__reactInternalSnapshotBeforeUpdate + ); + } + var w = o.updateQueue; + null !== w && sh(o, w, u); + break; + case 3: + var x = o.updateQueue; + if (null !== x) { + if (((i = null), null !== o.child)) + switch (o.child.tag) { + case 5: + case 1: + i = o.child.stateNode; + } + sh(o, x, i); + } + break; + case 5: + var C = o.stateNode; + if (null === i && 4 & o.flags) { + i = C; + var j = o.memoizedProps; + switch (o.type) { + case 'button': + case 'input': + case 'select': + case 'textarea': + j.autoFocus && i.focus(); + break; + case 'img': + j.src && (i.src = j.src); + } + } + break; + case 6: + case 4: + case 12: + case 19: + case 17: + case 21: + case 22: + case 23: + case 25: + break; + case 13: + if (null === o.memoizedState) { + var L = o.alternate; + if (null !== L) { + var B = L.memoizedState; + if (null !== B) { + var $ = B.dehydrated; + null !== $ && bd($); + } + } + } + break; + default: + throw Error(p(163)); + } + Os || (512 & o.flags && Rj(o)); + } catch (s) { + W(o, o.return, s); + } + } + if (o === s) { + js = null; + break; + } + if (null !== (i = o.sibling)) { + ((i.return = o.return), (js = i)); + break; + } + js = o.return; + } + } + function gk(s) { + for (; null !== js; ) { + var o = js; + if (o === s) { + js = null; + break; + } + var i = o.sibling; + if (null !== i) { + ((i.return = o.return), (js = i)); + break; + } + js = o.return; + } + } + function jk(s) { + for (; null !== js; ) { + var o = js; + try { + switch (o.tag) { + case 0: + case 11: + case 15: + var i = o.return; + try { + Qj(4, o); + } catch (s) { + W(o, i, s); + } + break; + case 1: + var u = o.stateNode; + if ('function' == typeof u.componentDidMount) { + var _ = o.return; + try { + u.componentDidMount(); + } catch (s) { + W(o, _, s); + } + } + var w = o.return; + try { + Rj(o); + } catch (s) { + W(o, w, s); + } + break; + case 5: + var x = o.return; + try { + Rj(o); + } catch (s) { + W(o, x, s); + } + } + } catch (s) { + W(o, o.return, s); + } + if (o === s) { + js = null; + break; + } + var C = o.sibling; + if (null !== C) { + ((C.return = o.return), (js = C)); + break; + } + js = o.return; + } + } + var Ts, + Ns = Math.ceil, + Rs = z.ReactCurrentDispatcher, + Ds = z.ReactCurrentOwner, + Ls = z.ReactCurrentBatchConfig, + Bs = 0, + Fs = null, + qs = null, + $s = 0, + Vs = 0, + Us = Uf(0), + zs = 0, + Ws = null, + Ks = 0, + Hs = 0, + Js = 0, + Gs = null, + Ys = null, + Xs = 0, + Zs = 1 / 0, + Qs = null, + eo = !1, + to = null, + ro = null, + no = !1, + so = null, + oo = 0, + io = 0, + ao = null, + lo = -1, + co = 0; + function R() { + return 6 & Bs ? dt() : -1 !== lo ? lo : (lo = dt()); + } + function yi(s) { + return 1 & s.mode + ? 2 & Bs && 0 !== $s + ? $s & -$s + : null !== $n.transition + ? (0 === co && (co = yc()), co) + : 0 !== (s = At) + ? s + : (s = void 0 === (s = window.event) ? 16 : jd(s.type)) + : 1; + } + function gi(s, o, i, u) { + if (50 < io) throw ((io = 0), (ao = null), Error(p(185))); + (Ac(s, i, u), + (2 & Bs && s === Fs) || + (s === Fs && (!(2 & Bs) && (Hs |= i), 4 === zs && Ck(s, $s)), + Dk(s, u), + 1 === i && 0 === Bs && !(1 & o.mode) && ((Zs = dt() + 500), Cn && jg()))); + } + function Dk(s, o) { + var i = s.callbackNode; + !(function wc(s, o) { + for ( + var i = s.suspendedLanes, + u = s.pingedLanes, + _ = s.expirationTimes, + w = s.pendingLanes; + 0 < w; + ) { + var x = 31 - St(w), + C = 1 << x, + j = _[x]; + (-1 === j + ? (C & i && !(C & u)) || (_[x] = vc(C, o)) + : j <= o && (s.expiredLanes |= C), + (w &= ~C)); + } + })(s, o); + var u = uc(s, s === Fs ? $s : 0); + if (0 === u) (null !== i && ut(i), (s.callbackNode = null), (s.callbackPriority = 0)); + else if (((o = u & -u), s.callbackPriority !== o)) { + if ((null != i && ut(i), 1 === o)) + (0 === s.tag + ? (function ig(s) { + ((Cn = !0), hg(s)); + })(Ek.bind(null, s)) + : hg(Ek.bind(null, s)), + pn(function () { + !(6 & Bs) && jg(); + }), + (i = null)); + else { + switch (Dc(u)) { + case 1: + i = gt; + break; + case 4: + i = yt; + break; + case 16: + default: + i = vt; + break; + case 536870912: + i = _t; + } + i = Fk(i, Gk.bind(null, s)); + } + ((s.callbackPriority = o), (s.callbackNode = i)); + } + } + function Gk(s, o) { + if (((lo = -1), (co = 0), 6 & Bs)) throw Error(p(327)); + var i = s.callbackNode; + if (Hk() && s.callbackNode !== i) return null; + var u = uc(s, s === Fs ? $s : 0); + if (0 === u) return null; + if (30 & u || u & s.expiredLanes || o) o = Ik(s, u); + else { + o = u; + var _ = Bs; + Bs |= 2; + var w = Jk(); + for ((Fs === s && $s === o) || ((Qs = null), (Zs = dt() + 500), Kk(s, o)); ; ) + try { + Lk(); + break; + } catch (o) { + Mk(s, o); + } + ($g(), + (Rs.current = w), + (Bs = _), + null !== qs ? (o = 0) : ((Fs = null), ($s = 0), (o = zs))); + } + if (0 !== o) { + if ((2 === o && 0 !== (_ = xc(s)) && ((u = _), (o = Nk(s, _))), 1 === o)) + throw ((i = Ws), Kk(s, 0), Ck(s, u), Dk(s, dt()), i); + if (6 === o) Ck(s, u); + else { + if ( + ((_ = s.current.alternate), + !( + 30 & u || + (function Ok(s) { + for (var o = s; ; ) { + if (16384 & o.flags) { + var i = o.updateQueue; + if (null !== i && null !== (i = i.stores)) + for (var u = 0; u < i.length; u++) { + var _ = i[u], + w = _.getSnapshot; + _ = _.value; + try { + if (!Lr(w(), _)) return !1; + } catch (s) { + return !1; + } + } + } + if (((i = o.child), 16384 & o.subtreeFlags && null !== i)) + ((i.return = o), (o = i)); + else { + if (o === s) break; + for (; null === o.sibling; ) { + if (null === o.return || o.return === s) return !0; + o = o.return; + } + ((o.sibling.return = o.return), (o = o.sibling)); + } + } + return !0; + })(_) || + ((o = Ik(s, u)), + 2 === o && ((w = xc(s)), 0 !== w && ((u = w), (o = Nk(s, w)))), + 1 !== o) + )) + ) + throw ((i = Ws), Kk(s, 0), Ck(s, u), Dk(s, dt()), i); + switch (((s.finishedWork = _), (s.finishedLanes = u), o)) { + case 0: + case 1: + throw Error(p(345)); + case 2: + case 5: + Pk(s, Ys, Qs); + break; + case 3: + if ((Ck(s, u), (130023424 & u) === u && 10 < (o = Xs + 500 - dt()))) { + if (0 !== uc(s, 0)) break; + if (((_ = s.suspendedLanes) & u) !== u) { + (R(), (s.pingedLanes |= s.suspendedLanes & _)); + break; + } + s.timeoutHandle = ln(Pk.bind(null, s, Ys, Qs), o); + break; + } + Pk(s, Ys, Qs); + break; + case 4: + if ((Ck(s, u), (4194240 & u) === u)) break; + for (o = s.eventTimes, _ = -1; 0 < u; ) { + var x = 31 - St(u); + ((w = 1 << x), (x = o[x]) > _ && (_ = x), (u &= ~w)); + } + if ( + ((u = _), + 10 < + (u = + (120 > (u = dt() - u) + ? 120 + : 480 > u + ? 480 + : 1080 > u + ? 1080 + : 1920 > u + ? 1920 + : 3e3 > u + ? 3e3 + : 4320 > u + ? 4320 + : 1960 * Ns(u / 1960)) - u)) + ) { + s.timeoutHandle = ln(Pk.bind(null, s, Ys, Qs), u); + break; + } + Pk(s, Ys, Qs); + break; + default: + throw Error(p(329)); + } + } + } + return (Dk(s, dt()), s.callbackNode === i ? Gk.bind(null, s) : null); + } + function Nk(s, o) { + var i = Gs; + return ( + s.current.memoizedState.isDehydrated && (Kk(s, o).flags |= 256), + 2 !== (s = Ik(s, o)) && ((o = Ys), (Ys = i), null !== o && Fj(o)), + s + ); + } + function Fj(s) { + null === Ys ? (Ys = s) : Ys.push.apply(Ys, s); + } + function Ck(s, o) { + for ( + o &= ~Js, o &= ~Hs, s.suspendedLanes |= o, s.pingedLanes &= ~o, s = s.expirationTimes; + 0 < o; + ) { + var i = 31 - St(o), + u = 1 << i; + ((s[i] = -1), (o &= ~u)); + } + } + function Ek(s) { + if (6 & Bs) throw Error(p(327)); + Hk(); + var o = uc(s, 0); + if (!(1 & o)) return (Dk(s, dt()), null); + var i = Ik(s, o); + if (0 !== s.tag && 2 === i) { + var u = xc(s); + 0 !== u && ((o = u), (i = Nk(s, u))); + } + if (1 === i) throw ((i = Ws), Kk(s, 0), Ck(s, o), Dk(s, dt()), i); + if (6 === i) throw Error(p(345)); + return ( + (s.finishedWork = s.current.alternate), + (s.finishedLanes = o), + Pk(s, Ys, Qs), + Dk(s, dt()), + null + ); + } + function Qk(s, o) { + var i = Bs; + Bs |= 1; + try { + return s(o); + } finally { + 0 === (Bs = i) && ((Zs = dt() + 500), Cn && jg()); + } + } + function Rk(s) { + null !== so && 0 === so.tag && !(6 & Bs) && Hk(); + var o = Bs; + Bs |= 1; + var i = Ls.transition, + u = At; + try { + if (((Ls.transition = null), (At = 1), s)) return s(); + } finally { + ((At = u), (Ls.transition = i), !(6 & (Bs = o)) && jg()); + } + } + function Hj() { + ((Vs = Us.current), E(Us)); + } + function Kk(s, o) { + ((s.finishedWork = null), (s.finishedLanes = 0)); + var i = s.timeoutHandle; + if ((-1 !== i && ((s.timeoutHandle = -1), cn(i)), null !== qs)) + for (i = qs.return; null !== i; ) { + var u = i; + switch ((wg(u), u.tag)) { + case 1: + null != (u = u.type.childContextTypes) && $f(); + break; + case 3: + (zh(), E(Sn), E(wn), Eh()); + break; + case 5: + Bh(u); + break; + case 4: + zh(); + break; + case 13: + case 19: + E(es); + break; + case 10: + ah(u.type._context); + break; + case 22: + case 23: + Hj(); + } + i = i.return; + } + if ( + ((Fs = s), + (qs = s = Pg(s.current, null)), + ($s = Vs = o), + (zs = 0), + (Ws = null), + (Js = Hs = Ks = 0), + (Ys = Gs = null), + null !== Jn) + ) { + for (o = 0; o < Jn.length; o++) + if (null !== (u = (i = Jn[o]).interleaved)) { + i.interleaved = null; + var _ = u.next, + w = i.pending; + if (null !== w) { + var x = w.next; + ((w.next = _), (u.next = x)); + } + i.pending = u; + } + Jn = null; + } + return s; + } + function Mk(s, o) { + for (;;) { + var i = qs; + try { + if (($g(), (rs.current = ds), cs)) { + for (var u = os.memoizedState; null !== u; ) { + var _ = u.queue; + (null !== _ && (_.pending = null), (u = u.next)); + } + cs = !1; + } + if ( + ((ss = 0), + (ls = as = os = null), + (us = !1), + (ps = 0), + (Ds.current = null), + null === i || null === i.return) + ) { + ((zs = 1), (Ws = o), (qs = null)); + break; + } + e: { + var w = s, + x = i.return, + C = i, + j = o; + if ( + ((o = $s), + (C.flags |= 32768), + null !== j && 'object' == typeof j && 'function' == typeof j.then) + ) { + var L = j, + B = C, + $ = B.tag; + if (!(1 & B.mode || (0 !== $ && 11 !== $ && 15 !== $))) { + var V = B.alternate; + V + ? ((B.updateQueue = V.updateQueue), + (B.memoizedState = V.memoizedState), + (B.lanes = V.lanes)) + : ((B.updateQueue = null), (B.memoizedState = null)); + } + var U = Ui(x); + if (null !== U) { + ((U.flags &= -257), Vi(U, x, C, 0, o), 1 & U.mode && Si(w, L, o), (j = L)); + var z = (o = U).updateQueue; + if (null === z) { + var Y = new Set(); + (Y.add(j), (o.updateQueue = Y)); + } else z.add(j); + break e; + } + if (!(1 & o)) { + (Si(w, L, o), tj()); + break e; + } + j = Error(p(426)); + } else if (Fn && 1 & C.mode) { + var Z = Ui(x); + if (null !== Z) { + (!(65536 & Z.flags) && (Z.flags |= 256), Vi(Z, x, C, 0, o), Jg(Ji(j, C))); + break e; + } + } + ((w = j = Ji(j, C)), + 4 !== zs && (zs = 2), + null === Gs ? (Gs = [w]) : Gs.push(w), + (w = x)); + do { + switch (w.tag) { + case 3: + ((w.flags |= 65536), (o &= -o), (w.lanes |= o), ph(w, Ni(0, j, o))); + break e; + case 1: + C = j; + var ee = w.type, + ie = w.stateNode; + if ( + !( + 128 & w.flags || + ('function' != typeof ee.getDerivedStateFromError && + (null === ie || + 'function' != typeof ie.componentDidCatch || + (null !== ro && ro.has(ie)))) + ) + ) { + ((w.flags |= 65536), (o &= -o), (w.lanes |= o), ph(w, Qi(w, C, o))); + break e; + } + } + w = w.return; + } while (null !== w); + } + Sk(i); + } catch (s) { + ((o = s), qs === i && null !== i && (qs = i = i.return)); + continue; + } + break; + } + } + function Jk() { + var s = Rs.current; + return ((Rs.current = ds), null === s ? ds : s); + } + function tj() { + ((0 !== zs && 3 !== zs && 2 !== zs) || (zs = 4), + null === Fs || (!(268435455 & Ks) && !(268435455 & Hs)) || Ck(Fs, $s)); + } + function Ik(s, o) { + var i = Bs; + Bs |= 2; + var u = Jk(); + for ((Fs === s && $s === o) || ((Qs = null), Kk(s, o)); ; ) + try { + Tk(); + break; + } catch (o) { + Mk(s, o); + } + if (($g(), (Bs = i), (Rs.current = u), null !== qs)) throw Error(p(261)); + return ((Fs = null), ($s = 0), zs); + } + function Tk() { + for (; null !== qs; ) Uk(qs); + } + function Lk() { + for (; null !== qs && !pt(); ) Uk(qs); + } + function Uk(s) { + var o = Ts(s.alternate, s, Vs); + ((s.memoizedProps = s.pendingProps), + null === o ? Sk(s) : (qs = o), + (Ds.current = null)); + } + function Sk(s) { + var o = s; + do { + var i = o.alternate; + if (((s = o.return), 32768 & o.flags)) { + if (null !== (i = Ij(i, o))) return ((i.flags &= 32767), void (qs = i)); + if (null === s) return ((zs = 6), void (qs = null)); + ((s.flags |= 32768), (s.subtreeFlags = 0), (s.deletions = null)); + } else if (null !== (i = Ej(i, o, Vs))) return void (qs = i); + if (null !== (o = o.sibling)) return void (qs = o); + qs = o = s; + } while (null !== o); + 0 === zs && (zs = 5); + } + function Pk(s, o, i) { + var u = At, + _ = Ls.transition; + try { + ((Ls.transition = null), + (At = 1), + (function Wk(s, o, i, u) { + do { + Hk(); + } while (null !== so); + if (6 & Bs) throw Error(p(327)); + i = s.finishedWork; + var _ = s.finishedLanes; + if (null === i) return null; + if (((s.finishedWork = null), (s.finishedLanes = 0), i === s.current)) + throw Error(p(177)); + ((s.callbackNode = null), (s.callbackPriority = 0)); + var w = i.lanes | i.childLanes; + if ( + ((function Bc(s, o) { + var i = s.pendingLanes & ~o; + ((s.pendingLanes = o), + (s.suspendedLanes = 0), + (s.pingedLanes = 0), + (s.expiredLanes &= o), + (s.mutableReadLanes &= o), + (s.entangledLanes &= o), + (o = s.entanglements)); + var u = s.eventTimes; + for (s = s.expirationTimes; 0 < i; ) { + var _ = 31 - St(i), + w = 1 << _; + ((o[_] = 0), (u[_] = -1), (s[_] = -1), (i &= ~w)); + } + })(s, w), + s === Fs && ((qs = Fs = null), ($s = 0)), + (!(2064 & i.subtreeFlags) && !(2064 & i.flags)) || + no || + ((no = !0), + Fk(vt, function () { + return (Hk(), null); + })), + (w = !!(15990 & i.flags)), + !!(15990 & i.subtreeFlags) || w) + ) { + ((w = Ls.transition), (Ls.transition = null)); + var x = At; + At = 1; + var C = Bs; + ((Bs |= 4), + (Ds.current = null), + (function Oj(s, o) { + if (((on = zt), Ne((s = Me())))) { + if ('selectionStart' in s) + var i = { start: s.selectionStart, end: s.selectionEnd }; + else + e: { + var u = + (i = ((i = s.ownerDocument) && i.defaultView) || window) + .getSelection && i.getSelection(); + if (u && 0 !== u.rangeCount) { + i = u.anchorNode; + var _ = u.anchorOffset, + w = u.focusNode; + u = u.focusOffset; + try { + (i.nodeType, w.nodeType); + } catch (s) { + i = null; + break e; + } + var x = 0, + C = -1, + j = -1, + L = 0, + B = 0, + $ = s, + V = null; + t: for (;;) { + for ( + var U; + $ !== i || (0 !== _ && 3 !== $.nodeType) || (C = x + _), + $ !== w || (0 !== u && 3 !== $.nodeType) || (j = x + u), + 3 === $.nodeType && (x += $.nodeValue.length), + null !== (U = $.firstChild); + ) + ((V = $), ($ = U)); + for (;;) { + if ($ === s) break t; + if ( + (V === i && ++L === _ && (C = x), + V === w && ++B === u && (j = x), + null !== (U = $.nextSibling)) + ) + break; + V = ($ = V).parentNode; + } + $ = U; + } + i = -1 === C || -1 === j ? null : { start: C, end: j }; + } else i = null; + } + i = i || { start: 0, end: 0 }; + } else i = null; + for ( + an = { focusedElem: s, selectionRange: i }, zt = !1, js = o; + null !== js; + ) + if (((s = (o = js).child), 1028 & o.subtreeFlags && null !== s)) + ((s.return = o), (js = s)); + else + for (; null !== js; ) { + o = js; + try { + var z = o.alternate; + if (1024 & o.flags) + switch (o.tag) { + case 0: + case 11: + case 15: + case 5: + case 6: + case 4: + case 17: + break; + case 1: + if (null !== z) { + var Y = z.memoizedProps, + Z = z.memoizedState, + ee = o.stateNode, + ie = ee.getSnapshotBeforeUpdate( + o.elementType === o.type ? Y : Ci(o.type, Y), + Z + ); + ee.__reactInternalSnapshotBeforeUpdate = ie; + } + break; + case 3: + var ae = o.stateNode.containerInfo; + 1 === ae.nodeType + ? (ae.textContent = '') + : 9 === ae.nodeType && + ae.documentElement && + ae.removeChild(ae.documentElement); + break; + default: + throw Error(p(163)); + } + } catch (s) { + W(o, o.return, s); + } + if (null !== (s = o.sibling)) { + ((s.return = o.return), (js = s)); + break; + } + js = o.return; + } + return ((z = Is), (Is = !1), z); + })(s, i), + dk(i, s), + Oe(an), + (zt = !!on), + (an = on = null), + (s.current = i), + hk(i, s, _), + ht(), + (Bs = C), + (At = x), + (Ls.transition = w)); + } else s.current = i; + if ( + (no && ((no = !1), (so = s), (oo = _)), + (w = s.pendingLanes), + 0 === w && (ro = null), + (function mc(s) { + if (wt && 'function' == typeof wt.onCommitFiberRoot) + try { + wt.onCommitFiberRoot(Et, s, void 0, !(128 & ~s.current.flags)); + } catch (s) {} + })(i.stateNode), + Dk(s, dt()), + null !== o) + ) + for (u = s.onRecoverableError, i = 0; i < o.length; i++) + ((_ = o[i]), u(_.value, { componentStack: _.stack, digest: _.digest })); + if (eo) throw ((eo = !1), (s = to), (to = null), s); + return ( + !!(1 & oo) && 0 !== s.tag && Hk(), + (w = s.pendingLanes), + 1 & w ? (s === ao ? io++ : ((io = 0), (ao = s))) : (io = 0), + jg(), + null + ); + })(s, o, i, u)); + } finally { + ((Ls.transition = _), (At = u)); + } + return null; + } + function Hk() { + if (null !== so) { + var s = Dc(oo), + o = Ls.transition, + i = At; + try { + if (((Ls.transition = null), (At = 16 > s ? 16 : s), null === so)) var u = !1; + else { + if (((s = so), (so = null), (oo = 0), 6 & Bs)) throw Error(p(331)); + var _ = Bs; + for (Bs |= 4, js = s.current; null !== js; ) { + var w = js, + x = w.child; + if (16 & js.flags) { + var C = w.deletions; + if (null !== C) { + for (var j = 0; j < C.length; j++) { + var L = C[j]; + for (js = L; null !== js; ) { + var B = js; + switch (B.tag) { + case 0: + case 11: + case 15: + Pj(8, B, w); + } + var $ = B.child; + if (null !== $) (($.return = B), (js = $)); + else + for (; null !== js; ) { + var V = (B = js).sibling, + U = B.return; + if ((Sj(B), B === L)) { + js = null; + break; + } + if (null !== V) { + ((V.return = U), (js = V)); + break; + } + js = U; + } + } + } + var z = w.alternate; + if (null !== z) { + var Y = z.child; + if (null !== Y) { + z.child = null; + do { + var Z = Y.sibling; + ((Y.sibling = null), (Y = Z)); + } while (null !== Y); + } + } + js = w; + } + } + if (2064 & w.subtreeFlags && null !== x) ((x.return = w), (js = x)); + else + e: for (; null !== js; ) { + if (2048 & (w = js).flags) + switch (w.tag) { + case 0: + case 11: + case 15: + Pj(9, w, w.return); + } + var ee = w.sibling; + if (null !== ee) { + ((ee.return = w.return), (js = ee)); + break e; + } + js = w.return; + } + } + var ie = s.current; + for (js = ie; null !== js; ) { + var ae = (x = js).child; + if (2064 & x.subtreeFlags && null !== ae) ((ae.return = x), (js = ae)); + else + e: for (x = ie; null !== js; ) { + if (2048 & (C = js).flags) + try { + switch (C.tag) { + case 0: + case 11: + case 15: + Qj(9, C); + } + } catch (s) { + W(C, C.return, s); + } + if (C === x) { + js = null; + break e; + } + var le = C.sibling; + if (null !== le) { + ((le.return = C.return), (js = le)); + break e; + } + js = C.return; + } + } + if (((Bs = _), jg(), wt && 'function' == typeof wt.onPostCommitFiberRoot)) + try { + wt.onPostCommitFiberRoot(Et, s); + } catch (s) {} + u = !0; + } + return u; + } finally { + ((At = i), (Ls.transition = o)); + } + } + return !1; + } + function Xk(s, o, i) { + ((s = nh(s, (o = Ni(0, (o = Ji(i, o)), 1)), 1)), + (o = R()), + null !== s && (Ac(s, 1, o), Dk(s, o))); + } + function W(s, o, i) { + if (3 === s.tag) Xk(s, s, i); + else + for (; null !== o; ) { + if (3 === o.tag) { + Xk(o, s, i); + break; + } + if (1 === o.tag) { + var u = o.stateNode; + if ( + 'function' == typeof o.type.getDerivedStateFromError || + ('function' == typeof u.componentDidCatch && (null === ro || !ro.has(u))) + ) { + ((o = nh(o, (s = Qi(o, (s = Ji(i, s)), 1)), 1)), + (s = R()), + null !== o && (Ac(o, 1, s), Dk(o, s))); + break; + } + } + o = o.return; + } + } + function Ti(s, o, i) { + var u = s.pingCache; + (null !== u && u.delete(o), + (o = R()), + (s.pingedLanes |= s.suspendedLanes & i), + Fs === s && + ($s & i) === i && + (4 === zs || (3 === zs && (130023424 & $s) === $s && 500 > dt() - Xs) + ? Kk(s, 0) + : (Js |= i)), + Dk(s, o)); + } + function Yk(s, o) { + 0 === o && + (1 & s.mode ? ((o = Ot), !(130023424 & (Ot <<= 1)) && (Ot = 4194304)) : (o = 1)); + var i = R(); + null !== (s = ih(s, o)) && (Ac(s, o, i), Dk(s, i)); + } + function uj(s) { + var o = s.memoizedState, + i = 0; + (null !== o && (i = o.retryLane), Yk(s, i)); + } + function bk(s, o) { + var i = 0; + switch (s.tag) { + case 13: + var u = s.stateNode, + _ = s.memoizedState; + null !== _ && (i = _.retryLane); + break; + case 19: + u = s.stateNode; + break; + default: + throw Error(p(314)); + } + (null !== u && u.delete(o), Yk(s, i)); + } + function Fk(s, o) { + return ct(s, o); + } + function $k(s, o, i, u) { + ((this.tag = s), + (this.key = i), + (this.sibling = + this.child = + this.return = + this.stateNode = + this.type = + this.elementType = + null), + (this.index = 0), + (this.ref = null), + (this.pendingProps = o), + (this.dependencies = + this.memoizedState = + this.updateQueue = + this.memoizedProps = + null), + (this.mode = u), + (this.subtreeFlags = this.flags = 0), + (this.deletions = null), + (this.childLanes = this.lanes = 0), + (this.alternate = null)); + } + function Bg(s, o, i, u) { + return new $k(s, o, i, u); + } + function aj(s) { + return !(!(s = s.prototype) || !s.isReactComponent); + } + function Pg(s, o) { + var i = s.alternate; + return ( + null === i + ? (((i = Bg(s.tag, o, s.key, s.mode)).elementType = s.elementType), + (i.type = s.type), + (i.stateNode = s.stateNode), + (i.alternate = s), + (s.alternate = i)) + : ((i.pendingProps = o), + (i.type = s.type), + (i.flags = 0), + (i.subtreeFlags = 0), + (i.deletions = null)), + (i.flags = 14680064 & s.flags), + (i.childLanes = s.childLanes), + (i.lanes = s.lanes), + (i.child = s.child), + (i.memoizedProps = s.memoizedProps), + (i.memoizedState = s.memoizedState), + (i.updateQueue = s.updateQueue), + (o = s.dependencies), + (i.dependencies = + null === o ? null : { lanes: o.lanes, firstContext: o.firstContext }), + (i.sibling = s.sibling), + (i.index = s.index), + (i.ref = s.ref), + i + ); + } + function Rg(s, o, i, u, _, w) { + var x = 2; + if (((u = s), 'function' == typeof s)) aj(s) && (x = 1); + else if ('string' == typeof s) x = 5; + else + e: switch (s) { + case ee: + return Tg(i.children, _, w, o); + case ie: + ((x = 8), (_ |= 8)); + break; + case ae: + return (((s = Bg(12, i, o, 2 | _)).elementType = ae), (s.lanes = w), s); + case de: + return (((s = Bg(13, i, o, _)).elementType = de), (s.lanes = w), s); + case fe: + return (((s = Bg(19, i, o, _)).elementType = fe), (s.lanes = w), s); + case _e: + return pj(i, _, w, o); + default: + if ('object' == typeof s && null !== s) + switch (s.$$typeof) { + case le: + x = 10; + break e; + case ce: + x = 9; + break e; + case pe: + x = 11; + break e; + case ye: + x = 14; + break e; + case be: + ((x = 16), (u = null)); + break e; + } + throw Error(p(130, null == s ? s : typeof s, '')); + } + return (((o = Bg(x, i, o, _)).elementType = s), (o.type = u), (o.lanes = w), o); + } + function Tg(s, o, i, u) { + return (((s = Bg(7, s, u, o)).lanes = i), s); + } + function pj(s, o, i, u) { + return ( + ((s = Bg(22, s, u, o)).elementType = _e), + (s.lanes = i), + (s.stateNode = { isHidden: !1 }), + s + ); + } + function Qg(s, o, i) { + return (((s = Bg(6, s, null, o)).lanes = i), s); + } + function Sg(s, o, i) { + return ( + ((o = Bg(4, null !== s.children ? s.children : [], s.key, o)).lanes = i), + (o.stateNode = { + containerInfo: s.containerInfo, + pendingChildren: null, + implementation: s.implementation + }), + o + ); + } + function al(s, o, i, u, _) { + ((this.tag = o), + (this.containerInfo = s), + (this.finishedWork = this.pingCache = this.current = this.pendingChildren = null), + (this.timeoutHandle = -1), + (this.callbackNode = this.pendingContext = this.context = null), + (this.callbackPriority = 0), + (this.eventTimes = zc(0)), + (this.expirationTimes = zc(-1)), + (this.entangledLanes = + this.finishedLanes = + this.mutableReadLanes = + this.expiredLanes = + this.pingedLanes = + this.suspendedLanes = + this.pendingLanes = + 0), + (this.entanglements = zc(0)), + (this.identifierPrefix = u), + (this.onRecoverableError = _), + (this.mutableSourceEagerHydrationData = null)); + } + function bl(s, o, i, u, _, w, x, C, j) { + return ( + (s = new al(s, o, i, C, j)), + 1 === o ? ((o = 1), !0 === w && (o |= 8)) : (o = 0), + (w = Bg(3, null, null, o)), + (s.current = w), + (w.stateNode = s), + (w.memoizedState = { + element: u, + isDehydrated: i, + cache: null, + transitions: null, + pendingSuspenseBoundaries: null + }), + kh(w), + s + ); + } + function dl(s) { + if (!s) return En; + e: { + if (Vb((s = s._reactInternals)) !== s || 1 !== s.tag) throw Error(p(170)); + var o = s; + do { + switch (o.tag) { + case 3: + o = o.stateNode.context; + break e; + case 1: + if (Zf(o.type)) { + o = o.stateNode.__reactInternalMemoizedMergedChildContext; + break e; + } + } + o = o.return; + } while (null !== o); + throw Error(p(171)); + } + if (1 === s.tag) { + var i = s.type; + if (Zf(i)) return bg(s, i, o); + } + return o; + } + function el(s, o, i, u, _, w, x, C, j) { + return ( + ((s = bl(i, u, !0, s, 0, w, 0, C, j)).context = dl(null)), + (i = s.current), + ((w = mh((u = R()), (_ = yi(i)))).callback = null != o ? o : null), + nh(i, w, _), + (s.current.lanes = _), + Ac(s, _, u), + Dk(s, u), + s + ); + } + function fl(s, o, i, u) { + var _ = o.current, + w = R(), + x = yi(_); + return ( + (i = dl(i)), + null === o.context ? (o.context = i) : (o.pendingContext = i), + ((o = mh(w, x)).payload = { element: s }), + null !== (u = void 0 === u ? null : u) && (o.callback = u), + null !== (s = nh(_, o, x)) && (gi(s, _, x, w), oh(s, _, x)), + x + ); + } + function gl(s) { + return (s = s.current).child ? (s.child.tag, s.child.stateNode) : null; + } + function hl(s, o) { + if (null !== (s = s.memoizedState) && null !== s.dehydrated) { + var i = s.retryLane; + s.retryLane = 0 !== i && i < o ? i : o; + } + } + function il(s, o) { + (hl(s, o), (s = s.alternate) && hl(s, o)); + } + Ts = function (s, o, i) { + if (null !== s) + if (s.memoizedProps !== o.pendingProps || Sn.current) _s = !0; + else { + if (!(s.lanes & i || 128 & o.flags)) + return ( + (_s = !1), + (function yj(s, o, i) { + switch (o.tag) { + case 3: + (kj(o), Ig()); + break; + case 5: + Ah(o); + break; + case 1: + Zf(o.type) && cg(o); + break; + case 4: + yh(o, o.stateNode.containerInfo); + break; + case 10: + var u = o.type._context, + _ = o.memoizedProps.value; + (G(zn, u._currentValue), (u._currentValue = _)); + break; + case 13: + if (null !== (u = o.memoizedState)) + return null !== u.dehydrated + ? (G(es, 1 & es.current), (o.flags |= 128), null) + : i & o.child.childLanes + ? oj(s, o, i) + : (G(es, 1 & es.current), + null !== (s = Zi(s, o, i)) ? s.sibling : null); + G(es, 1 & es.current); + break; + case 19: + if (((u = !!(i & o.childLanes)), 128 & s.flags)) { + if (u) return xj(s, o, i); + o.flags |= 128; + } + if ( + (null !== (_ = o.memoizedState) && + ((_.rendering = null), (_.tail = null), (_.lastEffect = null)), + G(es, es.current), + u) + ) + break; + return null; + case 22: + case 23: + return ((o.lanes = 0), dj(s, o, i)); + } + return Zi(s, o, i); + })(s, o, i) + ); + _s = !!(131072 & s.flags); + } + else ((_s = !1), Fn && 1048576 & o.flags && ug(o, Pn, o.index)); + switch (((o.lanes = 0), o.tag)) { + case 2: + var u = o.type; + (ij(s, o), (s = o.pendingProps)); + var _ = Yf(o, wn.current); + (ch(o, i), (_ = Nh(null, o, u, s, _, i))); + var w = Sh(); + return ( + (o.flags |= 1), + 'object' == typeof _ && + null !== _ && + 'function' == typeof _.render && + void 0 === _.$$typeof + ? ((o.tag = 1), + (o.memoizedState = null), + (o.updateQueue = null), + Zf(u) ? ((w = !0), cg(o)) : (w = !1), + (o.memoizedState = null !== _.state && void 0 !== _.state ? _.state : null), + kh(o), + (_.updater = ys), + (o.stateNode = _), + (_._reactInternals = o), + Ii(o, u, s, i), + (o = jj(null, o, u, !0, w, i))) + : ((o.tag = 0), Fn && w && vg(o), Xi(null, o, _, i), (o = o.child)), + o + ); + case 16: + u = o.elementType; + e: { + switch ( + (ij(s, o), + (s = o.pendingProps), + (u = (_ = u._init)(u._payload)), + (o.type = u), + (_ = o.tag = + (function Zk(s) { + if ('function' == typeof s) return aj(s) ? 1 : 0; + if (null != s) { + if ((s = s.$$typeof) === pe) return 11; + if (s === ye) return 14; + } + return 2; + })(u)), + (s = Ci(u, s)), + _) + ) { + case 0: + o = cj(null, o, u, s, i); + break e; + case 1: + o = hj(null, o, u, s, i); + break e; + case 11: + o = Yi(null, o, u, s, i); + break e; + case 14: + o = $i(null, o, u, Ci(u.type, s), i); + break e; + } + throw Error(p(306, u, '')); + } + return o; + case 0: + return ( + (u = o.type), + (_ = o.pendingProps), + cj(s, o, u, (_ = o.elementType === u ? _ : Ci(u, _)), i) + ); + case 1: + return ( + (u = o.type), + (_ = o.pendingProps), + hj(s, o, u, (_ = o.elementType === u ? _ : Ci(u, _)), i) + ); + case 3: + e: { + if ((kj(o), null === s)) throw Error(p(387)); + ((u = o.pendingProps), + (_ = (w = o.memoizedState).element), + lh(s, o), + qh(o, u, null, i)); + var x = o.memoizedState; + if (((u = x.element), w.isDehydrated)) { + if ( + ((w = { + element: u, + isDehydrated: !1, + cache: x.cache, + pendingSuspenseBoundaries: x.pendingSuspenseBoundaries, + transitions: x.transitions + }), + (o.updateQueue.baseState = w), + (o.memoizedState = w), + 256 & o.flags) + ) { + o = lj(s, o, u, i, (_ = Ji(Error(p(423)), o))); + break e; + } + if (u !== _) { + o = lj(s, o, u, i, (_ = Ji(Error(p(424)), o))); + break e; + } + for ( + Bn = Lf(o.stateNode.containerInfo.firstChild), + Ln = o, + Fn = !0, + qn = null, + i = Un(o, null, u, i), + o.child = i; + i; + ) + ((i.flags = (-3 & i.flags) | 4096), (i = i.sibling)); + } else { + if ((Ig(), u === _)) { + o = Zi(s, o, i); + break e; + } + Xi(s, o, u, i); + } + o = o.child; + } + return o; + case 5: + return ( + Ah(o), + null === s && Eg(o), + (u = o.type), + (_ = o.pendingProps), + (w = null !== s ? s.memoizedProps : null), + (x = _.children), + Ef(u, _) ? (x = null) : null !== w && Ef(u, w) && (o.flags |= 32), + gj(s, o), + Xi(s, o, x, i), + o.child + ); + case 6: + return (null === s && Eg(o), null); + case 13: + return oj(s, o, i); + case 4: + return ( + yh(o, o.stateNode.containerInfo), + (u = o.pendingProps), + null === s ? (o.child = Vn(o, null, u, i)) : Xi(s, o, u, i), + o.child + ); + case 11: + return ( + (u = o.type), + (_ = o.pendingProps), + Yi(s, o, u, (_ = o.elementType === u ? _ : Ci(u, _)), i) + ); + case 7: + return (Xi(s, o, o.pendingProps, i), o.child); + case 8: + case 12: + return (Xi(s, o, o.pendingProps.children, i), o.child); + case 10: + e: { + if ( + ((u = o.type._context), + (_ = o.pendingProps), + (w = o.memoizedProps), + (x = _.value), + G(zn, u._currentValue), + (u._currentValue = x), + null !== w) + ) + if (Lr(w.value, x)) { + if (w.children === _.children && !Sn.current) { + o = Zi(s, o, i); + break e; + } + } else + for (null !== (w = o.child) && (w.return = o); null !== w; ) { + var C = w.dependencies; + if (null !== C) { + x = w.child; + for (var j = C.firstContext; null !== j; ) { + if (j.context === u) { + if (1 === w.tag) { + (j = mh(-1, i & -i)).tag = 2; + var L = w.updateQueue; + if (null !== L) { + var B = (L = L.shared).pending; + (null === B ? (j.next = j) : ((j.next = B.next), (B.next = j)), + (L.pending = j)); + } + } + ((w.lanes |= i), + null !== (j = w.alternate) && (j.lanes |= i), + bh(w.return, i, o), + (C.lanes |= i)); + break; + } + j = j.next; + } + } else if (10 === w.tag) x = w.type === o.type ? null : w.child; + else if (18 === w.tag) { + if (null === (x = w.return)) throw Error(p(341)); + ((x.lanes |= i), + null !== (C = x.alternate) && (C.lanes |= i), + bh(x, i, o), + (x = w.sibling)); + } else x = w.child; + if (null !== x) x.return = w; + else + for (x = w; null !== x; ) { + if (x === o) { + x = null; + break; + } + if (null !== (w = x.sibling)) { + ((w.return = x.return), (x = w)); + break; + } + x = x.return; + } + w = x; + } + (Xi(s, o, _.children, i), (o = o.child)); + } + return o; + case 9: + return ( + (_ = o.type), + (u = o.pendingProps.children), + ch(o, i), + (u = u((_ = eh(_)))), + (o.flags |= 1), + Xi(s, o, u, i), + o.child + ); + case 14: + return ( + (_ = Ci((u = o.type), o.pendingProps)), + $i(s, o, u, (_ = Ci(u.type, _)), i) + ); + case 15: + return bj(s, o, o.type, o.pendingProps, i); + case 17: + return ( + (u = o.type), + (_ = o.pendingProps), + (_ = o.elementType === u ? _ : Ci(u, _)), + ij(s, o), + (o.tag = 1), + Zf(u) ? ((s = !0), cg(o)) : (s = !1), + ch(o, i), + Gi(o, u, _), + Ii(o, u, _, i), + jj(null, o, u, !0, s, i) + ); + case 19: + return xj(s, o, i); + case 22: + return dj(s, o, i); + } + throw Error(p(156, o.tag)); + }; + var uo = + 'function' == typeof reportError + ? reportError + : function (s) { + console.error(s); + }; + function ll(s) { + this._internalRoot = s; + } + function ml(s) { + this._internalRoot = s; + } + function nl(s) { + return !(!s || (1 !== s.nodeType && 9 !== s.nodeType && 11 !== s.nodeType)); + } + function ol(s) { + return !( + !s || + (1 !== s.nodeType && + 9 !== s.nodeType && + 11 !== s.nodeType && + (8 !== s.nodeType || ' react-mount-point-unstable ' !== s.nodeValue)) + ); + } + function pl() {} + function rl(s, o, i, u, _) { + var w = i._reactRootContainer; + if (w) { + var x = w; + if ('function' == typeof _) { + var C = _; + _ = function () { + var s = gl(x); + C.call(s); + }; + } + fl(o, x, s, _); + } else + x = (function ql(s, o, i, u, _) { + if (_) { + if ('function' == typeof u) { + var w = u; + u = function () { + var s = gl(x); + w.call(s); + }; + } + var x = el(o, u, s, 0, null, !1, 0, '', pl); + return ( + (s._reactRootContainer = x), + (s[mn] = x.current), + sf(8 === s.nodeType ? s.parentNode : s), + Rk(), + x + ); + } + for (; (_ = s.lastChild); ) s.removeChild(_); + if ('function' == typeof u) { + var C = u; + u = function () { + var s = gl(j); + C.call(s); + }; + } + var j = bl(s, 0, !1, null, 0, !1, 0, '', pl); + return ( + (s._reactRootContainer = j), + (s[mn] = j.current), + sf(8 === s.nodeType ? s.parentNode : s), + Rk(function () { + fl(o, j, i, u); + }), + j + ); + })(i, o, s, _, u); + return gl(x); + } + ((ml.prototype.render = ll.prototype.render = + function (s) { + var o = this._internalRoot; + if (null === o) throw Error(p(409)); + fl(s, o, null, null); + }), + (ml.prototype.unmount = ll.prototype.unmount = + function () { + var s = this._internalRoot; + if (null !== s) { + this._internalRoot = null; + var o = s.containerInfo; + (Rk(function () { + fl(null, s, null, null); + }), + (o[mn] = null)); + } + }), + (ml.prototype.unstable_scheduleHydration = function (s) { + if (s) { + var o = Mt(); + s = { blockedOn: null, target: s, priority: o }; + for (var i = 0; i < $t.length && 0 !== o && o < $t[i].priority; i++); + ($t.splice(i, 0, s), 0 === i && Vc(s)); + } + }), + (jt = function (s) { + switch (s.tag) { + case 3: + var o = s.stateNode; + if (o.current.memoizedState.isDehydrated) { + var i = tc(o.pendingLanes); + 0 !== i && (Cc(o, 1 | i), Dk(o, dt()), !(6 & Bs) && ((Zs = dt() + 500), jg())); + } + break; + case 13: + (Rk(function () { + var o = ih(s, 1); + if (null !== o) { + var i = R(); + gi(o, s, 1, i); + } + }), + il(s, 1)); + } + }), + (It = function (s) { + if (13 === s.tag) { + var o = ih(s, 134217728); + if (null !== o) gi(o, s, 134217728, R()); + il(s, 134217728); + } + }), + (Pt = function (s) { + if (13 === s.tag) { + var o = yi(s), + i = ih(s, o); + if (null !== i) gi(i, s, o, R()); + il(s, o); + } + }), + (Mt = function () { + return At; + }), + (Tt = function (s, o) { + var i = At; + try { + return ((At = s), o()); + } finally { + At = i; + } + }), + (Xe = function (s, o, i) { + switch (o) { + case 'input': + if ((bb(s, i), (o = i.name), 'radio' === i.type && null != o)) { + for (i = s; i.parentNode; ) i = i.parentNode; + for ( + i = i.querySelectorAll( + 'input[name=' + JSON.stringify('' + o) + '][type="radio"]' + ), + o = 0; + o < i.length; + o++ + ) { + var u = i[o]; + if (u !== s && u.form === s.form) { + var _ = Db(u); + if (!_) throw Error(p(90)); + (Wa(u), bb(u, _)); + } + } + } + break; + case 'textarea': + ib(s, i); + break; + case 'select': + null != (o = i.value) && fb(s, !!i.multiple, o, !1); + } + }), + (Gb = Qk), + (Hb = Rk)); + var po = { usingClientEntryPoint: !1, Events: [Cb, ue, Db, Eb, Fb, Qk] }, + ho = { + findFiberByHostInstance: Wc, + bundleType: 0, + version: '18.3.1', + rendererPackageName: 'react-dom' + }, + fo = { + bundleType: ho.bundleType, + version: ho.version, + rendererPackageName: ho.rendererPackageName, + rendererConfig: ho.rendererConfig, + overrideHookState: null, + overrideHookStateDeletePath: null, + overrideHookStateRenamePath: null, + overrideProps: null, + overridePropsDeletePath: null, + overridePropsRenamePath: null, + setErrorHandler: null, + setSuspenseHandler: null, + scheduleUpdate: null, + currentDispatcherRef: z.ReactCurrentDispatcher, + findHostInstanceByFiber: function (s) { + return null === (s = Zb(s)) ? null : s.stateNode; + }, + findFiberByHostInstance: + ho.findFiberByHostInstance || + function jl() { + return null; + }, + findHostInstancesForRefresh: null, + scheduleRefresh: null, + scheduleRoot: null, + setRefreshHandler: null, + getCurrentFiber: null, + reconcilerVersion: '18.3.1-next-f1338f8080-20240426' + }; + if ('undefined' != typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) { + var mo = __REACT_DEVTOOLS_GLOBAL_HOOK__; + if (!mo.isDisabled && mo.supportsFiber) + try { + ((Et = mo.inject(fo)), (wt = mo)); + } catch (qe) {} + } + ((o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = po), + (o.createPortal = function (s, o) { + var i = 2 < arguments.length && void 0 !== arguments[2] ? arguments[2] : null; + if (!nl(o)) throw Error(p(200)); + return (function cl(s, o, i) { + var u = 3 < arguments.length && void 0 !== arguments[3] ? arguments[3] : null; + return { + $$typeof: Z, + key: null == u ? null : '' + u, + children: s, + containerInfo: o, + implementation: i + }; + })(s, o, null, i); + }), + (o.createRoot = function (s, o) { + if (!nl(s)) throw Error(p(299)); + var i = !1, + u = '', + _ = uo; + return ( + null != o && + (!0 === o.unstable_strictMode && (i = !0), + void 0 !== o.identifierPrefix && (u = o.identifierPrefix), + void 0 !== o.onRecoverableError && (_ = o.onRecoverableError)), + (o = bl(s, 1, !1, null, 0, i, 0, u, _)), + (s[mn] = o.current), + sf(8 === s.nodeType ? s.parentNode : s), + new ll(o) + ); + }), + (o.findDOMNode = function (s) { + if (null == s) return null; + if (1 === s.nodeType) return s; + var o = s._reactInternals; + if (void 0 === o) { + if ('function' == typeof s.render) throw Error(p(188)); + throw ((s = Object.keys(s).join(',')), Error(p(268, s))); + } + return (s = null === (s = Zb(o)) ? null : s.stateNode); + }), + (o.flushSync = function (s) { + return Rk(s); + }), + (o.hydrate = function (s, o, i) { + if (!ol(o)) throw Error(p(200)); + return rl(null, s, o, !0, i); + }), + (o.hydrateRoot = function (s, o, i) { + if (!nl(s)) throw Error(p(405)); + var u = (null != i && i.hydratedSources) || null, + _ = !1, + w = '', + x = uo; + if ( + (null != i && + (!0 === i.unstable_strictMode && (_ = !0), + void 0 !== i.identifierPrefix && (w = i.identifierPrefix), + void 0 !== i.onRecoverableError && (x = i.onRecoverableError)), + (o = el(o, null, s, 1, null != i ? i : null, _, 0, w, x)), + (s[mn] = o.current), + sf(s), + u) + ) + for (s = 0; s < u.length; s++) + ((_ = (_ = (i = u[s])._getVersion)(i._source)), + null == o.mutableSourceEagerHydrationData + ? (o.mutableSourceEagerHydrationData = [i, _]) + : o.mutableSourceEagerHydrationData.push(i, _)); + return new ml(o); + }), + (o.render = function (s, o, i) { + if (!ol(o)) throw Error(p(200)); + return rl(null, s, o, !1, i); + }), + (o.unmountComponentAtNode = function (s) { + if (!ol(s)) throw Error(p(40)); + return ( + !!s._reactRootContainer && + (Rk(function () { + rl(null, null, s, !1, function () { + ((s._reactRootContainer = null), (s[mn] = null)); + }); + }), + !0) + ); + }), + (o.unstable_batchedUpdates = Qk), + (o.unstable_renderSubtreeIntoContainer = function (s, o, i, u) { + if (!ol(i)) throw Error(p(200)); + if (null == s || void 0 === s._reactInternals) throw Error(p(38)); + return rl(s, o, i, !1, u); + }), + (o.version = '18.3.1-next-f1338f8080-20240426')); + }, + 40961: (s, o, i) => { + 'use strict'; + (!(function checkDCE() { + if ( + 'undefined' != typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && + 'function' == typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE + ) + try { + __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(checkDCE); + } catch (s) { + console.error(s); + } + })(), + (s.exports = i(22551))); + }, + 2209: (s, o, i) => { + 'use strict'; + var u, + _ = i(9404), + w = function productionTypeChecker() { + invariant(!1, 'ImmutablePropTypes type checking code is stripped in production.'); + }; + w.isRequired = w; + var x = function getProductionTypeChecker() { + return w; + }; + function getPropType(s) { + var o = typeof s; + return Array.isArray(s) + ? 'array' + : s instanceof RegExp + ? 'object' + : s instanceof _.Iterable + ? 'Immutable.' + s.toSource().split(' ')[0] + : o; + } + function createChainableTypeChecker(s) { + function checkType(o, i, u, _, w, x) { + for (var C = arguments.length, j = Array(C > 6 ? C - 6 : 0), L = 6; L < C; L++) + j[L - 6] = arguments[L]; + return ( + (x = x || u), + (_ = _ || '<>'), + null != i[u] + ? s.apply(void 0, [i, u, _, w, x].concat(j)) + : o + ? new Error('Required ' + w + ' `' + x + '` was not specified in `' + _ + '`.') + : void 0 + ); + } + var o = checkType.bind(null, !1); + return ((o.isRequired = checkType.bind(null, !0)), o); + } + function createIterableSubclassTypeChecker(s, o) { + return (function createImmutableTypeChecker(s, o) { + return createChainableTypeChecker(function validate(i, u, _, w, x) { + var C = i[u]; + if (!o(C)) { + var j = getPropType(C); + return new Error( + 'Invalid ' + + w + + ' `' + + x + + '` of type `' + + j + + '` supplied to `' + + _ + + '`, expected `' + + s + + '`.' + ); + } + return null; + }); + })('Iterable.' + s, function (s) { + return _.Iterable.isIterable(s) && o(s); + }); + } + (((u = { + listOf: x, + mapOf: x, + orderedMapOf: x, + setOf: x, + orderedSetOf: x, + stackOf: x, + iterableOf: x, + recordOf: x, + shape: x, + contains: x, + mapContains: x, + orderedMapContains: x, + list: w, + map: w, + orderedMap: w, + set: w, + orderedSet: w, + stack: w, + seq: w, + record: w, + iterable: w + }).iterable.indexed = createIterableSubclassTypeChecker('Indexed', _.Iterable.isIndexed)), + (u.iterable.keyed = createIterableSubclassTypeChecker('Keyed', _.Iterable.isKeyed)), + (s.exports = u)); + }, + 15287: (s, o) => { + 'use strict'; + var i = Symbol.for('react.element'), + u = Symbol.for('react.portal'), + _ = Symbol.for('react.fragment'), + w = Symbol.for('react.strict_mode'), + x = Symbol.for('react.profiler'), + C = Symbol.for('react.provider'), + j = Symbol.for('react.context'), + L = Symbol.for('react.forward_ref'), + B = Symbol.for('react.suspense'), + $ = Symbol.for('react.memo'), + V = Symbol.for('react.lazy'), + U = Symbol.iterator; + var z = { + isMounted: function () { + return !1; + }, + enqueueForceUpdate: function () {}, + enqueueReplaceState: function () {}, + enqueueSetState: function () {} + }, + Y = Object.assign, + Z = {}; + function E(s, o, i) { + ((this.props = s), (this.context = o), (this.refs = Z), (this.updater = i || z)); + } + function F() {} + function G(s, o, i) { + ((this.props = s), (this.context = o), (this.refs = Z), (this.updater = i || z)); + } + ((E.prototype.isReactComponent = {}), + (E.prototype.setState = function (s, o) { + if ('object' != typeof s && 'function' != typeof s && null != s) + throw Error( + 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.' + ); + this.updater.enqueueSetState(this, s, o, 'setState'); + }), + (E.prototype.forceUpdate = function (s) { + this.updater.enqueueForceUpdate(this, s, 'forceUpdate'); + }), + (F.prototype = E.prototype)); + var ee = (G.prototype = new F()); + ((ee.constructor = G), Y(ee, E.prototype), (ee.isPureReactComponent = !0)); + var ie = Array.isArray, + ae = Object.prototype.hasOwnProperty, + le = { current: null }, + ce = { key: !0, ref: !0, __self: !0, __source: !0 }; + function M(s, o, u) { + var _, + w = {}, + x = null, + C = null; + if (null != o) + for (_ in (void 0 !== o.ref && (C = o.ref), void 0 !== o.key && (x = '' + o.key), o)) + ae.call(o, _) && !ce.hasOwnProperty(_) && (w[_] = o[_]); + var j = arguments.length - 2; + if (1 === j) w.children = u; + else if (1 < j) { + for (var L = Array(j), B = 0; B < j; B++) L[B] = arguments[B + 2]; + w.children = L; + } + if (s && s.defaultProps) + for (_ in (j = s.defaultProps)) void 0 === w[_] && (w[_] = j[_]); + return { $$typeof: i, type: s, key: x, ref: C, props: w, _owner: le.current }; + } + function O(s) { + return 'object' == typeof s && null !== s && s.$$typeof === i; + } + var pe = /\/+/g; + function Q(s, o) { + return 'object' == typeof s && null !== s && null != s.key + ? (function escape(s) { + var o = { '=': '=0', ':': '=2' }; + return ( + '$' + + s.replace(/[=:]/g, function (s) { + return o[s]; + }) + ); + })('' + s.key) + : o.toString(36); + } + function R(s, o, _, w, x) { + var C = typeof s; + ('undefined' !== C && 'boolean' !== C) || (s = null); + var j = !1; + if (null === s) j = !0; + else + switch (C) { + case 'string': + case 'number': + j = !0; + break; + case 'object': + switch (s.$$typeof) { + case i: + case u: + j = !0; + } + } + if (j) + return ( + (x = x((j = s))), + (s = '' === w ? '.' + Q(j, 0) : w), + ie(x) + ? ((_ = ''), + null != s && (_ = s.replace(pe, '$&/') + '/'), + R(x, o, _, '', function (s) { + return s; + })) + : null != x && + (O(x) && + (x = (function N(s, o) { + return { + $$typeof: i, + type: s.type, + key: o, + ref: s.ref, + props: s.props, + _owner: s._owner + }; + })( + x, + _ + + (!x.key || (j && j.key === x.key) + ? '' + : ('' + x.key).replace(pe, '$&/') + '/') + + s + )), + o.push(x)), + 1 + ); + if (((j = 0), (w = '' === w ? '.' : w + ':'), ie(s))) + for (var L = 0; L < s.length; L++) { + var B = w + Q((C = s[L]), L); + j += R(C, o, _, B, x); + } + else if ( + ((B = (function A(s) { + return null === s || 'object' != typeof s + ? null + : 'function' == typeof (s = (U && s[U]) || s['@@iterator']) + ? s + : null; + })(s)), + 'function' == typeof B) + ) + for (s = B.call(s), L = 0; !(C = s.next()).done; ) + j += R((C = C.value), o, _, (B = w + Q(C, L++)), x); + else if ('object' === C) + throw ( + (o = String(s)), + Error( + 'Objects are not valid as a React child (found: ' + + ('[object Object]' === o + ? 'object with keys {' + Object.keys(s).join(', ') + '}' + : o) + + '). If you meant to render a collection of children, use an array instead.' + ) + ); + return j; + } + function S(s, o, i) { + if (null == s) return s; + var u = [], + _ = 0; + return ( + R(s, u, '', '', function (s) { + return o.call(i, s, _++); + }), + u + ); + } + function T(s) { + if (-1 === s._status) { + var o = s._result; + ((o = o()).then( + function (o) { + (0 !== s._status && -1 !== s._status) || ((s._status = 1), (s._result = o)); + }, + function (o) { + (0 !== s._status && -1 !== s._status) || ((s._status = 2), (s._result = o)); + } + ), + -1 === s._status && ((s._status = 0), (s._result = o))); + } + if (1 === s._status) return s._result.default; + throw s._result; + } + var de = { current: null }, + fe = { transition: null }, + ye = { ReactCurrentDispatcher: de, ReactCurrentBatchConfig: fe, ReactCurrentOwner: le }; + function X() { + throw Error('act(...) is not supported in production builds of React.'); + } + ((o.Children = { + map: S, + forEach: function (s, o, i) { + S( + s, + function () { + o.apply(this, arguments); + }, + i + ); + }, + count: function (s) { + var o = 0; + return ( + S(s, function () { + o++; + }), + o + ); + }, + toArray: function (s) { + return ( + S(s, function (s) { + return s; + }) || [] + ); + }, + only: function (s) { + if (!O(s)) + throw Error( + 'React.Children.only expected to receive a single React element child.' + ); + return s; + } + }), + (o.Component = E), + (o.Fragment = _), + (o.Profiler = x), + (o.PureComponent = G), + (o.StrictMode = w), + (o.Suspense = B), + (o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ye), + (o.act = X), + (o.cloneElement = function (s, o, u) { + if (null == s) + throw Error( + 'React.cloneElement(...): The argument must be a React element, but you passed ' + + s + + '.' + ); + var _ = Y({}, s.props), + w = s.key, + x = s.ref, + C = s._owner; + if (null != o) { + if ( + (void 0 !== o.ref && ((x = o.ref), (C = le.current)), + void 0 !== o.key && (w = '' + o.key), + s.type && s.type.defaultProps) + ) + var j = s.type.defaultProps; + for (L in o) + ae.call(o, L) && + !ce.hasOwnProperty(L) && + (_[L] = void 0 === o[L] && void 0 !== j ? j[L] : o[L]); + } + var L = arguments.length - 2; + if (1 === L) _.children = u; + else if (1 < L) { + j = Array(L); + for (var B = 0; B < L; B++) j[B] = arguments[B + 2]; + _.children = j; + } + return { $$typeof: i, type: s.type, key: w, ref: x, props: _, _owner: C }; + }), + (o.createContext = function (s) { + return ( + ((s = { + $$typeof: j, + _currentValue: s, + _currentValue2: s, + _threadCount: 0, + Provider: null, + Consumer: null, + _defaultValue: null, + _globalName: null + }).Provider = { $$typeof: C, _context: s }), + (s.Consumer = s) + ); + }), + (o.createElement = M), + (o.createFactory = function (s) { + var o = M.bind(null, s); + return ((o.type = s), o); + }), + (o.createRef = function () { + return { current: null }; + }), + (o.forwardRef = function (s) { + return { $$typeof: L, render: s }; + }), + (o.isValidElement = O), + (o.lazy = function (s) { + return { $$typeof: V, _payload: { _status: -1, _result: s }, _init: T }; + }), + (o.memo = function (s, o) { + return { $$typeof: $, type: s, compare: void 0 === o ? null : o }; + }), + (o.startTransition = function (s) { + var o = fe.transition; + fe.transition = {}; + try { + s(); + } finally { + fe.transition = o; + } + }), + (o.unstable_act = X), + (o.useCallback = function (s, o) { + return de.current.useCallback(s, o); + }), + (o.useContext = function (s) { + return de.current.useContext(s); + }), + (o.useDebugValue = function () {}), + (o.useDeferredValue = function (s) { + return de.current.useDeferredValue(s); + }), + (o.useEffect = function (s, o) { + return de.current.useEffect(s, o); + }), + (o.useId = function () { + return de.current.useId(); + }), + (o.useImperativeHandle = function (s, o, i) { + return de.current.useImperativeHandle(s, o, i); + }), + (o.useInsertionEffect = function (s, o) { + return de.current.useInsertionEffect(s, o); + }), + (o.useLayoutEffect = function (s, o) { + return de.current.useLayoutEffect(s, o); + }), + (o.useMemo = function (s, o) { + return de.current.useMemo(s, o); + }), + (o.useReducer = function (s, o, i) { + return de.current.useReducer(s, o, i); + }), + (o.useRef = function (s) { + return de.current.useRef(s); + }), + (o.useState = function (s) { + return de.current.useState(s); + }), + (o.useSyncExternalStore = function (s, o, i) { + return de.current.useSyncExternalStore(s, o, i); + }), + (o.useTransition = function () { + return de.current.useTransition(); + }), + (o.version = '18.3.1')); + }, + 96540: (s, o, i) => { + 'use strict'; + s.exports = i(15287); + }, + 86048: (s) => { + 'use strict'; + var o = {}; + function createErrorType(s, i, u) { + u || (u = Error); + var _ = (function (s) { + function NodeError(o, u, _) { + return ( + s.call( + this, + (function getMessage(s, o, u) { + return 'string' == typeof i ? i : i(s, o, u); + })(o, u, _) + ) || this + ); + } + return ( + (function _inheritsLoose(s, o) { + ((s.prototype = Object.create(o.prototype)), + (s.prototype.constructor = s), + (s.__proto__ = o)); + })(NodeError, s), + NodeError + ); + })(u); + ((_.prototype.name = u.name), (_.prototype.code = s), (o[s] = _)); + } + function oneOf(s, o) { + if (Array.isArray(s)) { + var i = s.length; + return ( + (s = s.map(function (s) { + return String(s); + })), + i > 2 + ? 'one of '.concat(o, ' ').concat(s.slice(0, i - 1).join(', '), ', or ') + + s[i - 1] + : 2 === i + ? 'one of '.concat(o, ' ').concat(s[0], ' or ').concat(s[1]) + : 'of '.concat(o, ' ').concat(s[0]) + ); + } + return 'of '.concat(o, ' ').concat(String(s)); + } + (createErrorType( + 'ERR_INVALID_OPT_VALUE', + function (s, o) { + return 'The value "' + o + '" is invalid for option "' + s + '"'; + }, + TypeError + ), + createErrorType( + 'ERR_INVALID_ARG_TYPE', + function (s, o, i) { + var u, _; + if ( + ('string' == typeof o && + (function startsWith(s, o, i) { + return s.substr(!i || i < 0 ? 0 : +i, o.length) === o; + })(o, 'not ') + ? ((u = 'must not be'), (o = o.replace(/^not /, ''))) + : (u = 'must be'), + (function endsWith(s, o, i) { + return ( + (void 0 === i || i > s.length) && (i = s.length), + s.substring(i - o.length, i) === o + ); + })(s, ' argument')) + ) + _ = 'The '.concat(s, ' ').concat(u, ' ').concat(oneOf(o, 'type')); + else { + var w = (function includes(s, o, i) { + return ( + 'number' != typeof i && (i = 0), + !(i + o.length > s.length) && -1 !== s.indexOf(o, i) + ); + })(s, '.') + ? 'property' + : 'argument'; + _ = 'The "' + .concat(s, '" ') + .concat(w, ' ') + .concat(u, ' ') + .concat(oneOf(o, 'type')); + } + return (_ += '. Received type '.concat(typeof i)); + }, + TypeError + ), + createErrorType('ERR_STREAM_PUSH_AFTER_EOF', 'stream.push() after EOF'), + createErrorType('ERR_METHOD_NOT_IMPLEMENTED', function (s) { + return 'The ' + s + ' method is not implemented'; + }), + createErrorType('ERR_STREAM_PREMATURE_CLOSE', 'Premature close'), + createErrorType('ERR_STREAM_DESTROYED', function (s) { + return 'Cannot call ' + s + ' after a stream was destroyed'; + }), + createErrorType('ERR_MULTIPLE_CALLBACK', 'Callback called multiple times'), + createErrorType('ERR_STREAM_CANNOT_PIPE', 'Cannot pipe, not readable'), + createErrorType('ERR_STREAM_WRITE_AFTER_END', 'write after end'), + createErrorType( + 'ERR_STREAM_NULL_VALUES', + 'May not write null values to stream', + TypeError + ), + createErrorType( + 'ERR_UNKNOWN_ENCODING', + function (s) { + return 'Unknown encoding: ' + s; + }, + TypeError + ), + createErrorType( + 'ERR_STREAM_UNSHIFT_AFTER_END_EVENT', + 'stream.unshift() after end event' + ), + (s.exports.F = o)); + }, + 25382: (s, o, i) => { + 'use strict'; + var u = i(65606), + _ = + Object.keys || + function (s) { + var o = []; + for (var i in s) o.push(i); + return o; + }; + s.exports = Duplex; + var w = i(45412), + x = i(16708); + i(56698)(Duplex, w); + for (var C = _(x.prototype), j = 0; j < C.length; j++) { + var L = C[j]; + Duplex.prototype[L] || (Duplex.prototype[L] = x.prototype[L]); + } + function Duplex(s) { + if (!(this instanceof Duplex)) return new Duplex(s); + (w.call(this, s), + x.call(this, s), + (this.allowHalfOpen = !0), + s && + (!1 === s.readable && (this.readable = !1), + !1 === s.writable && (this.writable = !1), + !1 === s.allowHalfOpen && ((this.allowHalfOpen = !1), this.once('end', onend)))); + } + function onend() { + this._writableState.ended || u.nextTick(onEndNT, this); + } + function onEndNT(s) { + s.end(); + } + (Object.defineProperty(Duplex.prototype, 'writableHighWaterMark', { + enumerable: !1, + get: function get() { + return this._writableState.highWaterMark; + } + }), + Object.defineProperty(Duplex.prototype, 'writableBuffer', { + enumerable: !1, + get: function get() { + return this._writableState && this._writableState.getBuffer(); + } + }), + Object.defineProperty(Duplex.prototype, 'writableLength', { + enumerable: !1, + get: function get() { + return this._writableState.length; + } + }), + Object.defineProperty(Duplex.prototype, 'destroyed', { + enumerable: !1, + get: function get() { + return ( + void 0 !== this._readableState && + void 0 !== this._writableState && + this._readableState.destroyed && + this._writableState.destroyed + ); + }, + set: function set(s) { + void 0 !== this._readableState && + void 0 !== this._writableState && + ((this._readableState.destroyed = s), (this._writableState.destroyed = s)); + } + })); + }, + 63600: (s, o, i) => { + 'use strict'; + s.exports = PassThrough; + var u = i(74610); + function PassThrough(s) { + if (!(this instanceof PassThrough)) return new PassThrough(s); + u.call(this, s); + } + (i(56698)(PassThrough, u), + (PassThrough.prototype._transform = function (s, o, i) { + i(null, s); + })); + }, + 45412: (s, o, i) => { + 'use strict'; + var u, + _ = i(65606); + ((s.exports = Readable), (Readable.ReadableState = ReadableState)); + i(37007).EventEmitter; + var w = function EElistenerCount(s, o) { + return s.listeners(o).length; + }, + x = i(40345), + C = i(48287).Buffer, + j = + (void 0 !== i.g + ? i.g + : 'undefined' != typeof window + ? window + : 'undefined' != typeof self + ? self + : {} + ).Uint8Array || function () {}; + var L, + B = i(79838); + L = B && B.debuglog ? B.debuglog('stream') : function debug() {}; + var $, + V, + U, + z = i(80345), + Y = i(75896), + Z = i(65291).getHighWaterMark, + ee = i(86048).F, + ie = ee.ERR_INVALID_ARG_TYPE, + ae = ee.ERR_STREAM_PUSH_AFTER_EOF, + le = ee.ERR_METHOD_NOT_IMPLEMENTED, + ce = ee.ERR_STREAM_UNSHIFT_AFTER_END_EVENT; + i(56698)(Readable, x); + var pe = Y.errorOrDestroy, + de = ['error', 'close', 'destroy', 'pause', 'resume']; + function ReadableState(s, o, _) { + ((u = u || i(25382)), + (s = s || {}), + 'boolean' != typeof _ && (_ = o instanceof u), + (this.objectMode = !!s.objectMode), + _ && (this.objectMode = this.objectMode || !!s.readableObjectMode), + (this.highWaterMark = Z(this, s, 'readableHighWaterMark', _)), + (this.buffer = new z()), + (this.length = 0), + (this.pipes = null), + (this.pipesCount = 0), + (this.flowing = null), + (this.ended = !1), + (this.endEmitted = !1), + (this.reading = !1), + (this.sync = !0), + (this.needReadable = !1), + (this.emittedReadable = !1), + (this.readableListening = !1), + (this.resumeScheduled = !1), + (this.paused = !0), + (this.emitClose = !1 !== s.emitClose), + (this.autoDestroy = !!s.autoDestroy), + (this.destroyed = !1), + (this.defaultEncoding = s.defaultEncoding || 'utf8'), + (this.awaitDrain = 0), + (this.readingMore = !1), + (this.decoder = null), + (this.encoding = null), + s.encoding && + ($ || ($ = i(83141).I), + (this.decoder = new $(s.encoding)), + (this.encoding = s.encoding))); + } + function Readable(s) { + if (((u = u || i(25382)), !(this instanceof Readable))) return new Readable(s); + var o = this instanceof u; + ((this._readableState = new ReadableState(s, this, o)), + (this.readable = !0), + s && + ('function' == typeof s.read && (this._read = s.read), + 'function' == typeof s.destroy && (this._destroy = s.destroy)), + x.call(this)); + } + function readableAddChunk(s, o, i, u, _) { + L('readableAddChunk', o); + var w, + x = s._readableState; + if (null === o) + ((x.reading = !1), + (function onEofChunk(s, o) { + if ((L('onEofChunk'), o.ended)) return; + if (o.decoder) { + var i = o.decoder.end(); + i && i.length && (o.buffer.push(i), (o.length += o.objectMode ? 1 : i.length)); + } + ((o.ended = !0), + o.sync + ? emitReadable(s) + : ((o.needReadable = !1), + o.emittedReadable || ((o.emittedReadable = !0), emitReadable_(s)))); + })(s, x)); + else if ( + (_ || + (w = (function chunkInvalid(s, o) { + var i; + (function _isUint8Array(s) { + return C.isBuffer(s) || s instanceof j; + })(o) || + 'string' == typeof o || + void 0 === o || + s.objectMode || + (i = new ie('chunk', ['string', 'Buffer', 'Uint8Array'], o)); + return i; + })(x, o)), + w) + ) + pe(s, w); + else if (x.objectMode || (o && o.length > 0)) + if ( + ('string' == typeof o || + x.objectMode || + Object.getPrototypeOf(o) === C.prototype || + (o = (function _uint8ArrayToBuffer(s) { + return C.from(s); + })(o)), + u) + ) + x.endEmitted ? pe(s, new ce()) : addChunk(s, x, o, !0); + else if (x.ended) pe(s, new ae()); + else { + if (x.destroyed) return !1; + ((x.reading = !1), + x.decoder && !i + ? ((o = x.decoder.write(o)), + x.objectMode || 0 !== o.length ? addChunk(s, x, o, !1) : maybeReadMore(s, x)) + : addChunk(s, x, o, !1)); + } + else u || ((x.reading = !1), maybeReadMore(s, x)); + return !x.ended && (x.length < x.highWaterMark || 0 === x.length); + } + function addChunk(s, o, i, u) { + (o.flowing && 0 === o.length && !o.sync + ? ((o.awaitDrain = 0), s.emit('data', i)) + : ((o.length += o.objectMode ? 1 : i.length), + u ? o.buffer.unshift(i) : o.buffer.push(i), + o.needReadable && emitReadable(s)), + maybeReadMore(s, o)); + } + (Object.defineProperty(Readable.prototype, 'destroyed', { + enumerable: !1, + get: function get() { + return void 0 !== this._readableState && this._readableState.destroyed; + }, + set: function set(s) { + this._readableState && (this._readableState.destroyed = s); + } + }), + (Readable.prototype.destroy = Y.destroy), + (Readable.prototype._undestroy = Y.undestroy), + (Readable.prototype._destroy = function (s, o) { + o(s); + }), + (Readable.prototype.push = function (s, o) { + var i, + u = this._readableState; + return ( + u.objectMode + ? (i = !0) + : 'string' == typeof s && + ((o = o || u.defaultEncoding) !== u.encoding && ((s = C.from(s, o)), (o = '')), + (i = !0)), + readableAddChunk(this, s, o, !1, i) + ); + }), + (Readable.prototype.unshift = function (s) { + return readableAddChunk(this, s, null, !0, !1); + }), + (Readable.prototype.isPaused = function () { + return !1 === this._readableState.flowing; + }), + (Readable.prototype.setEncoding = function (s) { + $ || ($ = i(83141).I); + var o = new $(s); + ((this._readableState.decoder = o), + (this._readableState.encoding = this._readableState.decoder.encoding)); + for (var u = this._readableState.buffer.head, _ = ''; null !== u; ) + ((_ += o.write(u.data)), (u = u.next)); + return ( + this._readableState.buffer.clear(), + '' !== _ && this._readableState.buffer.push(_), + (this._readableState.length = _.length), + this + ); + })); + var fe = 1073741824; + function howMuchToRead(s, o) { + return s <= 0 || (0 === o.length && o.ended) + ? 0 + : o.objectMode + ? 1 + : s != s + ? o.flowing && o.length + ? o.buffer.head.data.length + : o.length + : (s > o.highWaterMark && + (o.highWaterMark = (function computeNewHighWaterMark(s) { + return ( + s >= fe + ? (s = fe) + : (s--, + (s |= s >>> 1), + (s |= s >>> 2), + (s |= s >>> 4), + (s |= s >>> 8), + (s |= s >>> 16), + s++), + s + ); + })(s)), + s <= o.length ? s : o.ended ? o.length : ((o.needReadable = !0), 0)); + } + function emitReadable(s) { + var o = s._readableState; + (L('emitReadable', o.needReadable, o.emittedReadable), + (o.needReadable = !1), + o.emittedReadable || + (L('emitReadable', o.flowing), + (o.emittedReadable = !0), + _.nextTick(emitReadable_, s))); + } + function emitReadable_(s) { + var o = s._readableState; + (L('emitReadable_', o.destroyed, o.length, o.ended), + o.destroyed || + (!o.length && !o.ended) || + (s.emit('readable'), (o.emittedReadable = !1)), + (o.needReadable = !o.flowing && !o.ended && o.length <= o.highWaterMark), + flow(s)); + } + function maybeReadMore(s, o) { + o.readingMore || ((o.readingMore = !0), _.nextTick(maybeReadMore_, s, o)); + } + function maybeReadMore_(s, o) { + for ( + ; + !o.reading && + !o.ended && + (o.length < o.highWaterMark || (o.flowing && 0 === o.length)); + ) { + var i = o.length; + if ((L('maybeReadMore read 0'), s.read(0), i === o.length)) break; + } + o.readingMore = !1; + } + function updateReadableListening(s) { + var o = s._readableState; + ((o.readableListening = s.listenerCount('readable') > 0), + o.resumeScheduled && !o.paused + ? (o.flowing = !0) + : s.listenerCount('data') > 0 && s.resume()); + } + function nReadingNextTick(s) { + (L('readable nexttick read 0'), s.read(0)); + } + function resume_(s, o) { + (L('resume', o.reading), + o.reading || s.read(0), + (o.resumeScheduled = !1), + s.emit('resume'), + flow(s), + o.flowing && !o.reading && s.read(0)); + } + function flow(s) { + var o = s._readableState; + for (L('flow', o.flowing); o.flowing && null !== s.read(); ); + } + function fromList(s, o) { + return 0 === o.length + ? null + : (o.objectMode + ? (i = o.buffer.shift()) + : !s || s >= o.length + ? ((i = o.decoder + ? o.buffer.join('') + : 1 === o.buffer.length + ? o.buffer.first() + : o.buffer.concat(o.length)), + o.buffer.clear()) + : (i = o.buffer.consume(s, o.decoder)), + i); + var i; + } + function endReadable(s) { + var o = s._readableState; + (L('endReadable', o.endEmitted), + o.endEmitted || ((o.ended = !0), _.nextTick(endReadableNT, o, s))); + } + function endReadableNT(s, o) { + if ( + (L('endReadableNT', s.endEmitted, s.length), + !s.endEmitted && + 0 === s.length && + ((s.endEmitted = !0), (o.readable = !1), o.emit('end'), s.autoDestroy)) + ) { + var i = o._writableState; + (!i || (i.autoDestroy && i.finished)) && o.destroy(); + } + } + function indexOf(s, o) { + for (var i = 0, u = s.length; i < u; i++) if (s[i] === o) return i; + return -1; + } + ((Readable.prototype.read = function (s) { + (L('read', s), (s = parseInt(s, 10))); + var o = this._readableState, + i = s; + if ( + (0 !== s && (o.emittedReadable = !1), + 0 === s && + o.needReadable && + ((0 !== o.highWaterMark ? o.length >= o.highWaterMark : o.length > 0) || o.ended)) + ) + return ( + L('read: emitReadable', o.length, o.ended), + 0 === o.length && o.ended ? endReadable(this) : emitReadable(this), + null + ); + if (0 === (s = howMuchToRead(s, o)) && o.ended) + return (0 === o.length && endReadable(this), null); + var u, + _ = o.needReadable; + return ( + L('need readable', _), + (0 === o.length || o.length - s < o.highWaterMark) && + L('length less than watermark', (_ = !0)), + o.ended || o.reading + ? L('reading or ended', (_ = !1)) + : _ && + (L('do read'), + (o.reading = !0), + (o.sync = !0), + 0 === o.length && (o.needReadable = !0), + this._read(o.highWaterMark), + (o.sync = !1), + o.reading || (s = howMuchToRead(i, o))), + null === (u = s > 0 ? fromList(s, o) : null) + ? ((o.needReadable = o.length <= o.highWaterMark), (s = 0)) + : ((o.length -= s), (o.awaitDrain = 0)), + 0 === o.length && + (o.ended || (o.needReadable = !0), i !== s && o.ended && endReadable(this)), + null !== u && this.emit('data', u), + u + ); + }), + (Readable.prototype._read = function (s) { + pe(this, new le('_read()')); + }), + (Readable.prototype.pipe = function (s, o) { + var i = this, + u = this._readableState; + switch (u.pipesCount) { + case 0: + u.pipes = s; + break; + case 1: + u.pipes = [u.pipes, s]; + break; + default: + u.pipes.push(s); + } + ((u.pipesCount += 1), L('pipe count=%d opts=%j', u.pipesCount, o)); + var x = (!o || !1 !== o.end) && s !== _.stdout && s !== _.stderr ? onend : unpipe; + function onunpipe(o, _) { + (L('onunpipe'), + o === i && + _ && + !1 === _.hasUnpiped && + ((_.hasUnpiped = !0), + (function cleanup() { + (L('cleanup'), + s.removeListener('close', onclose), + s.removeListener('finish', onfinish), + s.removeListener('drain', C), + s.removeListener('error', onerror), + s.removeListener('unpipe', onunpipe), + i.removeListener('end', onend), + i.removeListener('end', unpipe), + i.removeListener('data', ondata), + (j = !0), + !u.awaitDrain || (s._writableState && !s._writableState.needDrain) || C()); + })())); + } + function onend() { + (L('onend'), s.end()); + } + (u.endEmitted ? _.nextTick(x) : i.once('end', x), s.on('unpipe', onunpipe)); + var C = (function pipeOnDrain(s) { + return function pipeOnDrainFunctionResult() { + var o = s._readableState; + (L('pipeOnDrain', o.awaitDrain), + o.awaitDrain && o.awaitDrain--, + 0 === o.awaitDrain && w(s, 'data') && ((o.flowing = !0), flow(s))); + }; + })(i); + s.on('drain', C); + var j = !1; + function ondata(o) { + L('ondata'); + var _ = s.write(o); + (L('dest.write', _), + !1 === _ && + (((1 === u.pipesCount && u.pipes === s) || + (u.pipesCount > 1 && -1 !== indexOf(u.pipes, s))) && + !j && + (L('false write response, pause', u.awaitDrain), u.awaitDrain++), + i.pause())); + } + function onerror(o) { + (L('onerror', o), + unpipe(), + s.removeListener('error', onerror), + 0 === w(s, 'error') && pe(s, o)); + } + function onclose() { + (s.removeListener('finish', onfinish), unpipe()); + } + function onfinish() { + (L('onfinish'), s.removeListener('close', onclose), unpipe()); + } + function unpipe() { + (L('unpipe'), i.unpipe(s)); + } + return ( + i.on('data', ondata), + (function prependListener(s, o, i) { + if ('function' == typeof s.prependListener) return s.prependListener(o, i); + s._events && s._events[o] + ? Array.isArray(s._events[o]) + ? s._events[o].unshift(i) + : (s._events[o] = [i, s._events[o]]) + : s.on(o, i); + })(s, 'error', onerror), + s.once('close', onclose), + s.once('finish', onfinish), + s.emit('pipe', i), + u.flowing || (L('pipe resume'), i.resume()), + s + ); + }), + (Readable.prototype.unpipe = function (s) { + var o = this._readableState, + i = { hasUnpiped: !1 }; + if (0 === o.pipesCount) return this; + if (1 === o.pipesCount) + return ( + (s && s !== o.pipes) || + (s || (s = o.pipes), + (o.pipes = null), + (o.pipesCount = 0), + (o.flowing = !1), + s && s.emit('unpipe', this, i)), + this + ); + if (!s) { + var u = o.pipes, + _ = o.pipesCount; + ((o.pipes = null), (o.pipesCount = 0), (o.flowing = !1)); + for (var w = 0; w < _; w++) u[w].emit('unpipe', this, { hasUnpiped: !1 }); + return this; + } + var x = indexOf(o.pipes, s); + return ( + -1 === x || + (o.pipes.splice(x, 1), + (o.pipesCount -= 1), + 1 === o.pipesCount && (o.pipes = o.pipes[0]), + s.emit('unpipe', this, i)), + this + ); + }), + (Readable.prototype.on = function (s, o) { + var i = x.prototype.on.call(this, s, o), + u = this._readableState; + return ( + 'data' === s + ? ((u.readableListening = this.listenerCount('readable') > 0), + !1 !== u.flowing && this.resume()) + : 'readable' === s && + (u.endEmitted || + u.readableListening || + ((u.readableListening = u.needReadable = !0), + (u.flowing = !1), + (u.emittedReadable = !1), + L('on readable', u.length, u.reading), + u.length + ? emitReadable(this) + : u.reading || _.nextTick(nReadingNextTick, this))), + i + ); + }), + (Readable.prototype.addListener = Readable.prototype.on), + (Readable.prototype.removeListener = function (s, o) { + var i = x.prototype.removeListener.call(this, s, o); + return ('readable' === s && _.nextTick(updateReadableListening, this), i); + }), + (Readable.prototype.removeAllListeners = function (s) { + var o = x.prototype.removeAllListeners.apply(this, arguments); + return ( + ('readable' !== s && void 0 !== s) || _.nextTick(updateReadableListening, this), + o + ); + }), + (Readable.prototype.resume = function () { + var s = this._readableState; + return ( + s.flowing || + (L('resume'), + (s.flowing = !s.readableListening), + (function resume(s, o) { + o.resumeScheduled || ((o.resumeScheduled = !0), _.nextTick(resume_, s, o)); + })(this, s)), + (s.paused = !1), + this + ); + }), + (Readable.prototype.pause = function () { + return ( + L('call pause flowing=%j', this._readableState.flowing), + !1 !== this._readableState.flowing && + (L('pause'), (this._readableState.flowing = !1), this.emit('pause')), + (this._readableState.paused = !0), + this + ); + }), + (Readable.prototype.wrap = function (s) { + var o = this, + i = this._readableState, + u = !1; + for (var _ in (s.on('end', function () { + if ((L('wrapped end'), i.decoder && !i.ended)) { + var s = i.decoder.end(); + s && s.length && o.push(s); + } + o.push(null); + }), + s.on('data', function (_) { + (L('wrapped data'), + i.decoder && (_ = i.decoder.write(_)), + i.objectMode && null == _) || + ((i.objectMode || (_ && _.length)) && (o.push(_) || ((u = !0), s.pause()))); + }), + s)) + void 0 === this[_] && + 'function' == typeof s[_] && + (this[_] = (function methodWrap(o) { + return function methodWrapReturnFunction() { + return s[o].apply(s, arguments); + }; + })(_)); + for (var w = 0; w < de.length; w++) s.on(de[w], this.emit.bind(this, de[w])); + return ( + (this._read = function (o) { + (L('wrapped _read', o), u && ((u = !1), s.resume())); + }), + this + ); + }), + 'function' == typeof Symbol && + (Readable.prototype[Symbol.asyncIterator] = function () { + return (void 0 === V && (V = i(2955)), V(this)); + }), + Object.defineProperty(Readable.prototype, 'readableHighWaterMark', { + enumerable: !1, + get: function get() { + return this._readableState.highWaterMark; + } + }), + Object.defineProperty(Readable.prototype, 'readableBuffer', { + enumerable: !1, + get: function get() { + return this._readableState && this._readableState.buffer; + } + }), + Object.defineProperty(Readable.prototype, 'readableFlowing', { + enumerable: !1, + get: function get() { + return this._readableState.flowing; + }, + set: function set(s) { + this._readableState && (this._readableState.flowing = s); + } + }), + (Readable._fromList = fromList), + Object.defineProperty(Readable.prototype, 'readableLength', { + enumerable: !1, + get: function get() { + return this._readableState.length; + } + }), + 'function' == typeof Symbol && + (Readable.from = function (s, o) { + return (void 0 === U && (U = i(55157)), U(Readable, s, o)); + })); + }, + 74610: (s, o, i) => { + 'use strict'; + s.exports = Transform; + var u = i(86048).F, + _ = u.ERR_METHOD_NOT_IMPLEMENTED, + w = u.ERR_MULTIPLE_CALLBACK, + x = u.ERR_TRANSFORM_ALREADY_TRANSFORMING, + C = u.ERR_TRANSFORM_WITH_LENGTH_0, + j = i(25382); + function afterTransform(s, o) { + var i = this._transformState; + i.transforming = !1; + var u = i.writecb; + if (null === u) return this.emit('error', new w()); + ((i.writechunk = null), (i.writecb = null), null != o && this.push(o), u(s)); + var _ = this._readableState; + ((_.reading = !1), + (_.needReadable || _.length < _.highWaterMark) && this._read(_.highWaterMark)); + } + function Transform(s) { + if (!(this instanceof Transform)) return new Transform(s); + (j.call(this, s), + (this._transformState = { + afterTransform: afterTransform.bind(this), + needTransform: !1, + transforming: !1, + writecb: null, + writechunk: null, + writeencoding: null + }), + (this._readableState.needReadable = !0), + (this._readableState.sync = !1), + s && + ('function' == typeof s.transform && (this._transform = s.transform), + 'function' == typeof s.flush && (this._flush = s.flush)), + this.on('prefinish', prefinish)); + } + function prefinish() { + var s = this; + 'function' != typeof this._flush || this._readableState.destroyed + ? done(this, null, null) + : this._flush(function (o, i) { + done(s, o, i); + }); + } + function done(s, o, i) { + if (o) return s.emit('error', o); + if ((null != i && s.push(i), s._writableState.length)) throw new C(); + if (s._transformState.transforming) throw new x(); + return s.push(null); + } + (i(56698)(Transform, j), + (Transform.prototype.push = function (s, o) { + return ((this._transformState.needTransform = !1), j.prototype.push.call(this, s, o)); + }), + (Transform.prototype._transform = function (s, o, i) { + i(new _('_transform()')); + }), + (Transform.prototype._write = function (s, o, i) { + var u = this._transformState; + if (((u.writecb = i), (u.writechunk = s), (u.writeencoding = o), !u.transforming)) { + var _ = this._readableState; + (u.needTransform || _.needReadable || _.length < _.highWaterMark) && + this._read(_.highWaterMark); + } + }), + (Transform.prototype._read = function (s) { + var o = this._transformState; + null === o.writechunk || o.transforming + ? (o.needTransform = !0) + : ((o.transforming = !0), + this._transform(o.writechunk, o.writeencoding, o.afterTransform)); + }), + (Transform.prototype._destroy = function (s, o) { + j.prototype._destroy.call(this, s, function (s) { + o(s); + }); + })); + }, + 16708: (s, o, i) => { + 'use strict'; + var u, + _ = i(65606); + function CorkedRequest(s) { + var o = this; + ((this.next = null), + (this.entry = null), + (this.finish = function () { + !(function onCorkedFinish(s, o, i) { + var u = s.entry; + s.entry = null; + for (; u; ) { + var _ = u.callback; + (o.pendingcb--, _(i), (u = u.next)); + } + o.corkedRequestsFree.next = s; + })(o, s); + })); + } + ((s.exports = Writable), (Writable.WritableState = WritableState)); + var w = { deprecate: i(94643) }, + x = i(40345), + C = i(48287).Buffer, + j = + (void 0 !== i.g + ? i.g + : 'undefined' != typeof window + ? window + : 'undefined' != typeof self + ? self + : {} + ).Uint8Array || function () {}; + var L, + B = i(75896), + $ = i(65291).getHighWaterMark, + V = i(86048).F, + U = V.ERR_INVALID_ARG_TYPE, + z = V.ERR_METHOD_NOT_IMPLEMENTED, + Y = V.ERR_MULTIPLE_CALLBACK, + Z = V.ERR_STREAM_CANNOT_PIPE, + ee = V.ERR_STREAM_DESTROYED, + ie = V.ERR_STREAM_NULL_VALUES, + ae = V.ERR_STREAM_WRITE_AFTER_END, + le = V.ERR_UNKNOWN_ENCODING, + ce = B.errorOrDestroy; + function nop() {} + function WritableState(s, o, w) { + ((u = u || i(25382)), + (s = s || {}), + 'boolean' != typeof w && (w = o instanceof u), + (this.objectMode = !!s.objectMode), + w && (this.objectMode = this.objectMode || !!s.writableObjectMode), + (this.highWaterMark = $(this, s, 'writableHighWaterMark', w)), + (this.finalCalled = !1), + (this.needDrain = !1), + (this.ending = !1), + (this.ended = !1), + (this.finished = !1), + (this.destroyed = !1)); + var x = !1 === s.decodeStrings; + ((this.decodeStrings = !x), + (this.defaultEncoding = s.defaultEncoding || 'utf8'), + (this.length = 0), + (this.writing = !1), + (this.corked = 0), + (this.sync = !0), + (this.bufferProcessing = !1), + (this.onwrite = function (s) { + !(function onwrite(s, o) { + var i = s._writableState, + u = i.sync, + w = i.writecb; + if ('function' != typeof w) throw new Y(); + if ( + ((function onwriteStateUpdate(s) { + ((s.writing = !1), + (s.writecb = null), + (s.length -= s.writelen), + (s.writelen = 0)); + })(i), + o) + ) + !(function onwriteError(s, o, i, u, w) { + (--o.pendingcb, + i + ? (_.nextTick(w, u), + _.nextTick(finishMaybe, s, o), + (s._writableState.errorEmitted = !0), + ce(s, u)) + : (w(u), + (s._writableState.errorEmitted = !0), + ce(s, u), + finishMaybe(s, o))); + })(s, i, u, o, w); + else { + var x = needFinish(i) || s.destroyed; + (x || i.corked || i.bufferProcessing || !i.bufferedRequest || clearBuffer(s, i), + u ? _.nextTick(afterWrite, s, i, x, w) : afterWrite(s, i, x, w)); + } + })(o, s); + }), + (this.writecb = null), + (this.writelen = 0), + (this.bufferedRequest = null), + (this.lastBufferedRequest = null), + (this.pendingcb = 0), + (this.prefinished = !1), + (this.errorEmitted = !1), + (this.emitClose = !1 !== s.emitClose), + (this.autoDestroy = !!s.autoDestroy), + (this.bufferedRequestCount = 0), + (this.corkedRequestsFree = new CorkedRequest(this))); + } + function Writable(s) { + var o = this instanceof (u = u || i(25382)); + if (!o && !L.call(Writable, this)) return new Writable(s); + ((this._writableState = new WritableState(s, this, o)), + (this.writable = !0), + s && + ('function' == typeof s.write && (this._write = s.write), + 'function' == typeof s.writev && (this._writev = s.writev), + 'function' == typeof s.destroy && (this._destroy = s.destroy), + 'function' == typeof s.final && (this._final = s.final)), + x.call(this)); + } + function doWrite(s, o, i, u, _, w, x) { + ((o.writelen = u), + (o.writecb = x), + (o.writing = !0), + (o.sync = !0), + o.destroyed + ? o.onwrite(new ee('write')) + : i + ? s._writev(_, o.onwrite) + : s._write(_, w, o.onwrite), + (o.sync = !1)); + } + function afterWrite(s, o, i, u) { + (i || + (function onwriteDrain(s, o) { + 0 === o.length && o.needDrain && ((o.needDrain = !1), s.emit('drain')); + })(s, o), + o.pendingcb--, + u(), + finishMaybe(s, o)); + } + function clearBuffer(s, o) { + o.bufferProcessing = !0; + var i = o.bufferedRequest; + if (s._writev && i && i.next) { + var u = o.bufferedRequestCount, + _ = new Array(u), + w = o.corkedRequestsFree; + w.entry = i; + for (var x = 0, C = !0; i; ) + ((_[x] = i), i.isBuf || (C = !1), (i = i.next), (x += 1)); + ((_.allBuffers = C), + doWrite(s, o, !0, o.length, _, '', w.finish), + o.pendingcb++, + (o.lastBufferedRequest = null), + w.next + ? ((o.corkedRequestsFree = w.next), (w.next = null)) + : (o.corkedRequestsFree = new CorkedRequest(o)), + (o.bufferedRequestCount = 0)); + } else { + for (; i; ) { + var j = i.chunk, + L = i.encoding, + B = i.callback; + if ( + (doWrite(s, o, !1, o.objectMode ? 1 : j.length, j, L, B), + (i = i.next), + o.bufferedRequestCount--, + o.writing) + ) + break; + } + null === i && (o.lastBufferedRequest = null); + } + ((o.bufferedRequest = i), (o.bufferProcessing = !1)); + } + function needFinish(s) { + return ( + s.ending && 0 === s.length && null === s.bufferedRequest && !s.finished && !s.writing + ); + } + function callFinal(s, o) { + s._final(function (i) { + (o.pendingcb--, + i && ce(s, i), + (o.prefinished = !0), + s.emit('prefinish'), + finishMaybe(s, o)); + }); + } + function finishMaybe(s, o) { + var i = needFinish(o); + if ( + i && + ((function prefinish(s, o) { + o.prefinished || + o.finalCalled || + ('function' != typeof s._final || o.destroyed + ? ((o.prefinished = !0), s.emit('prefinish')) + : (o.pendingcb++, (o.finalCalled = !0), _.nextTick(callFinal, s, o))); + })(s, o), + 0 === o.pendingcb && ((o.finished = !0), s.emit('finish'), o.autoDestroy)) + ) { + var u = s._readableState; + (!u || (u.autoDestroy && u.endEmitted)) && s.destroy(); + } + return i; + } + (i(56698)(Writable, x), + (WritableState.prototype.getBuffer = function getBuffer() { + for (var s = this.bufferedRequest, o = []; s; ) (o.push(s), (s = s.next)); + return o; + }), + (function () { + try { + Object.defineProperty(WritableState.prototype, 'buffer', { + get: w.deprecate( + function writableStateBufferGetter() { + return this.getBuffer(); + }, + '_writableState.buffer is deprecated. Use _writableState.getBuffer instead.', + 'DEP0003' + ) + }); + } catch (s) {} + })(), + 'function' == typeof Symbol && + Symbol.hasInstance && + 'function' == typeof Function.prototype[Symbol.hasInstance] + ? ((L = Function.prototype[Symbol.hasInstance]), + Object.defineProperty(Writable, Symbol.hasInstance, { + value: function value(s) { + return ( + !!L.call(this, s) || + (this === Writable && s && s._writableState instanceof WritableState) + ); + } + })) + : (L = function realHasInstance(s) { + return s instanceof this; + }), + (Writable.prototype.pipe = function () { + ce(this, new Z()); + }), + (Writable.prototype.write = function (s, o, i) { + var u = this._writableState, + w = !1, + x = + !u.objectMode && + (function _isUint8Array(s) { + return C.isBuffer(s) || s instanceof j; + })(s); + return ( + x && + !C.isBuffer(s) && + (s = (function _uint8ArrayToBuffer(s) { + return C.from(s); + })(s)), + 'function' == typeof o && ((i = o), (o = null)), + x ? (o = 'buffer') : o || (o = u.defaultEncoding), + 'function' != typeof i && (i = nop), + u.ending + ? (function writeAfterEnd(s, o) { + var i = new ae(); + (ce(s, i), _.nextTick(o, i)); + })(this, i) + : (x || + (function validChunk(s, o, i, u) { + var w; + return ( + null === i + ? (w = new ie()) + : 'string' == typeof i || + o.objectMode || + (w = new U('chunk', ['string', 'Buffer'], i)), + !w || (ce(s, w), _.nextTick(u, w), !1) + ); + })(this, u, s, i)) && + (u.pendingcb++, + (w = (function writeOrBuffer(s, o, i, u, _, w) { + if (!i) { + var x = (function decodeChunk(s, o, i) { + s.objectMode || + !1 === s.decodeStrings || + 'string' != typeof o || + (o = C.from(o, i)); + return o; + })(o, u, _); + u !== x && ((i = !0), (_ = 'buffer'), (u = x)); + } + var j = o.objectMode ? 1 : u.length; + o.length += j; + var L = o.length < o.highWaterMark; + L || (o.needDrain = !0); + if (o.writing || o.corked) { + var B = o.lastBufferedRequest; + ((o.lastBufferedRequest = { + chunk: u, + encoding: _, + isBuf: i, + callback: w, + next: null + }), + B + ? (B.next = o.lastBufferedRequest) + : (o.bufferedRequest = o.lastBufferedRequest), + (o.bufferedRequestCount += 1)); + } else doWrite(s, o, !1, j, u, _, w); + return L; + })(this, u, x, s, o, i))), + w + ); + }), + (Writable.prototype.cork = function () { + this._writableState.corked++; + }), + (Writable.prototype.uncork = function () { + var s = this._writableState; + s.corked && + (s.corked--, + s.writing || + s.corked || + s.bufferProcessing || + !s.bufferedRequest || + clearBuffer(this, s)); + }), + (Writable.prototype.setDefaultEncoding = function setDefaultEncoding(s) { + if ( + ('string' == typeof s && (s = s.toLowerCase()), + !( + [ + 'hex', + 'utf8', + 'utf-8', + 'ascii', + 'binary', + 'base64', + 'ucs2', + 'ucs-2', + 'utf16le', + 'utf-16le', + 'raw' + ].indexOf((s + '').toLowerCase()) > -1 + )) + ) + throw new le(s); + return ((this._writableState.defaultEncoding = s), this); + }), + Object.defineProperty(Writable.prototype, 'writableBuffer', { + enumerable: !1, + get: function get() { + return this._writableState && this._writableState.getBuffer(); + } + }), + Object.defineProperty(Writable.prototype, 'writableHighWaterMark', { + enumerable: !1, + get: function get() { + return this._writableState.highWaterMark; + } + }), + (Writable.prototype._write = function (s, o, i) { + i(new z('_write()')); + }), + (Writable.prototype._writev = null), + (Writable.prototype.end = function (s, o, i) { + var u = this._writableState; + return ( + 'function' == typeof s + ? ((i = s), (s = null), (o = null)) + : 'function' == typeof o && ((i = o), (o = null)), + null != s && this.write(s, o), + u.corked && ((u.corked = 1), this.uncork()), + u.ending || + (function endWritable(s, o, i) { + ((o.ending = !0), + finishMaybe(s, o), + i && (o.finished ? _.nextTick(i) : s.once('finish', i))); + ((o.ended = !0), (s.writable = !1)); + })(this, u, i), + this + ); + }), + Object.defineProperty(Writable.prototype, 'writableLength', { + enumerable: !1, + get: function get() { + return this._writableState.length; + } + }), + Object.defineProperty(Writable.prototype, 'destroyed', { + enumerable: !1, + get: function get() { + return void 0 !== this._writableState && this._writableState.destroyed; + }, + set: function set(s) { + this._writableState && (this._writableState.destroyed = s); + } + }), + (Writable.prototype.destroy = B.destroy), + (Writable.prototype._undestroy = B.undestroy), + (Writable.prototype._destroy = function (s, o) { + o(s); + })); + }, + 2955: (s, o, i) => { + 'use strict'; + var u, + _ = i(65606); + function _defineProperty(s, o, i) { + return ( + (o = (function _toPropertyKey(s) { + var o = (function _toPrimitive(s, o) { + if ('object' != typeof s || null === s) return s; + var i = s[Symbol.toPrimitive]; + if (void 0 !== i) { + var u = i.call(s, o || 'default'); + if ('object' != typeof u) return u; + throw new TypeError('@@toPrimitive must return a primitive value.'); + } + return ('string' === o ? String : Number)(s); + })(s, 'string'); + return 'symbol' == typeof o ? o : String(o); + })(o)) in s + ? Object.defineProperty(s, o, { + value: i, + enumerable: !0, + configurable: !0, + writable: !0 + }) + : (s[o] = i), + s + ); + } + var w = i(86238), + x = Symbol('lastResolve'), + C = Symbol('lastReject'), + j = Symbol('error'), + L = Symbol('ended'), + B = Symbol('lastPromise'), + $ = Symbol('handlePromise'), + V = Symbol('stream'); + function createIterResult(s, o) { + return { value: s, done: o }; + } + function readAndResolve(s) { + var o = s[x]; + if (null !== o) { + var i = s[V].read(); + null !== i && + ((s[B] = null), (s[x] = null), (s[C] = null), o(createIterResult(i, !1))); + } + } + function onReadable(s) { + _.nextTick(readAndResolve, s); + } + var U = Object.getPrototypeOf(function () {}), + z = Object.setPrototypeOf( + (_defineProperty( + (u = { + get stream() { + return this[V]; + }, + next: function next() { + var s = this, + o = this[j]; + if (null !== o) return Promise.reject(o); + if (this[L]) return Promise.resolve(createIterResult(void 0, !0)); + if (this[V].destroyed) + return new Promise(function (o, i) { + _.nextTick(function () { + s[j] ? i(s[j]) : o(createIterResult(void 0, !0)); + }); + }); + var i, + u = this[B]; + if (u) + i = new Promise( + (function wrapForNext(s, o) { + return function (i, u) { + s.then(function () { + o[L] ? i(createIterResult(void 0, !0)) : o[$](i, u); + }, u); + }; + })(u, this) + ); + else { + var w = this[V].read(); + if (null !== w) return Promise.resolve(createIterResult(w, !1)); + i = new Promise(this[$]); + } + return ((this[B] = i), i); + } + }), + Symbol.asyncIterator, + function () { + return this; + } + ), + _defineProperty(u, 'return', function _return() { + var s = this; + return new Promise(function (o, i) { + s[V].destroy(null, function (s) { + s ? i(s) : o(createIterResult(void 0, !0)); + }); + }); + }), + u), + U + ); + s.exports = function createReadableStreamAsyncIterator(s) { + var o, + i = Object.create( + z, + (_defineProperty((o = {}), V, { value: s, writable: !0 }), + _defineProperty(o, x, { value: null, writable: !0 }), + _defineProperty(o, C, { value: null, writable: !0 }), + _defineProperty(o, j, { value: null, writable: !0 }), + _defineProperty(o, L, { value: s._readableState.endEmitted, writable: !0 }), + _defineProperty(o, $, { + value: function value(s, o) { + var u = i[V].read(); + u + ? ((i[B] = null), (i[x] = null), (i[C] = null), s(createIterResult(u, !1))) + : ((i[x] = s), (i[C] = o)); + }, + writable: !0 + }), + o) + ); + return ( + (i[B] = null), + w(s, function (s) { + if (s && 'ERR_STREAM_PREMATURE_CLOSE' !== s.code) { + var o = i[C]; + return ( + null !== o && ((i[B] = null), (i[x] = null), (i[C] = null), o(s)), + void (i[j] = s) + ); + } + var u = i[x]; + (null !== u && + ((i[B] = null), (i[x] = null), (i[C] = null), u(createIterResult(void 0, !0))), + (i[L] = !0)); + }), + s.on('readable', onReadable.bind(null, i)), + i + ); + }; + }, + 80345: (s, o, i) => { + 'use strict'; + function ownKeys(s, o) { + var i = Object.keys(s); + if (Object.getOwnPropertySymbols) { + var u = Object.getOwnPropertySymbols(s); + (o && + (u = u.filter(function (o) { + return Object.getOwnPropertyDescriptor(s, o).enumerable; + })), + i.push.apply(i, u)); + } + return i; + } + function _objectSpread(s) { + for (var o = 1; o < arguments.length; o++) { + var i = null != arguments[o] ? arguments[o] : {}; + o % 2 + ? ownKeys(Object(i), !0).forEach(function (o) { + _defineProperty(s, o, i[o]); + }) + : Object.getOwnPropertyDescriptors + ? Object.defineProperties(s, Object.getOwnPropertyDescriptors(i)) + : ownKeys(Object(i)).forEach(function (o) { + Object.defineProperty(s, o, Object.getOwnPropertyDescriptor(i, o)); + }); + } + return s; + } + function _defineProperty(s, o, i) { + return ( + (o = _toPropertyKey(o)) in s + ? Object.defineProperty(s, o, { + value: i, + enumerable: !0, + configurable: !0, + writable: !0 + }) + : (s[o] = i), + s + ); + } + function _defineProperties(s, o) { + for (var i = 0; i < o.length; i++) { + var u = o[i]; + ((u.enumerable = u.enumerable || !1), + (u.configurable = !0), + 'value' in u && (u.writable = !0), + Object.defineProperty(s, _toPropertyKey(u.key), u)); + } + } + function _toPropertyKey(s) { + var o = (function _toPrimitive(s, o) { + if ('object' != typeof s || null === s) return s; + var i = s[Symbol.toPrimitive]; + if (void 0 !== i) { + var u = i.call(s, o || 'default'); + if ('object' != typeof u) return u; + throw new TypeError('@@toPrimitive must return a primitive value.'); + } + return ('string' === o ? String : Number)(s); + })(s, 'string'); + return 'symbol' == typeof o ? o : String(o); + } + var u = i(48287).Buffer, + _ = i(15340).inspect, + w = (_ && _.custom) || 'inspect'; + s.exports = (function () { + function BufferList() { + (!(function _classCallCheck(s, o) { + if (!(s instanceof o)) throw new TypeError('Cannot call a class as a function'); + })(this, BufferList), + (this.head = null), + (this.tail = null), + (this.length = 0)); + } + return ( + (function _createClass(s, o, i) { + return ( + o && _defineProperties(s.prototype, o), + i && _defineProperties(s, i), + Object.defineProperty(s, 'prototype', { writable: !1 }), + s + ); + })(BufferList, [ + { + key: 'push', + value: function push(s) { + var o = { data: s, next: null }; + (this.length > 0 ? (this.tail.next = o) : (this.head = o), + (this.tail = o), + ++this.length); + } + }, + { + key: 'unshift', + value: function unshift(s) { + var o = { data: s, next: this.head }; + (0 === this.length && (this.tail = o), (this.head = o), ++this.length); + } + }, + { + key: 'shift', + value: function shift() { + if (0 !== this.length) { + var s = this.head.data; + return ( + 1 === this.length + ? (this.head = this.tail = null) + : (this.head = this.head.next), + --this.length, + s + ); + } + } + }, + { + key: 'clear', + value: function clear() { + ((this.head = this.tail = null), (this.length = 0)); + } + }, + { + key: 'join', + value: function join(s) { + if (0 === this.length) return ''; + for (var o = this.head, i = '' + o.data; (o = o.next); ) i += s + o.data; + return i; + } + }, + { + key: 'concat', + value: function concat(s) { + if (0 === this.length) return u.alloc(0); + for (var o, i, _, w = u.allocUnsafe(s >>> 0), x = this.head, C = 0; x; ) + ((o = x.data), + (i = w), + (_ = C), + u.prototype.copy.call(o, i, _), + (C += x.data.length), + (x = x.next)); + return w; + } + }, + { + key: 'consume', + value: function consume(s, o) { + var i; + return ( + s < this.head.data.length + ? ((i = this.head.data.slice(0, s)), + (this.head.data = this.head.data.slice(s))) + : (i = + s === this.head.data.length + ? this.shift() + : o + ? this._getString(s) + : this._getBuffer(s)), + i + ); + } + }, + { + key: 'first', + value: function first() { + return this.head.data; + } + }, + { + key: '_getString', + value: function _getString(s) { + var o = this.head, + i = 1, + u = o.data; + for (s -= u.length; (o = o.next); ) { + var _ = o.data, + w = s > _.length ? _.length : s; + if ((w === _.length ? (u += _) : (u += _.slice(0, s)), 0 === (s -= w))) { + w === _.length + ? (++i, o.next ? (this.head = o.next) : (this.head = this.tail = null)) + : ((this.head = o), (o.data = _.slice(w))); + break; + } + ++i; + } + return ((this.length -= i), u); + } + }, + { + key: '_getBuffer', + value: function _getBuffer(s) { + var o = u.allocUnsafe(s), + i = this.head, + _ = 1; + for (i.data.copy(o), s -= i.data.length; (i = i.next); ) { + var w = i.data, + x = s > w.length ? w.length : s; + if ((w.copy(o, o.length - s, 0, x), 0 === (s -= x))) { + x === w.length + ? (++_, i.next ? (this.head = i.next) : (this.head = this.tail = null)) + : ((this.head = i), (i.data = w.slice(x))); + break; + } + ++_; + } + return ((this.length -= _), o); + } + }, + { + key: w, + value: function value(s, o) { + return _( + this, + _objectSpread(_objectSpread({}, o), {}, { depth: 0, customInspect: !1 }) + ); + } + } + ]), + BufferList + ); + })(); + }, + 75896: (s, o, i) => { + 'use strict'; + var u = i(65606); + function emitErrorAndCloseNT(s, o) { + (emitErrorNT(s, o), emitCloseNT(s)); + } + function emitCloseNT(s) { + (s._writableState && !s._writableState.emitClose) || + (s._readableState && !s._readableState.emitClose) || + s.emit('close'); + } + function emitErrorNT(s, o) { + s.emit('error', o); + } + s.exports = { + destroy: function destroy(s, o) { + var i = this, + _ = this._readableState && this._readableState.destroyed, + w = this._writableState && this._writableState.destroyed; + return _ || w + ? (o + ? o(s) + : s && + (this._writableState + ? this._writableState.errorEmitted || + ((this._writableState.errorEmitted = !0), + u.nextTick(emitErrorNT, this, s)) + : u.nextTick(emitErrorNT, this, s)), + this) + : (this._readableState && (this._readableState.destroyed = !0), + this._writableState && (this._writableState.destroyed = !0), + this._destroy(s || null, function (s) { + !o && s + ? i._writableState + ? i._writableState.errorEmitted + ? u.nextTick(emitCloseNT, i) + : ((i._writableState.errorEmitted = !0), + u.nextTick(emitErrorAndCloseNT, i, s)) + : u.nextTick(emitErrorAndCloseNT, i, s) + : o + ? (u.nextTick(emitCloseNT, i), o(s)) + : u.nextTick(emitCloseNT, i); + }), + this); + }, + undestroy: function undestroy() { + (this._readableState && + ((this._readableState.destroyed = !1), + (this._readableState.reading = !1), + (this._readableState.ended = !1), + (this._readableState.endEmitted = !1)), + this._writableState && + ((this._writableState.destroyed = !1), + (this._writableState.ended = !1), + (this._writableState.ending = !1), + (this._writableState.finalCalled = !1), + (this._writableState.prefinished = !1), + (this._writableState.finished = !1), + (this._writableState.errorEmitted = !1))); + }, + errorOrDestroy: function errorOrDestroy(s, o) { + var i = s._readableState, + u = s._writableState; + (i && i.autoDestroy) || (u && u.autoDestroy) ? s.destroy(o) : s.emit('error', o); + } + }; + }, + 86238: (s, o, i) => { + 'use strict'; + var u = i(86048).F.ERR_STREAM_PREMATURE_CLOSE; + function noop() {} + s.exports = function eos(s, o, i) { + if ('function' == typeof o) return eos(s, null, o); + (o || (o = {}), + (i = (function once(s) { + var o = !1; + return function () { + if (!o) { + o = !0; + for (var i = arguments.length, u = new Array(i), _ = 0; _ < i; _++) + u[_] = arguments[_]; + s.apply(this, u); + } + }; + })(i || noop))); + var _ = o.readable || (!1 !== o.readable && s.readable), + w = o.writable || (!1 !== o.writable && s.writable), + x = function onlegacyfinish() { + s.writable || j(); + }, + C = s._writableState && s._writableState.finished, + j = function onfinish() { + ((w = !1), (C = !0), _ || i.call(s)); + }, + L = s._readableState && s._readableState.endEmitted, + B = function onend() { + ((_ = !1), (L = !0), w || i.call(s)); + }, + $ = function onerror(o) { + i.call(s, o); + }, + V = function onclose() { + var o; + return _ && !L + ? ((s._readableState && s._readableState.ended) || (o = new u()), i.call(s, o)) + : w && !C + ? ((s._writableState && s._writableState.ended) || (o = new u()), i.call(s, o)) + : void 0; + }, + U = function onrequest() { + s.req.on('finish', j); + }; + return ( + !(function isRequest(s) { + return s.setHeader && 'function' == typeof s.abort; + })(s) + ? w && !s._writableState && (s.on('end', x), s.on('close', x)) + : (s.on('complete', j), s.on('abort', V), s.req ? U() : s.on('request', U)), + s.on('end', B), + s.on('finish', j), + !1 !== o.error && s.on('error', $), + s.on('close', V), + function () { + (s.removeListener('complete', j), + s.removeListener('abort', V), + s.removeListener('request', U), + s.req && s.req.removeListener('finish', j), + s.removeListener('end', x), + s.removeListener('close', x), + s.removeListener('finish', j), + s.removeListener('end', B), + s.removeListener('error', $), + s.removeListener('close', V)); + } + ); + }; + }, + 55157: (s) => { + s.exports = function () { + throw new Error('Readable.from is not available in the browser'); + }; + }, + 57758: (s, o, i) => { + 'use strict'; + var u; + var _ = i(86048).F, + w = _.ERR_MISSING_ARGS, + x = _.ERR_STREAM_DESTROYED; + function noop(s) { + if (s) throw s; + } + function call(s) { + s(); + } + function pipe(s, o) { + return s.pipe(o); + } + s.exports = function pipeline() { + for (var s = arguments.length, o = new Array(s), _ = 0; _ < s; _++) o[_] = arguments[_]; + var C, + j = (function popCallback(s) { + return s.length ? ('function' != typeof s[s.length - 1] ? noop : s.pop()) : noop; + })(o); + if ((Array.isArray(o[0]) && (o = o[0]), o.length < 2)) throw new w('streams'); + var L = o.map(function (s, _) { + var w = _ < o.length - 1; + return (function destroyer(s, o, _, w) { + w = (function once(s) { + var o = !1; + return function () { + o || ((o = !0), s.apply(void 0, arguments)); + }; + })(w); + var C = !1; + (s.on('close', function () { + C = !0; + }), + void 0 === u && (u = i(86238)), + u(s, { readable: o, writable: _ }, function (s) { + if (s) return w(s); + ((C = !0), w()); + })); + var j = !1; + return function (o) { + if (!C && !j) + return ( + (j = !0), + (function isRequest(s) { + return s.setHeader && 'function' == typeof s.abort; + })(s) + ? s.abort() + : 'function' == typeof s.destroy + ? s.destroy() + : void w(o || new x('pipe')) + ); + }; + })(s, w, _ > 0, function (s) { + (C || (C = s), s && L.forEach(call), w || (L.forEach(call), j(C))); + }); + }); + return o.reduce(pipe); + }; + }, + 65291: (s, o, i) => { + 'use strict'; + var u = i(86048).F.ERR_INVALID_OPT_VALUE; + s.exports = { + getHighWaterMark: function getHighWaterMark(s, o, i, _) { + var w = (function highWaterMarkFrom(s, o, i) { + return null != s.highWaterMark ? s.highWaterMark : o ? s[i] : null; + })(o, _, i); + if (null != w) { + if (!isFinite(w) || Math.floor(w) !== w || w < 0) + throw new u(_ ? i : 'highWaterMark', w); + return Math.floor(w); + } + return s.objectMode ? 16 : 16384; + } + }; + }, + 40345: (s, o, i) => { + s.exports = i(37007).EventEmitter; + }, + 84977: (s, o, i) => { + 'use strict'; + Object.defineProperty(o, '__esModule', { value: !0 }); + var u = (function _interopRequireDefault(s) { + return s && s.__esModule ? s : { default: s }; + })(i(9404)), + _ = i(55674); + ((o.default = function (s) { + var o = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : u.default.Map, + i = Object.keys(s); + return function () { + var u = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : o(), + w = arguments[1]; + return u.withMutations(function (o) { + i.forEach(function (i) { + var u = (0, s[i])(o.get(i), w); + ((0, _.validateNextState)(u, i, w), o.set(i, u)); + }); + }); + }; + }), + (s.exports = o.default)); + }, + 89593: (s, o, i) => { + 'use strict'; + o.H = void 0; + var u = (function _interopRequireDefault(s) { + return s && s.__esModule ? s : { default: s }; + })(i(84977)); + o.H = u.default; + }, + 48590: (s, o) => { + 'use strict'; + (Object.defineProperty(o, '__esModule', { value: !0 }), + (o.default = function (s) { + return s && '@@redux/INIT' === s.type + ? 'initialState argument passed to createStore' + : 'previous state received by the reducer'; + }), + (s.exports = o.default)); + }, + 82261: (s, o, i) => { + 'use strict'; + Object.defineProperty(o, '__esModule', { value: !0 }); + var u = _interopRequireDefault(i(9404)), + _ = _interopRequireDefault(i(48590)); + function _interopRequireDefault(s) { + return s && s.__esModule ? s : { default: s }; + } + ((o.default = function (s, o, i) { + var w = Object.keys(o); + if (!w.length) + return 'Store does not have a valid reducer. Make sure the argument passed to combineReducers is an object whose values are reducers.'; + var x = (0, _.default)(i); + if ( + u.default.isImmutable ? !u.default.isImmutable(s) : !u.default.Iterable.isIterable(s) + ) + return ( + 'The ' + + x + + ' is of unexpected type. Expected argument to be an instance of Immutable.Collection or Immutable.Record with the following properties: "' + + w.join('", "') + + '".' + ); + var C = s + .toSeq() + .keySeq() + .toArray() + .filter(function (s) { + return !o.hasOwnProperty(s); + }); + return C.length > 0 + ? 'Unexpected ' + + (1 === C.length ? 'property' : 'properties') + + ' "' + + C.join('", "') + + '" found in ' + + x + + '. Expected to find one of the known reducer property names instead: "' + + w.join('", "') + + '". Unexpected properties will be ignored.' + : null; + }), + (s.exports = o.default)); + }, + 55674: (s, o, i) => { + 'use strict'; + (Object.defineProperty(o, '__esModule', { value: !0 }), + (o.validateNextState = + o.getUnexpectedInvocationParameterMessage = + o.getStateName = + void 0)); + var u = _interopRequireDefault(i(48590)), + _ = _interopRequireDefault(i(82261)), + w = _interopRequireDefault(i(27374)); + function _interopRequireDefault(s) { + return s && s.__esModule ? s : { default: s }; + } + ((o.getStateName = u.default), + (o.getUnexpectedInvocationParameterMessage = _.default), + (o.validateNextState = w.default)); + }, + 27374: (s, o) => { + 'use strict'; + (Object.defineProperty(o, '__esModule', { value: !0 }), + (o.default = function (s, o, i) { + if (void 0 === s) + throw new Error( + 'Reducer "' + + o + + '" returned undefined when handling "' + + i.type + + '" action. To ignore an action, you must explicitly return the previous state.' + ); + }), + (s.exports = o.default)); + }, + 75208: (s) => { + 'use strict'; + var o, + i = ''; + s.exports = function repeat(s, u) { + if ('string' != typeof s) throw new TypeError('expected a string'); + if (1 === u) return s; + if (2 === u) return s + s; + var _ = s.length * u; + if (o !== s || void 0 === o) ((o = s), (i = '')); + else if (i.length >= _) return i.substr(0, _); + for (; _ > i.length && u > 1; ) (1 & u && (i += s), (u >>= 1), (s += s)); + return (i = (i += s).substr(0, _)); + }; + }, + 92063: (s) => { + 'use strict'; + s.exports = function required(s, o) { + if (((o = o.split(':')[0]), !(s = +s))) return !1; + switch (o) { + case 'http': + case 'ws': + return 80 !== s; + case 'https': + case 'wss': + return 443 !== s; + case 'ftp': + return 21 !== s; + case 'gopher': + return 70 !== s; + case 'file': + return !1; + } + return 0 !== s; + }; + }, + 27096: (s, o, i) => { + const u = i(87586), + _ = i(6205), + w = i(10023), + x = i(8048); + ((s.exports = (s) => { + var o, + i, + C = 0, + j = { type: _.ROOT, stack: [] }, + L = j, + B = j.stack, + $ = [], + repeatErr = (o) => { + u.error(s, 'Nothing to repeat at column ' + (o - 1)); + }, + V = u.strToChars(s); + for (o = V.length; C < o; ) + switch ((i = V[C++])) { + case '\\': + switch ((i = V[C++])) { + case 'b': + B.push(x.wordBoundary()); + break; + case 'B': + B.push(x.nonWordBoundary()); + break; + case 'w': + B.push(w.words()); + break; + case 'W': + B.push(w.notWords()); + break; + case 'd': + B.push(w.ints()); + break; + case 'D': + B.push(w.notInts()); + break; + case 's': + B.push(w.whitespace()); + break; + case 'S': + B.push(w.notWhitespace()); + break; + default: + /\d/.test(i) + ? B.push({ type: _.REFERENCE, value: parseInt(i, 10) }) + : B.push({ type: _.CHAR, value: i.charCodeAt(0) }); + } + break; + case '^': + B.push(x.begin()); + break; + case '$': + B.push(x.end()); + break; + case '[': + var U; + '^' === V[C] ? ((U = !0), C++) : (U = !1); + var z = u.tokenizeClass(V.slice(C), s); + ((C += z[1]), B.push({ type: _.SET, set: z[0], not: U })); + break; + case '.': + B.push(w.anyChar()); + break; + case '(': + var Y = { type: _.GROUP, stack: [], remember: !0 }; + ('?' === (i = V[C]) && + ((i = V[C + 1]), + (C += 2), + '=' === i + ? (Y.followedBy = !0) + : '!' === i + ? (Y.notFollowedBy = !0) + : ':' !== i && + u.error( + s, + `Invalid group, character '${i}' after '?' at column ` + (C - 1) + ), + (Y.remember = !1)), + B.push(Y), + $.push(L), + (L = Y), + (B = Y.stack)); + break; + case ')': + (0 === $.length && u.error(s, 'Unmatched ) at column ' + (C - 1)), + (B = (L = $.pop()).options ? L.options[L.options.length - 1] : L.stack)); + break; + case '|': + L.options || ((L.options = [L.stack]), delete L.stack); + var Z = []; + (L.options.push(Z), (B = Z)); + break; + case '{': + var ee, + ie, + ae = /^(\d+)(,(\d+)?)?\}/.exec(V.slice(C)); + null !== ae + ? (0 === B.length && repeatErr(C), + (ee = parseInt(ae[1], 10)), + (ie = ae[2] ? (ae[3] ? parseInt(ae[3], 10) : 1 / 0) : ee), + (C += ae[0].length), + B.push({ type: _.REPETITION, min: ee, max: ie, value: B.pop() })) + : B.push({ type: _.CHAR, value: 123 }); + break; + case '?': + (0 === B.length && repeatErr(C), + B.push({ type: _.REPETITION, min: 0, max: 1, value: B.pop() })); + break; + case '+': + (0 === B.length && repeatErr(C), + B.push({ type: _.REPETITION, min: 1, max: 1 / 0, value: B.pop() })); + break; + case '*': + (0 === B.length && repeatErr(C), + B.push({ type: _.REPETITION, min: 0, max: 1 / 0, value: B.pop() })); + break; + default: + B.push({ type: _.CHAR, value: i.charCodeAt(0) }); + } + return (0 !== $.length && u.error(s, 'Unterminated group'), j); + }), + (s.exports.types = _)); + }, + 8048: (s, o, i) => { + const u = i(6205); + ((o.wordBoundary = () => ({ type: u.POSITION, value: 'b' })), + (o.nonWordBoundary = () => ({ type: u.POSITION, value: 'B' })), + (o.begin = () => ({ type: u.POSITION, value: '^' })), + (o.end = () => ({ type: u.POSITION, value: '$' }))); + }, + 10023: (s, o, i) => { + const u = i(6205), + INTS = () => [{ type: u.RANGE, from: 48, to: 57 }], + WORDS = () => + [ + { type: u.CHAR, value: 95 }, + { type: u.RANGE, from: 97, to: 122 }, + { type: u.RANGE, from: 65, to: 90 } + ].concat(INTS()), + WHITESPACE = () => [ + { type: u.CHAR, value: 9 }, + { type: u.CHAR, value: 10 }, + { type: u.CHAR, value: 11 }, + { type: u.CHAR, value: 12 }, + { type: u.CHAR, value: 13 }, + { type: u.CHAR, value: 32 }, + { type: u.CHAR, value: 160 }, + { type: u.CHAR, value: 5760 }, + { type: u.RANGE, from: 8192, to: 8202 }, + { type: u.CHAR, value: 8232 }, + { type: u.CHAR, value: 8233 }, + { type: u.CHAR, value: 8239 }, + { type: u.CHAR, value: 8287 }, + { type: u.CHAR, value: 12288 }, + { type: u.CHAR, value: 65279 } + ]; + ((o.words = () => ({ type: u.SET, set: WORDS(), not: !1 })), + (o.notWords = () => ({ type: u.SET, set: WORDS(), not: !0 })), + (o.ints = () => ({ type: u.SET, set: INTS(), not: !1 })), + (o.notInts = () => ({ type: u.SET, set: INTS(), not: !0 })), + (o.whitespace = () => ({ type: u.SET, set: WHITESPACE(), not: !1 })), + (o.notWhitespace = () => ({ type: u.SET, set: WHITESPACE(), not: !0 })), + (o.anyChar = () => ({ + type: u.SET, + set: [ + { type: u.CHAR, value: 10 }, + { type: u.CHAR, value: 13 }, + { type: u.CHAR, value: 8232 }, + { type: u.CHAR, value: 8233 } + ], + not: !0 + }))); + }, + 6205: (s) => { + s.exports = { + ROOT: 0, + GROUP: 1, + POSITION: 2, + SET: 3, + RANGE: 4, + REPETITION: 5, + REFERENCE: 6, + CHAR: 7 + }; + }, + 87586: (s, o, i) => { + const u = i(6205), + _ = i(10023), + w = { 0: 0, t: 9, n: 10, v: 11, f: 12, r: 13 }; + ((o.strToChars = function (s) { + return (s = s.replace( + /(\[\\b\])|(\\)?\\(?:u([A-F0-9]{4})|x([A-F0-9]{2})|(0?[0-7]{2})|c([@A-Z[\\\]^?])|([0tnvfr]))/g, + function (s, o, i, u, _, x, C, j) { + if (i) return s; + var L = o + ? 8 + : u + ? parseInt(u, 16) + : _ + ? parseInt(_, 16) + : x + ? parseInt(x, 8) + : C + ? '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^ ?'.indexOf(C) + : w[j], + B = String.fromCharCode(L); + return (/[[\]{}^$.|?*+()]/.test(B) && (B = '\\' + B), B); + } + )); + }), + (o.tokenizeClass = (s, i) => { + for ( + var w, + x, + C = [], + j = + /\\(?:(w)|(d)|(s)|(W)|(D)|(S))|((?:(?:\\)(.)|([^\]\\]))-(?:\\)?([^\]]))|(\])|(?:\\)?([^])/g; + null != (w = j.exec(s)); + ) + if (w[1]) C.push(_.words()); + else if (w[2]) C.push(_.ints()); + else if (w[3]) C.push(_.whitespace()); + else if (w[4]) C.push(_.notWords()); + else if (w[5]) C.push(_.notInts()); + else if (w[6]) C.push(_.notWhitespace()); + else if (w[7]) + C.push({ + type: u.RANGE, + from: (w[8] || w[9]).charCodeAt(0), + to: w[10].charCodeAt(0) + }); + else { + if (!(x = w[12])) return [C, j.lastIndex]; + C.push({ type: u.CHAR, value: x.charCodeAt(0) }); + } + o.error(i, 'Unterminated character class'); + }), + (o.error = (s, o) => { + throw new SyntaxError('Invalid regular expression: /' + s + '/: ' + o); + })); + }, + 92861: (s, o, i) => { + var u = i(48287), + _ = u.Buffer; + function copyProps(s, o) { + for (var i in s) o[i] = s[i]; + } + function SafeBuffer(s, o, i) { + return _(s, o, i); + } + (_.from && _.alloc && _.allocUnsafe && _.allocUnsafeSlow + ? (s.exports = u) + : (copyProps(u, o), (o.Buffer = SafeBuffer)), + (SafeBuffer.prototype = Object.create(_.prototype)), + copyProps(_, SafeBuffer), + (SafeBuffer.from = function (s, o, i) { + if ('number' == typeof s) throw new TypeError('Argument must not be a number'); + return _(s, o, i); + }), + (SafeBuffer.alloc = function (s, o, i) { + if ('number' != typeof s) throw new TypeError('Argument must be a number'); + var u = _(s); + return ( + void 0 !== o ? ('string' == typeof i ? u.fill(o, i) : u.fill(o)) : u.fill(0), + u + ); + }), + (SafeBuffer.allocUnsafe = function (s) { + if ('number' != typeof s) throw new TypeError('Argument must be a number'); + return _(s); + }), + (SafeBuffer.allocUnsafeSlow = function (s) { + if ('number' != typeof s) throw new TypeError('Argument must be a number'); + return u.SlowBuffer(s); + })); + }, + 29844: (s, o) => { + 'use strict'; + function f(s, o) { + var i = s.length; + s.push(o); + e: for (; 0 < i; ) { + var u = (i - 1) >>> 1, + _ = s[u]; + if (!(0 < g(_, o))) break e; + ((s[u] = o), (s[i] = _), (i = u)); + } + } + function h(s) { + return 0 === s.length ? null : s[0]; + } + function k(s) { + if (0 === s.length) return null; + var o = s[0], + i = s.pop(); + if (i !== o) { + s[0] = i; + e: for (var u = 0, _ = s.length, w = _ >>> 1; u < w; ) { + var x = 2 * (u + 1) - 1, + C = s[x], + j = x + 1, + L = s[j]; + if (0 > g(C, i)) + j < _ && 0 > g(L, C) + ? ((s[u] = L), (s[j] = i), (u = j)) + : ((s[u] = C), (s[x] = i), (u = x)); + else { + if (!(j < _ && 0 > g(L, i))) break e; + ((s[u] = L), (s[j] = i), (u = j)); + } + } + } + return o; + } + function g(s, o) { + var i = s.sortIndex - o.sortIndex; + return 0 !== i ? i : s.id - o.id; + } + if ('object' == typeof performance && 'function' == typeof performance.now) { + var i = performance; + o.unstable_now = function () { + return i.now(); + }; + } else { + var u = Date, + _ = u.now(); + o.unstable_now = function () { + return u.now() - _; + }; + } + var w = [], + x = [], + C = 1, + j = null, + L = 3, + B = !1, + $ = !1, + V = !1, + U = 'function' == typeof setTimeout ? setTimeout : null, + z = 'function' == typeof clearTimeout ? clearTimeout : null, + Y = 'undefined' != typeof setImmediate ? setImmediate : null; + function G(s) { + for (var o = h(x); null !== o; ) { + if (null === o.callback) k(x); + else { + if (!(o.startTime <= s)) break; + (k(x), (o.sortIndex = o.expirationTime), f(w, o)); + } + o = h(x); + } + } + function H(s) { + if (((V = !1), G(s), !$)) + if (null !== h(w)) (($ = !0), I(J)); + else { + var o = h(x); + null !== o && K(H, o.startTime - s); + } + } + function J(s, i) { + (($ = !1), V && ((V = !1), z(ae), (ae = -1)), (B = !0)); + var u = L; + try { + for (G(i), j = h(w); null !== j && (!(j.expirationTime > i) || (s && !M())); ) { + var _ = j.callback; + if ('function' == typeof _) { + ((j.callback = null), (L = j.priorityLevel)); + var C = _(j.expirationTime <= i); + ((i = o.unstable_now()), + 'function' == typeof C ? (j.callback = C) : j === h(w) && k(w), + G(i)); + } else k(w); + j = h(w); + } + if (null !== j) var U = !0; + else { + var Y = h(x); + (null !== Y && K(H, Y.startTime - i), (U = !1)); + } + return U; + } finally { + ((j = null), (L = u), (B = !1)); + } + } + 'undefined' != typeof navigator && + void 0 !== navigator.scheduling && + void 0 !== navigator.scheduling.isInputPending && + navigator.scheduling.isInputPending.bind(navigator.scheduling); + var Z, + ee = !1, + ie = null, + ae = -1, + le = 5, + ce = -1; + function M() { + return !(o.unstable_now() - ce < le); + } + function R() { + if (null !== ie) { + var s = o.unstable_now(); + ce = s; + var i = !0; + try { + i = ie(!0, s); + } finally { + i ? Z() : ((ee = !1), (ie = null)); + } + } else ee = !1; + } + if ('function' == typeof Y) + Z = function () { + Y(R); + }; + else if ('undefined' != typeof MessageChannel) { + var pe = new MessageChannel(), + de = pe.port2; + ((pe.port1.onmessage = R), + (Z = function () { + de.postMessage(null); + })); + } else + Z = function () { + U(R, 0); + }; + function I(s) { + ((ie = s), ee || ((ee = !0), Z())); + } + function K(s, i) { + ae = U(function () { + s(o.unstable_now()); + }, i); + } + ((o.unstable_IdlePriority = 5), + (o.unstable_ImmediatePriority = 1), + (o.unstable_LowPriority = 4), + (o.unstable_NormalPriority = 3), + (o.unstable_Profiling = null), + (o.unstable_UserBlockingPriority = 2), + (o.unstable_cancelCallback = function (s) { + s.callback = null; + }), + (o.unstable_continueExecution = function () { + $ || B || (($ = !0), I(J)); + }), + (o.unstable_forceFrameRate = function (s) { + 0 > s || 125 < s + ? console.error( + 'forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported' + ) + : (le = 0 < s ? Math.floor(1e3 / s) : 5); + }), + (o.unstable_getCurrentPriorityLevel = function () { + return L; + }), + (o.unstable_getFirstCallbackNode = function () { + return h(w); + }), + (o.unstable_next = function (s) { + switch (L) { + case 1: + case 2: + case 3: + var o = 3; + break; + default: + o = L; + } + var i = L; + L = o; + try { + return s(); + } finally { + L = i; + } + }), + (o.unstable_pauseExecution = function () {}), + (o.unstable_requestPaint = function () {}), + (o.unstable_runWithPriority = function (s, o) { + switch (s) { + case 1: + case 2: + case 3: + case 4: + case 5: + break; + default: + s = 3; + } + var i = L; + L = s; + try { + return o(); + } finally { + L = i; + } + }), + (o.unstable_scheduleCallback = function (s, i, u) { + var _ = o.unstable_now(); + switch ( + ('object' == typeof u && null !== u + ? (u = 'number' == typeof (u = u.delay) && 0 < u ? _ + u : _) + : (u = _), + s) + ) { + case 1: + var j = -1; + break; + case 2: + j = 250; + break; + case 5: + j = 1073741823; + break; + case 4: + j = 1e4; + break; + default: + j = 5e3; + } + return ( + (s = { + id: C++, + callback: i, + priorityLevel: s, + startTime: u, + expirationTime: (j = u + j), + sortIndex: -1 + }), + u > _ + ? ((s.sortIndex = u), + f(x, s), + null === h(w) && s === h(x) && (V ? (z(ae), (ae = -1)) : (V = !0), K(H, u - _))) + : ((s.sortIndex = j), f(w, s), $ || B || (($ = !0), I(J))), + s + ); + }), + (o.unstable_shouldYield = M), + (o.unstable_wrapCallback = function (s) { + var o = L; + return function () { + var i = L; + L = o; + try { + return s.apply(this, arguments); + } finally { + L = i; + } + }; + })); + }, + 69982: (s, o, i) => { + 'use strict'; + s.exports = i(29844); + }, + 20334: (s, o, i) => { + 'use strict'; + var u = i(48287).Buffer; + class NonError extends Error { + constructor(s) { + (super(NonError._prepareSuperMessage(s)), + Object.defineProperty(this, 'name', { + value: 'NonError', + configurable: !0, + writable: !0 + }), + Error.captureStackTrace && Error.captureStackTrace(this, NonError)); + } + static _prepareSuperMessage(s) { + try { + return JSON.stringify(s); + } catch { + return String(s); + } + } + } + const _ = [ + { property: 'name', enumerable: !1 }, + { property: 'message', enumerable: !1 }, + { property: 'stack', enumerable: !1 }, + { property: 'code', enumerable: !0 } + ], + w = Symbol('.toJSON called'), + destroyCircular = ({ + from: s, + seen: o, + to_: i, + forceEnumerable: x, + maxDepth: C, + depth: j + }) => { + const L = i || (Array.isArray(s) ? [] : {}); + if ((o.push(s), j >= C)) return L; + if ('function' == typeof s.toJSON && !0 !== s[w]) + return ((s) => { + s[w] = !0; + const o = s.toJSON(); + return (delete s[w], o); + })(s); + for (const [i, _] of Object.entries(s)) + 'function' == typeof u && u.isBuffer(_) + ? (L[i] = '[object Buffer]') + : 'function' != typeof _ && + (_ && 'object' == typeof _ + ? o.includes(s[i]) + ? (L[i] = '[Circular]') + : (j++, + (L[i] = destroyCircular({ + from: s[i], + seen: o.slice(), + forceEnumerable: x, + maxDepth: C, + depth: j + }))) + : (L[i] = _)); + for (const { property: o, enumerable: i } of _) + 'string' == typeof s[o] && + Object.defineProperty(L, o, { + value: s[o], + enumerable: !!x || i, + configurable: !0, + writable: !0 + }); + return L; + }; + s.exports = { + serializeError: (s, o = {}) => { + const { maxDepth: i = Number.POSITIVE_INFINITY } = o; + return 'object' == typeof s && null !== s + ? destroyCircular({ from: s, seen: [], forceEnumerable: !0, maxDepth: i, depth: 0 }) + : 'function' == typeof s + ? `[Function: ${s.name || 'anonymous'}]` + : s; + }, + deserializeError: (s, o = {}) => { + const { maxDepth: i = Number.POSITIVE_INFINITY } = o; + if (s instanceof Error) return s; + if ('object' == typeof s && null !== s && !Array.isArray(s)) { + const o = new Error(); + return (destroyCircular({ from: s, seen: [], to_: o, maxDepth: i, depth: 0 }), o); + } + return new NonError(s); + } + }; + }, + 90392: (s, o, i) => { + var u = i(92861).Buffer; + function Hash(s, o) { + ((this._block = u.alloc(s)), + (this._finalSize = o), + (this._blockSize = s), + (this._len = 0)); + } + ((Hash.prototype.update = function (s, o) { + 'string' == typeof s && ((o = o || 'utf8'), (s = u.from(s, o))); + for ( + var i = this._block, _ = this._blockSize, w = s.length, x = this._len, C = 0; + C < w; + ) { + for (var j = x % _, L = Math.min(w - C, _ - j), B = 0; B < L; B++) + i[j + B] = s[C + B]; + ((C += L), (x += L) % _ == 0 && this._update(i)); + } + return ((this._len += w), this); + }), + (Hash.prototype.digest = function (s) { + var o = this._len % this._blockSize; + ((this._block[o] = 128), + this._block.fill(0, o + 1), + o >= this._finalSize && (this._update(this._block), this._block.fill(0))); + var i = 8 * this._len; + if (i <= 4294967295) this._block.writeUInt32BE(i, this._blockSize - 4); + else { + var u = (4294967295 & i) >>> 0, + _ = (i - u) / 4294967296; + (this._block.writeUInt32BE(_, this._blockSize - 8), + this._block.writeUInt32BE(u, this._blockSize - 4)); + } + this._update(this._block); + var w = this._hash(); + return s ? w.toString(s) : w; + }), + (Hash.prototype._update = function () { + throw new Error('_update must be implemented by subclass'); + }), + (s.exports = Hash)); + }, + 62802: (s, o, i) => { + var u = (s.exports = function SHA(s) { + s = s.toLowerCase(); + var o = u[s]; + if (!o) throw new Error(s + ' is not supported (we accept pull requests)'); + return new o(); + }); + ((u.sha = i(27816)), + (u.sha1 = i(63737)), + (u.sha224 = i(26710)), + (u.sha256 = i(24107)), + (u.sha384 = i(32827)), + (u.sha512 = i(82890))); + }, + 27816: (s, o, i) => { + var u = i(56698), + _ = i(90392), + w = i(92861).Buffer, + x = [1518500249, 1859775393, -1894007588, -899497514], + C = new Array(80); + function Sha() { + (this.init(), (this._w = C), _.call(this, 64, 56)); + } + function rotl30(s) { + return (s << 30) | (s >>> 2); + } + function ft(s, o, i, u) { + return 0 === s ? (o & i) | (~o & u) : 2 === s ? (o & i) | (o & u) | (i & u) : o ^ i ^ u; + } + (u(Sha, _), + (Sha.prototype.init = function () { + return ( + (this._a = 1732584193), + (this._b = 4023233417), + (this._c = 2562383102), + (this._d = 271733878), + (this._e = 3285377520), + this + ); + }), + (Sha.prototype._update = function (s) { + for ( + var o, + i = this._w, + u = 0 | this._a, + _ = 0 | this._b, + w = 0 | this._c, + C = 0 | this._d, + j = 0 | this._e, + L = 0; + L < 16; + ++L + ) + i[L] = s.readInt32BE(4 * L); + for (; L < 80; ++L) i[L] = i[L - 3] ^ i[L - 8] ^ i[L - 14] ^ i[L - 16]; + for (var B = 0; B < 80; ++B) { + var $ = ~~(B / 20), + V = 0 | ((((o = u) << 5) | (o >>> 27)) + ft($, _, w, C) + j + i[B] + x[$]); + ((j = C), (C = w), (w = rotl30(_)), (_ = u), (u = V)); + } + ((this._a = (u + this._a) | 0), + (this._b = (_ + this._b) | 0), + (this._c = (w + this._c) | 0), + (this._d = (C + this._d) | 0), + (this._e = (j + this._e) | 0)); + }), + (Sha.prototype._hash = function () { + var s = w.allocUnsafe(20); + return ( + s.writeInt32BE(0 | this._a, 0), + s.writeInt32BE(0 | this._b, 4), + s.writeInt32BE(0 | this._c, 8), + s.writeInt32BE(0 | this._d, 12), + s.writeInt32BE(0 | this._e, 16), + s + ); + }), + (s.exports = Sha)); + }, + 63737: (s, o, i) => { + var u = i(56698), + _ = i(90392), + w = i(92861).Buffer, + x = [1518500249, 1859775393, -1894007588, -899497514], + C = new Array(80); + function Sha1() { + (this.init(), (this._w = C), _.call(this, 64, 56)); + } + function rotl5(s) { + return (s << 5) | (s >>> 27); + } + function rotl30(s) { + return (s << 30) | (s >>> 2); + } + function ft(s, o, i, u) { + return 0 === s ? (o & i) | (~o & u) : 2 === s ? (o & i) | (o & u) | (i & u) : o ^ i ^ u; + } + (u(Sha1, _), + (Sha1.prototype.init = function () { + return ( + (this._a = 1732584193), + (this._b = 4023233417), + (this._c = 2562383102), + (this._d = 271733878), + (this._e = 3285377520), + this + ); + }), + (Sha1.prototype._update = function (s) { + for ( + var o, + i = this._w, + u = 0 | this._a, + _ = 0 | this._b, + w = 0 | this._c, + C = 0 | this._d, + j = 0 | this._e, + L = 0; + L < 16; + ++L + ) + i[L] = s.readInt32BE(4 * L); + for (; L < 80; ++L) + i[L] = ((o = i[L - 3] ^ i[L - 8] ^ i[L - 14] ^ i[L - 16]) << 1) | (o >>> 31); + for (var B = 0; B < 80; ++B) { + var $ = ~~(B / 20), + V = (rotl5(u) + ft($, _, w, C) + j + i[B] + x[$]) | 0; + ((j = C), (C = w), (w = rotl30(_)), (_ = u), (u = V)); + } + ((this._a = (u + this._a) | 0), + (this._b = (_ + this._b) | 0), + (this._c = (w + this._c) | 0), + (this._d = (C + this._d) | 0), + (this._e = (j + this._e) | 0)); + }), + (Sha1.prototype._hash = function () { + var s = w.allocUnsafe(20); + return ( + s.writeInt32BE(0 | this._a, 0), + s.writeInt32BE(0 | this._b, 4), + s.writeInt32BE(0 | this._c, 8), + s.writeInt32BE(0 | this._d, 12), + s.writeInt32BE(0 | this._e, 16), + s + ); + }), + (s.exports = Sha1)); + }, + 26710: (s, o, i) => { + var u = i(56698), + _ = i(24107), + w = i(90392), + x = i(92861).Buffer, + C = new Array(64); + function Sha224() { + (this.init(), (this._w = C), w.call(this, 64, 56)); + } + (u(Sha224, _), + (Sha224.prototype.init = function () { + return ( + (this._a = 3238371032), + (this._b = 914150663), + (this._c = 812702999), + (this._d = 4144912697), + (this._e = 4290775857), + (this._f = 1750603025), + (this._g = 1694076839), + (this._h = 3204075428), + this + ); + }), + (Sha224.prototype._hash = function () { + var s = x.allocUnsafe(28); + return ( + s.writeInt32BE(this._a, 0), + s.writeInt32BE(this._b, 4), + s.writeInt32BE(this._c, 8), + s.writeInt32BE(this._d, 12), + s.writeInt32BE(this._e, 16), + s.writeInt32BE(this._f, 20), + s.writeInt32BE(this._g, 24), + s + ); + }), + (s.exports = Sha224)); + }, + 24107: (s, o, i) => { + var u = i(56698), + _ = i(90392), + w = i(92861).Buffer, + x = [ + 1116352408, 1899447441, 3049323471, 3921009573, 961987163, 1508970993, 2453635748, + 2870763221, 3624381080, 310598401, 607225278, 1426881987, 1925078388, 2162078206, + 2614888103, 3248222580, 3835390401, 4022224774, 264347078, 604807628, 770255983, + 1249150122, 1555081692, 1996064986, 2554220882, 2821834349, 2952996808, 3210313671, + 3336571891, 3584528711, 113926993, 338241895, 666307205, 773529912, 1294757372, + 1396182291, 1695183700, 1986661051, 2177026350, 2456956037, 2730485921, 2820302411, + 3259730800, 3345764771, 3516065817, 3600352804, 4094571909, 275423344, 430227734, + 506948616, 659060556, 883997877, 958139571, 1322822218, 1537002063, 1747873779, + 1955562222, 2024104815, 2227730452, 2361852424, 2428436474, 2756734187, 3204031479, + 3329325298 + ], + C = new Array(64); + function Sha256() { + (this.init(), (this._w = C), _.call(this, 64, 56)); + } + function ch(s, o, i) { + return i ^ (s & (o ^ i)); + } + function maj(s, o, i) { + return (s & o) | (i & (s | o)); + } + function sigma0(s) { + return ((s >>> 2) | (s << 30)) ^ ((s >>> 13) | (s << 19)) ^ ((s >>> 22) | (s << 10)); + } + function sigma1(s) { + return ((s >>> 6) | (s << 26)) ^ ((s >>> 11) | (s << 21)) ^ ((s >>> 25) | (s << 7)); + } + function gamma0(s) { + return ((s >>> 7) | (s << 25)) ^ ((s >>> 18) | (s << 14)) ^ (s >>> 3); + } + (u(Sha256, _), + (Sha256.prototype.init = function () { + return ( + (this._a = 1779033703), + (this._b = 3144134277), + (this._c = 1013904242), + (this._d = 2773480762), + (this._e = 1359893119), + (this._f = 2600822924), + (this._g = 528734635), + (this._h = 1541459225), + this + ); + }), + (Sha256.prototype._update = function (s) { + for ( + var o, + i = this._w, + u = 0 | this._a, + _ = 0 | this._b, + w = 0 | this._c, + C = 0 | this._d, + j = 0 | this._e, + L = 0 | this._f, + B = 0 | this._g, + $ = 0 | this._h, + V = 0; + V < 16; + ++V + ) + i[V] = s.readInt32BE(4 * V); + for (; V < 64; ++V) + i[V] = + 0 | + (((((o = i[V - 2]) >>> 17) | (o << 15)) ^ ((o >>> 19) | (o << 13)) ^ (o >>> 10)) + + i[V - 7] + + gamma0(i[V - 15]) + + i[V - 16]); + for (var U = 0; U < 64; ++U) { + var z = ($ + sigma1(j) + ch(j, L, B) + x[U] + i[U]) | 0, + Y = (sigma0(u) + maj(u, _, w)) | 0; + (($ = B), + (B = L), + (L = j), + (j = (C + z) | 0), + (C = w), + (w = _), + (_ = u), + (u = (z + Y) | 0)); + } + ((this._a = (u + this._a) | 0), + (this._b = (_ + this._b) | 0), + (this._c = (w + this._c) | 0), + (this._d = (C + this._d) | 0), + (this._e = (j + this._e) | 0), + (this._f = (L + this._f) | 0), + (this._g = (B + this._g) | 0), + (this._h = ($ + this._h) | 0)); + }), + (Sha256.prototype._hash = function () { + var s = w.allocUnsafe(32); + return ( + s.writeInt32BE(this._a, 0), + s.writeInt32BE(this._b, 4), + s.writeInt32BE(this._c, 8), + s.writeInt32BE(this._d, 12), + s.writeInt32BE(this._e, 16), + s.writeInt32BE(this._f, 20), + s.writeInt32BE(this._g, 24), + s.writeInt32BE(this._h, 28), + s + ); + }), + (s.exports = Sha256)); + }, + 32827: (s, o, i) => { + var u = i(56698), + _ = i(82890), + w = i(90392), + x = i(92861).Buffer, + C = new Array(160); + function Sha384() { + (this.init(), (this._w = C), w.call(this, 128, 112)); + } + (u(Sha384, _), + (Sha384.prototype.init = function () { + return ( + (this._ah = 3418070365), + (this._bh = 1654270250), + (this._ch = 2438529370), + (this._dh = 355462360), + (this._eh = 1731405415), + (this._fh = 2394180231), + (this._gh = 3675008525), + (this._hh = 1203062813), + (this._al = 3238371032), + (this._bl = 914150663), + (this._cl = 812702999), + (this._dl = 4144912697), + (this._el = 4290775857), + (this._fl = 1750603025), + (this._gl = 1694076839), + (this._hl = 3204075428), + this + ); + }), + (Sha384.prototype._hash = function () { + var s = x.allocUnsafe(48); + function writeInt64BE(o, i, u) { + (s.writeInt32BE(o, u), s.writeInt32BE(i, u + 4)); + } + return ( + writeInt64BE(this._ah, this._al, 0), + writeInt64BE(this._bh, this._bl, 8), + writeInt64BE(this._ch, this._cl, 16), + writeInt64BE(this._dh, this._dl, 24), + writeInt64BE(this._eh, this._el, 32), + writeInt64BE(this._fh, this._fl, 40), + s + ); + }), + (s.exports = Sha384)); + }, + 82890: (s, o, i) => { + var u = i(56698), + _ = i(90392), + w = i(92861).Buffer, + x = [ + 1116352408, 3609767458, 1899447441, 602891725, 3049323471, 3964484399, 3921009573, + 2173295548, 961987163, 4081628472, 1508970993, 3053834265, 2453635748, 2937671579, + 2870763221, 3664609560, 3624381080, 2734883394, 310598401, 1164996542, 607225278, + 1323610764, 1426881987, 3590304994, 1925078388, 4068182383, 2162078206, 991336113, + 2614888103, 633803317, 3248222580, 3479774868, 3835390401, 2666613458, 4022224774, + 944711139, 264347078, 2341262773, 604807628, 2007800933, 770255983, 1495990901, + 1249150122, 1856431235, 1555081692, 3175218132, 1996064986, 2198950837, 2554220882, + 3999719339, 2821834349, 766784016, 2952996808, 2566594879, 3210313671, 3203337956, + 3336571891, 1034457026, 3584528711, 2466948901, 113926993, 3758326383, 338241895, + 168717936, 666307205, 1188179964, 773529912, 1546045734, 1294757372, 1522805485, + 1396182291, 2643833823, 1695183700, 2343527390, 1986661051, 1014477480, 2177026350, + 1206759142, 2456956037, 344077627, 2730485921, 1290863460, 2820302411, 3158454273, + 3259730800, 3505952657, 3345764771, 106217008, 3516065817, 3606008344, 3600352804, + 1432725776, 4094571909, 1467031594, 275423344, 851169720, 430227734, 3100823752, + 506948616, 1363258195, 659060556, 3750685593, 883997877, 3785050280, 958139571, + 3318307427, 1322822218, 3812723403, 1537002063, 2003034995, 1747873779, 3602036899, + 1955562222, 1575990012, 2024104815, 1125592928, 2227730452, 2716904306, 2361852424, + 442776044, 2428436474, 593698344, 2756734187, 3733110249, 3204031479, 2999351573, + 3329325298, 3815920427, 3391569614, 3928383900, 3515267271, 566280711, 3940187606, + 3454069534, 4118630271, 4000239992, 116418474, 1914138554, 174292421, 2731055270, + 289380356, 3203993006, 460393269, 320620315, 685471733, 587496836, 852142971, + 1086792851, 1017036298, 365543100, 1126000580, 2618297676, 1288033470, 3409855158, + 1501505948, 4234509866, 1607167915, 987167468, 1816402316, 1246189591 + ], + C = new Array(160); + function Sha512() { + (this.init(), (this._w = C), _.call(this, 128, 112)); + } + function Ch(s, o, i) { + return i ^ (s & (o ^ i)); + } + function maj(s, o, i) { + return (s & o) | (i & (s | o)); + } + function sigma0(s, o) { + return ((s >>> 28) | (o << 4)) ^ ((o >>> 2) | (s << 30)) ^ ((o >>> 7) | (s << 25)); + } + function sigma1(s, o) { + return ((s >>> 14) | (o << 18)) ^ ((s >>> 18) | (o << 14)) ^ ((o >>> 9) | (s << 23)); + } + function Gamma0(s, o) { + return ((s >>> 1) | (o << 31)) ^ ((s >>> 8) | (o << 24)) ^ (s >>> 7); + } + function Gamma0l(s, o) { + return ((s >>> 1) | (o << 31)) ^ ((s >>> 8) | (o << 24)) ^ ((s >>> 7) | (o << 25)); + } + function Gamma1(s, o) { + return ((s >>> 19) | (o << 13)) ^ ((o >>> 29) | (s << 3)) ^ (s >>> 6); + } + function Gamma1l(s, o) { + return ((s >>> 19) | (o << 13)) ^ ((o >>> 29) | (s << 3)) ^ ((s >>> 6) | (o << 26)); + } + function getCarry(s, o) { + return s >>> 0 < o >>> 0 ? 1 : 0; + } + (u(Sha512, _), + (Sha512.prototype.init = function () { + return ( + (this._ah = 1779033703), + (this._bh = 3144134277), + (this._ch = 1013904242), + (this._dh = 2773480762), + (this._eh = 1359893119), + (this._fh = 2600822924), + (this._gh = 528734635), + (this._hh = 1541459225), + (this._al = 4089235720), + (this._bl = 2227873595), + (this._cl = 4271175723), + (this._dl = 1595750129), + (this._el = 2917565137), + (this._fl = 725511199), + (this._gl = 4215389547), + (this._hl = 327033209), + this + ); + }), + (Sha512.prototype._update = function (s) { + for ( + var o = this._w, + i = 0 | this._ah, + u = 0 | this._bh, + _ = 0 | this._ch, + w = 0 | this._dh, + C = 0 | this._eh, + j = 0 | this._fh, + L = 0 | this._gh, + B = 0 | this._hh, + $ = 0 | this._al, + V = 0 | this._bl, + U = 0 | this._cl, + z = 0 | this._dl, + Y = 0 | this._el, + Z = 0 | this._fl, + ee = 0 | this._gl, + ie = 0 | this._hl, + ae = 0; + ae < 32; + ae += 2 + ) + ((o[ae] = s.readInt32BE(4 * ae)), (o[ae + 1] = s.readInt32BE(4 * ae + 4))); + for (; ae < 160; ae += 2) { + var le = o[ae - 30], + ce = o[ae - 30 + 1], + pe = Gamma0(le, ce), + de = Gamma0l(ce, le), + fe = Gamma1((le = o[ae - 4]), (ce = o[ae - 4 + 1])), + ye = Gamma1l(ce, le), + be = o[ae - 14], + _e = o[ae - 14 + 1], + we = o[ae - 32], + Se = o[ae - 32 + 1], + xe = (de + _e) | 0, + Pe = (pe + be + getCarry(xe, de)) | 0; + ((Pe = + ((Pe = (Pe + fe + getCarry((xe = (xe + ye) | 0), ye)) | 0) + + we + + getCarry((xe = (xe + Se) | 0), Se)) | + 0), + (o[ae] = Pe), + (o[ae + 1] = xe)); + } + for (var Te = 0; Te < 160; Te += 2) { + ((Pe = o[Te]), (xe = o[Te + 1])); + var Re = maj(i, u, _), + qe = maj($, V, U), + $e = sigma0(i, $), + ze = sigma0($, i), + We = sigma1(C, Y), + He = sigma1(Y, C), + Ye = x[Te], + Xe = x[Te + 1], + Qe = Ch(C, j, L), + et = Ch(Y, Z, ee), + tt = (ie + He) | 0, + rt = (B + We + getCarry(tt, ie)) | 0; + rt = + ((rt = + ((rt = (rt + Qe + getCarry((tt = (tt + et) | 0), et)) | 0) + + Ye + + getCarry((tt = (tt + Xe) | 0), Xe)) | + 0) + + Pe + + getCarry((tt = (tt + xe) | 0), xe)) | + 0; + var nt = (ze + qe) | 0, + st = ($e + Re + getCarry(nt, ze)) | 0; + ((B = L), + (ie = ee), + (L = j), + (ee = Z), + (j = C), + (Z = Y), + (C = (w + rt + getCarry((Y = (z + tt) | 0), z)) | 0), + (w = _), + (z = U), + (_ = u), + (U = V), + (u = i), + (V = $), + (i = (rt + st + getCarry(($ = (tt + nt) | 0), tt)) | 0)); + } + ((this._al = (this._al + $) | 0), + (this._bl = (this._bl + V) | 0), + (this._cl = (this._cl + U) | 0), + (this._dl = (this._dl + z) | 0), + (this._el = (this._el + Y) | 0), + (this._fl = (this._fl + Z) | 0), + (this._gl = (this._gl + ee) | 0), + (this._hl = (this._hl + ie) | 0), + (this._ah = (this._ah + i + getCarry(this._al, $)) | 0), + (this._bh = (this._bh + u + getCarry(this._bl, V)) | 0), + (this._ch = (this._ch + _ + getCarry(this._cl, U)) | 0), + (this._dh = (this._dh + w + getCarry(this._dl, z)) | 0), + (this._eh = (this._eh + C + getCarry(this._el, Y)) | 0), + (this._fh = (this._fh + j + getCarry(this._fl, Z)) | 0), + (this._gh = (this._gh + L + getCarry(this._gl, ee)) | 0), + (this._hh = (this._hh + B + getCarry(this._hl, ie)) | 0)); + }), + (Sha512.prototype._hash = function () { + var s = w.allocUnsafe(64); + function writeInt64BE(o, i, u) { + (s.writeInt32BE(o, u), s.writeInt32BE(i, u + 4)); + } + return ( + writeInt64BE(this._ah, this._al, 0), + writeInt64BE(this._bh, this._bl, 8), + writeInt64BE(this._ch, this._cl, 16), + writeInt64BE(this._dh, this._dl, 24), + writeInt64BE(this._eh, this._el, 32), + writeInt64BE(this._fh, this._fl, 40), + writeInt64BE(this._gh, this._gl, 48), + writeInt64BE(this._hh, this._hl, 56), + s + ); + }), + (s.exports = Sha512)); + }, + 8068: (s) => { + 'use strict'; + var o = (() => { + var s = Object.defineProperty, + o = Object.getOwnPropertyDescriptor, + i = Object.getOwnPropertyNames, + u = Object.getOwnPropertySymbols, + _ = Object.prototype.hasOwnProperty, + w = Object.prototype.propertyIsEnumerable, + __defNormalProp = (o, i, u) => + i in o + ? s(o, i, { enumerable: !0, configurable: !0, writable: !0, value: u }) + : (o[i] = u), + __spreadValues = (s, o) => { + for (var i in o || (o = {})) _.call(o, i) && __defNormalProp(s, i, o[i]); + if (u) for (var i of u(o)) w.call(o, i) && __defNormalProp(s, i, o[i]); + return s; + }, + __publicField = (s, o, i) => ( + __defNormalProp(s, 'symbol' != typeof o ? o + '' : o, i), + i + ), + x = {}; + ((o, i) => { + for (var u in i) s(o, u, { get: i[u], enumerable: !0 }); + })(x, { DEFAULT_OPTIONS: () => j, DEFAULT_UUID_LENGTH: () => C, default: () => $ }); + var C = 6, + j = { dictionary: 'alphanum', shuffle: !0, debug: !1, length: C, counter: 0 }, + L = class _ShortUniqueId { + constructor(s = {}) { + (__publicField(this, 'counter'), + __publicField(this, 'debug'), + __publicField(this, 'dict'), + __publicField(this, 'version'), + __publicField(this, 'dictIndex', 0), + __publicField(this, 'dictRange', []), + __publicField(this, 'lowerBound', 0), + __publicField(this, 'upperBound', 0), + __publicField(this, 'dictLength', 0), + __publicField(this, 'uuidLength'), + __publicField(this, '_digit_first_ascii', 48), + __publicField(this, '_digit_last_ascii', 58), + __publicField(this, '_alpha_lower_first_ascii', 97), + __publicField(this, '_alpha_lower_last_ascii', 123), + __publicField(this, '_hex_last_ascii', 103), + __publicField(this, '_alpha_upper_first_ascii', 65), + __publicField(this, '_alpha_upper_last_ascii', 91), + __publicField(this, '_number_dict_ranges', { + digits: [this._digit_first_ascii, this._digit_last_ascii] + }), + __publicField(this, '_alpha_dict_ranges', { + lowerCase: [this._alpha_lower_first_ascii, this._alpha_lower_last_ascii], + upperCase: [this._alpha_upper_first_ascii, this._alpha_upper_last_ascii] + }), + __publicField(this, '_alpha_lower_dict_ranges', { + lowerCase: [this._alpha_lower_first_ascii, this._alpha_lower_last_ascii] + }), + __publicField(this, '_alpha_upper_dict_ranges', { + upperCase: [this._alpha_upper_first_ascii, this._alpha_upper_last_ascii] + }), + __publicField(this, '_alphanum_dict_ranges', { + digits: [this._digit_first_ascii, this._digit_last_ascii], + lowerCase: [this._alpha_lower_first_ascii, this._alpha_lower_last_ascii], + upperCase: [this._alpha_upper_first_ascii, this._alpha_upper_last_ascii] + }), + __publicField(this, '_alphanum_lower_dict_ranges', { + digits: [this._digit_first_ascii, this._digit_last_ascii], + lowerCase: [this._alpha_lower_first_ascii, this._alpha_lower_last_ascii] + }), + __publicField(this, '_alphanum_upper_dict_ranges', { + digits: [this._digit_first_ascii, this._digit_last_ascii], + upperCase: [this._alpha_upper_first_ascii, this._alpha_upper_last_ascii] + }), + __publicField(this, '_hex_dict_ranges', { + decDigits: [this._digit_first_ascii, this._digit_last_ascii], + alphaDigits: [this._alpha_lower_first_ascii, this._hex_last_ascii] + }), + __publicField(this, '_dict_ranges', { + _number_dict_ranges: this._number_dict_ranges, + _alpha_dict_ranges: this._alpha_dict_ranges, + _alpha_lower_dict_ranges: this._alpha_lower_dict_ranges, + _alpha_upper_dict_ranges: this._alpha_upper_dict_ranges, + _alphanum_dict_ranges: this._alphanum_dict_ranges, + _alphanum_lower_dict_ranges: this._alphanum_lower_dict_ranges, + _alphanum_upper_dict_ranges: this._alphanum_upper_dict_ranges, + _hex_dict_ranges: this._hex_dict_ranges + }), + __publicField(this, 'log', (...s) => { + const o = [...s]; + if ( + ((o[0] = `[short-unique-id] ${s[0]}`), + !0 === this.debug && 'undefined' != typeof console && null !== console) + ) + return console.log(...o); + }), + __publicField(this, '_normalizeDictionary', (s, o) => { + let i; + if (s && Array.isArray(s) && s.length > 1) i = s; + else { + let o; + ((i = []), (this.dictIndex = o = 0)); + const u = `_${s}_dict_ranges`, + _ = this._dict_ranges[u]; + Object.keys(_).forEach((s) => { + const u = s; + for ( + this.dictRange = _[u], + this.lowerBound = this.dictRange[0], + this.upperBound = this.dictRange[1], + this.dictIndex = o = this.lowerBound; + this.lowerBound <= this.upperBound + ? o < this.upperBound + : o > this.upperBound; + this.dictIndex = + this.lowerBound <= this.upperBound ? (o += 1) : (o -= 1) + ) + i.push(String.fromCharCode(this.dictIndex)); + }); + } + if (o) { + const s = 0.5; + i = i.sort(() => Math.random() - s); + } + return i; + }), + __publicField(this, 'setDictionary', (s, o) => { + ((this.dict = this._normalizeDictionary(s, o)), + (this.dictLength = this.dict.length), + this.setCounter(0)); + }), + __publicField(this, 'seq', () => this.sequentialUUID()), + __publicField(this, 'sequentialUUID', () => { + let s, + o, + i = ''; + s = this.counter; + do { + ((o = s % this.dictLength), + (s = Math.trunc(s / this.dictLength)), + (i += this.dict[o])); + } while (0 !== s); + return ((this.counter += 1), i); + }), + __publicField(this, 'rnd', (s = this.uuidLength || C) => this.randomUUID(s)), + __publicField(this, 'randomUUID', (s = this.uuidLength || C) => { + let o, i, u; + if (null == s || s < 1) throw new Error('Invalid UUID Length Provided'); + for (o = '', u = 0; u < s; u += 1) + ((i = + parseInt((Math.random() * this.dictLength).toFixed(0), 10) % + this.dictLength), + (o += this.dict[i])); + return o; + }), + __publicField(this, 'fmt', (s, o) => this.formattedUUID(s, o)), + __publicField(this, 'formattedUUID', (s, o) => { + const i = { $r: this.randomUUID, $s: this.sequentialUUID, $t: this.stamp }; + return s.replace(/\$[rs]\d{0,}|\$t0|\$t[1-9]\d{1,}/g, (s) => { + const u = s.slice(0, 2), + _ = parseInt(s.slice(2), 10); + return '$s' === u + ? i[u]().padStart(_, '0') + : '$t' === u && o + ? i[u](_, o) + : i[u](_); + }); + }), + __publicField(this, 'availableUUIDs', (s = this.uuidLength) => + parseFloat(Math.pow([...new Set(this.dict)].length, s).toFixed(0)) + ), + __publicField( + this, + 'approxMaxBeforeCollision', + (s = this.availableUUIDs(this.uuidLength)) => + parseFloat(Math.sqrt((Math.PI / 2) * s).toFixed(20)) + ), + __publicField( + this, + 'collisionProbability', + (s = this.availableUUIDs(this.uuidLength), o = this.uuidLength) => + parseFloat( + (this.approxMaxBeforeCollision(s) / this.availableUUIDs(o)).toFixed(20) + ) + ), + __publicField( + this, + 'uniqueness', + (s = this.availableUUIDs(this.uuidLength)) => { + const o = parseFloat( + (1 - this.approxMaxBeforeCollision(s) / s).toFixed(20) + ); + return o > 1 ? 1 : o < 0 ? 0 : o; + } + ), + __publicField(this, 'getVersion', () => this.version), + __publicField(this, 'stamp', (s, o) => { + const i = Math.floor(+(o || new Date()) / 1e3).toString(16); + if ('number' == typeof s && 0 === s) return i; + if ('number' != typeof s || s < 10) + throw new Error( + [ + 'Param finalLength must be a number greater than or equal to 10,', + 'or 0 if you want the raw hexadecimal timestamp' + ].join('\n') + ); + const u = s - 9, + _ = Math.round(Math.random() * (u > 15 ? 15 : u)), + w = this.randomUUID(u); + return `${w.substring(0, _)}${i}${w.substring(_)}${_.toString(16)}`; + }), + __publicField(this, 'parseStamp', (s, o) => { + if (o && !/t0|t[1-9]\d{1,}/.test(o)) + throw new Error( + 'Cannot extract date from a formated UUID with no timestamp in the format' + ); + const i = o + ? o + .replace(/\$[rs]\d{0,}|\$t0|\$t[1-9]\d{1,}/g, (s) => { + const o = { + $r: (s) => [...Array(s)].map(() => 'r').join(''), + $s: (s) => [...Array(s)].map(() => 's').join(''), + $t: (s) => [...Array(s)].map(() => 't').join('') + }, + i = s.slice(0, 2), + u = parseInt(s.slice(2), 10); + return o[i](u); + }) + .replace(/^(.*?)(t{8,})(.*)$/g, (o, i, u) => + s.substring(i.length, i.length + u.length) + ) + : s; + if (8 === i.length) return new Date(1e3 * parseInt(i, 16)); + if (i.length < 10) throw new Error('Stamp length invalid'); + const u = parseInt(i.substring(i.length - 1), 16); + return new Date(1e3 * parseInt(i.substring(u, u + 8), 16)); + }), + __publicField(this, 'setCounter', (s) => { + this.counter = s; + }), + __publicField(this, 'validate', (s, o) => { + const i = o ? this._normalizeDictionary(o) : this.dict; + return s.split('').every((s) => i.includes(s)); + })); + const o = __spreadValues(__spreadValues({}, j), s); + ((this.counter = 0), + (this.debug = !1), + (this.dict = []), + (this.version = '5.2.0')); + const { dictionary: i, shuffle: u, length: _, counter: w } = o; + return ( + (this.uuidLength = _), + this.setDictionary(i, u), + this.setCounter(w), + (this.debug = o.debug), + this.log(this.dict), + this.log( + `Generator instantiated with Dictionary Size ${this.dictLength} and counter set to ${this.counter}` + ), + (this.log = this.log.bind(this)), + (this.setDictionary = this.setDictionary.bind(this)), + (this.setCounter = this.setCounter.bind(this)), + (this.seq = this.seq.bind(this)), + (this.sequentialUUID = this.sequentialUUID.bind(this)), + (this.rnd = this.rnd.bind(this)), + (this.randomUUID = this.randomUUID.bind(this)), + (this.fmt = this.fmt.bind(this)), + (this.formattedUUID = this.formattedUUID.bind(this)), + (this.availableUUIDs = this.availableUUIDs.bind(this)), + (this.approxMaxBeforeCollision = this.approxMaxBeforeCollision.bind(this)), + (this.collisionProbability = this.collisionProbability.bind(this)), + (this.uniqueness = this.uniqueness.bind(this)), + (this.getVersion = this.getVersion.bind(this)), + (this.stamp = this.stamp.bind(this)), + (this.parseStamp = this.parseStamp.bind(this)), + this + ); + } + }; + __publicField(L, 'default', L); + var B, + $ = L; + return ( + (B = x), + ((u, w, x, C) => { + if ((w && 'object' == typeof w) || 'function' == typeof w) + for (let j of i(w)) + _.call(u, j) || + j === x || + s(u, j, { get: () => w[j], enumerable: !(C = o(w, j)) || C.enumerable }); + return u; + })(s({}, '__esModule', { value: !0 }), B) + ); + })(); + ((s.exports = o.default), 'undefined' != typeof window && (o = o.default)); + }, + 88310: (s, o, i) => { + s.exports = Stream; + var u = i(37007).EventEmitter; + function Stream() { + u.call(this); + } + (i(56698)(Stream, u), + (Stream.Readable = i(45412)), + (Stream.Writable = i(16708)), + (Stream.Duplex = i(25382)), + (Stream.Transform = i(74610)), + (Stream.PassThrough = i(63600)), + (Stream.finished = i(86238)), + (Stream.pipeline = i(57758)), + (Stream.Stream = Stream), + (Stream.prototype.pipe = function (s, o) { + var i = this; + function ondata(o) { + s.writable && !1 === s.write(o) && i.pause && i.pause(); + } + function ondrain() { + i.readable && i.resume && i.resume(); + } + (i.on('data', ondata), + s.on('drain', ondrain), + s._isStdio || (o && !1 === o.end) || (i.on('end', onend), i.on('close', onclose))); + var _ = !1; + function onend() { + _ || ((_ = !0), s.end()); + } + function onclose() { + _ || ((_ = !0), 'function' == typeof s.destroy && s.destroy()); + } + function onerror(s) { + if ((cleanup(), 0 === u.listenerCount(this, 'error'))) throw s; + } + function cleanup() { + (i.removeListener('data', ondata), + s.removeListener('drain', ondrain), + i.removeListener('end', onend), + i.removeListener('close', onclose), + i.removeListener('error', onerror), + s.removeListener('error', onerror), + i.removeListener('end', cleanup), + i.removeListener('close', cleanup), + s.removeListener('close', cleanup)); + } + return ( + i.on('error', onerror), + s.on('error', onerror), + i.on('end', cleanup), + i.on('close', cleanup), + s.on('close', cleanup), + s.emit('pipe', i), + s + ); + })); + }, + 83141: (s, o, i) => { + 'use strict'; + var u = i(92861).Buffer, + _ = + u.isEncoding || + function (s) { + switch ((s = '' + s) && s.toLowerCase()) { + case 'hex': + case 'utf8': + case 'utf-8': + case 'ascii': + case 'binary': + case 'base64': + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + case 'raw': + return !0; + default: + return !1; + } + }; + function StringDecoder(s) { + var o; + switch ( + ((this.encoding = (function normalizeEncoding(s) { + var o = (function _normalizeEncoding(s) { + if (!s) return 'utf8'; + for (var o; ; ) + switch (s) { + case 'utf8': + case 'utf-8': + return 'utf8'; + case 'ucs2': + case 'ucs-2': + case 'utf16le': + case 'utf-16le': + return 'utf16le'; + case 'latin1': + case 'binary': + return 'latin1'; + case 'base64': + case 'ascii': + case 'hex': + return s; + default: + if (o) return; + ((s = ('' + s).toLowerCase()), (o = !0)); + } + })(s); + if ('string' != typeof o && (u.isEncoding === _ || !_(s))) + throw new Error('Unknown encoding: ' + s); + return o || s; + })(s)), + this.encoding) + ) { + case 'utf16le': + ((this.text = utf16Text), (this.end = utf16End), (o = 4)); + break; + case 'utf8': + ((this.fillLast = utf8FillLast), (o = 4)); + break; + case 'base64': + ((this.text = base64Text), (this.end = base64End), (o = 3)); + break; + default: + return ((this.write = simpleWrite), void (this.end = simpleEnd)); + } + ((this.lastNeed = 0), (this.lastTotal = 0), (this.lastChar = u.allocUnsafe(o))); + } + function utf8CheckByte(s) { + return s <= 127 + ? 0 + : s >> 5 == 6 + ? 2 + : s >> 4 == 14 + ? 3 + : s >> 3 == 30 + ? 4 + : s >> 6 == 2 + ? -1 + : -2; + } + function utf8FillLast(s) { + var o = this.lastTotal - this.lastNeed, + i = (function utf8CheckExtraBytes(s, o, i) { + if (128 != (192 & o[0])) return ((s.lastNeed = 0), '�'); + if (s.lastNeed > 1 && o.length > 1) { + if (128 != (192 & o[1])) return ((s.lastNeed = 1), '�'); + if (s.lastNeed > 2 && o.length > 2 && 128 != (192 & o[2])) + return ((s.lastNeed = 2), '�'); + } + })(this, s); + return void 0 !== i + ? i + : this.lastNeed <= s.length + ? (s.copy(this.lastChar, o, 0, this.lastNeed), + this.lastChar.toString(this.encoding, 0, this.lastTotal)) + : (s.copy(this.lastChar, o, 0, s.length), void (this.lastNeed -= s.length)); + } + function utf16Text(s, o) { + if ((s.length - o) % 2 == 0) { + var i = s.toString('utf16le', o); + if (i) { + var u = i.charCodeAt(i.length - 1); + if (u >= 55296 && u <= 56319) + return ( + (this.lastNeed = 2), + (this.lastTotal = 4), + (this.lastChar[0] = s[s.length - 2]), + (this.lastChar[1] = s[s.length - 1]), + i.slice(0, -1) + ); + } + return i; + } + return ( + (this.lastNeed = 1), + (this.lastTotal = 2), + (this.lastChar[0] = s[s.length - 1]), + s.toString('utf16le', o, s.length - 1) + ); + } + function utf16End(s) { + var o = s && s.length ? this.write(s) : ''; + if (this.lastNeed) { + var i = this.lastTotal - this.lastNeed; + return o + this.lastChar.toString('utf16le', 0, i); + } + return o; + } + function base64Text(s, o) { + var i = (s.length - o) % 3; + return 0 === i + ? s.toString('base64', o) + : ((this.lastNeed = 3 - i), + (this.lastTotal = 3), + 1 === i + ? (this.lastChar[0] = s[s.length - 1]) + : ((this.lastChar[0] = s[s.length - 2]), (this.lastChar[1] = s[s.length - 1])), + s.toString('base64', o, s.length - i)); + } + function base64End(s) { + var o = s && s.length ? this.write(s) : ''; + return this.lastNeed ? o + this.lastChar.toString('base64', 0, 3 - this.lastNeed) : o; + } + function simpleWrite(s) { + return s.toString(this.encoding); + } + function simpleEnd(s) { + return s && s.length ? this.write(s) : ''; + } + ((o.I = StringDecoder), + (StringDecoder.prototype.write = function (s) { + if (0 === s.length) return ''; + var o, i; + if (this.lastNeed) { + if (void 0 === (o = this.fillLast(s))) return ''; + ((i = this.lastNeed), (this.lastNeed = 0)); + } else i = 0; + return i < s.length ? (o ? o + this.text(s, i) : this.text(s, i)) : o || ''; + }), + (StringDecoder.prototype.end = function utf8End(s) { + var o = s && s.length ? this.write(s) : ''; + return this.lastNeed ? o + '�' : o; + }), + (StringDecoder.prototype.text = function utf8Text(s, o) { + var i = (function utf8CheckIncomplete(s, o, i) { + var u = o.length - 1; + if (u < i) return 0; + var _ = utf8CheckByte(o[u]); + if (_ >= 0) return (_ > 0 && (s.lastNeed = _ - 1), _); + if (--u < i || -2 === _) return 0; + if (((_ = utf8CheckByte(o[u])), _ >= 0)) return (_ > 0 && (s.lastNeed = _ - 2), _); + if (--u < i || -2 === _) return 0; + if (((_ = utf8CheckByte(o[u])), _ >= 0)) + return (_ > 0 && (2 === _ ? (_ = 0) : (s.lastNeed = _ - 3)), _); + return 0; + })(this, s, o); + if (!this.lastNeed) return s.toString('utf8', o); + this.lastTotal = i; + var u = s.length - (i - this.lastNeed); + return (s.copy(this.lastChar, 0, u), s.toString('utf8', o, u)); + }), + (StringDecoder.prototype.fillLast = function (s) { + if (this.lastNeed <= s.length) + return ( + s.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, this.lastNeed), + this.lastChar.toString(this.encoding, 0, this.lastTotal) + ); + (s.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, s.length), + (this.lastNeed -= s.length)); + })); + }, + 69883: (s, o) => { + 'use strict'; + ((o.parse = function parse(s, o) { + if ('string' != typeof s) throw new TypeError('argument str must be a string'); + var i = {}, + _ = s.length; + if (_ < 2) return i; + var w = (o && o.decode) || decode, + x = 0, + C = 0, + j = 0; + do { + if (-1 === (C = s.indexOf('=', x))) break; + if (-1 === (j = s.indexOf(';', x))) j = _; + else if (C > j) { + x = s.lastIndexOf(';', C - 1) + 1; + continue; + } + var L = startIndex(s, x, C), + B = endIndex(s, C, L), + $ = s.slice(L, B); + if (!u.call(i, $)) { + var V = startIndex(s, C + 1, j), + U = endIndex(s, j, V); + 34 === s.charCodeAt(V) && 34 === s.charCodeAt(U - 1) && (V++, U--); + var z = s.slice(V, U); + i[$] = tryDecode(z, w); + } + x = j + 1; + } while (x < _); + return i; + }), + (o.serialize = function serialize(s, o, u) { + var j = (u && u.encode) || encodeURIComponent; + if ('function' != typeof j) throw new TypeError('option encode is invalid'); + if (!_.test(s)) throw new TypeError('argument name is invalid'); + var L = j(o); + if (!w.test(L)) throw new TypeError('argument val is invalid'); + var B = s + '=' + L; + if (!u) return B; + if (null != u.maxAge) { + var $ = Math.floor(u.maxAge); + if (!isFinite($)) throw new TypeError('option maxAge is invalid'); + B += '; Max-Age=' + $; + } + if (u.domain) { + if (!x.test(u.domain)) throw new TypeError('option domain is invalid'); + B += '; Domain=' + u.domain; + } + if (u.path) { + if (!C.test(u.path)) throw new TypeError('option path is invalid'); + B += '; Path=' + u.path; + } + if (u.expires) { + var V = u.expires; + if ( + !(function isDate(s) { + return '[object Date]' === i.call(s); + })(V) || + isNaN(V.valueOf()) + ) + throw new TypeError('option expires is invalid'); + B += '; Expires=' + V.toUTCString(); + } + u.httpOnly && (B += '; HttpOnly'); + u.secure && (B += '; Secure'); + u.partitioned && (B += '; Partitioned'); + if (u.priority) { + switch ('string' == typeof u.priority ? u.priority.toLowerCase() : u.priority) { + case 'low': + B += '; Priority=Low'; + break; + case 'medium': + B += '; Priority=Medium'; + break; + case 'high': + B += '; Priority=High'; + break; + default: + throw new TypeError('option priority is invalid'); + } + } + if (u.sameSite) { + switch ('string' == typeof u.sameSite ? u.sameSite.toLowerCase() : u.sameSite) { + case !0: + B += '; SameSite=Strict'; + break; + case 'lax': + B += '; SameSite=Lax'; + break; + case 'strict': + B += '; SameSite=Strict'; + break; + case 'none': + B += '; SameSite=None'; + break; + default: + throw new TypeError('option sameSite is invalid'); + } + } + return B; + })); + var i = Object.prototype.toString, + u = Object.prototype.hasOwnProperty, + _ = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/, + w = /^("?)[\u0021\u0023-\u002B\u002D-\u003A\u003C-\u005B\u005D-\u007E]*\1$/, + x = + /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i, + C = /^[\u0020-\u003A\u003D-\u007E]*$/; + function startIndex(s, o, i) { + do { + var u = s.charCodeAt(o); + if (32 !== u && 9 !== u) return o; + } while (++o < i); + return i; + } + function endIndex(s, o, i) { + for (; o > i; ) { + var u = s.charCodeAt(--o); + if (32 !== u && 9 !== u) return o + 1; + } + return i; + } + function decode(s) { + return -1 !== s.indexOf('%') ? decodeURIComponent(s) : s; + } + function tryDecode(s, o) { + try { + return o(s); + } catch (o) { + return s; + } + } + }, + 16426: (s) => { + s.exports = function () { + var s = document.getSelection(); + if (!s.rangeCount) return function () {}; + for (var o = document.activeElement, i = [], u = 0; u < s.rangeCount; u++) + i.push(s.getRangeAt(u)); + switch (o.tagName.toUpperCase()) { + case 'INPUT': + case 'TEXTAREA': + o.blur(); + break; + default: + o = null; + } + return ( + s.removeAllRanges(), + function () { + ('Caret' === s.type && s.removeAllRanges(), + s.rangeCount || + i.forEach(function (o) { + s.addRange(o); + }), + o && o.focus()); + } + ); + }; + }, + 61160: (s, o, i) => { + 'use strict'; + var u = i(92063), + _ = i(73992), + w = /^[\x00-\x20\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/, + x = /[\n\r\t]/g, + C = /^[A-Za-z][A-Za-z0-9+-.]*:\/\//, + j = /:\d+$/, + L = /^([a-z][a-z0-9.+-]*:)?(\/\/)?([\\/]+)?([\S\s]*)/i, + B = /^[a-zA-Z]:/; + function trimLeft(s) { + return (s || '').toString().replace(w, ''); + } + var $ = [ + ['#', 'hash'], + ['?', 'query'], + function sanitize(s, o) { + return isSpecial(o.protocol) ? s.replace(/\\/g, '/') : s; + }, + ['/', 'pathname'], + ['@', 'auth', 1], + [NaN, 'host', void 0, 1, 1], + [/:(\d*)$/, 'port', void 0, 1], + [NaN, 'hostname', void 0, 1, 1] + ], + V = { hash: 1, query: 1 }; + function lolcation(s) { + var o, + u = + ('undefined' != typeof window + ? window + : void 0 !== i.g + ? i.g + : 'undefined' != typeof self + ? self + : {} + ).location || {}, + _ = {}, + w = typeof (s = s || u); + if ('blob:' === s.protocol) _ = new Url(unescape(s.pathname), {}); + else if ('string' === w) for (o in ((_ = new Url(s, {})), V)) delete _[o]; + else if ('object' === w) { + for (o in s) o in V || (_[o] = s[o]); + void 0 === _.slashes && (_.slashes = C.test(s.href)); + } + return _; + } + function isSpecial(s) { + return ( + 'file:' === s || + 'ftp:' === s || + 'http:' === s || + 'https:' === s || + 'ws:' === s || + 'wss:' === s + ); + } + function extractProtocol(s, o) { + ((s = (s = trimLeft(s)).replace(x, '')), (o = o || {})); + var i, + u = L.exec(s), + _ = u[1] ? u[1].toLowerCase() : '', + w = !!u[2], + C = !!u[3], + j = 0; + return ( + w + ? C + ? ((i = u[2] + u[3] + u[4]), (j = u[2].length + u[3].length)) + : ((i = u[2] + u[4]), (j = u[2].length)) + : C + ? ((i = u[3] + u[4]), (j = u[3].length)) + : (i = u[4]), + 'file:' === _ + ? j >= 2 && (i = i.slice(2)) + : isSpecial(_) + ? (i = u[4]) + : _ + ? w && (i = i.slice(2)) + : j >= 2 && isSpecial(o.protocol) && (i = u[4]), + { protocol: _, slashes: w || isSpecial(_), slashesCount: j, rest: i } + ); + } + function Url(s, o, i) { + if (((s = (s = trimLeft(s)).replace(x, '')), !(this instanceof Url))) + return new Url(s, o, i); + var w, + C, + j, + L, + V, + U, + z = $.slice(), + Y = typeof o, + Z = this, + ee = 0; + for ( + 'object' !== Y && 'string' !== Y && ((i = o), (o = null)), + i && 'function' != typeof i && (i = _.parse), + w = !(C = extractProtocol(s || '', (o = lolcation(o)))).protocol && !C.slashes, + Z.slashes = C.slashes || (w && o.slashes), + Z.protocol = C.protocol || o.protocol || '', + s = C.rest, + (('file:' === C.protocol && (2 !== C.slashesCount || B.test(s))) || + (!C.slashes && (C.protocol || C.slashesCount < 2 || !isSpecial(Z.protocol)))) && + (z[3] = [/(.*)/, 'pathname']); + ee < z.length; + ee++ + ) + 'function' != typeof (L = z[ee]) + ? ((j = L[0]), + (U = L[1]), + j != j + ? (Z[U] = s) + : 'string' == typeof j + ? ~(V = '@' === j ? s.lastIndexOf(j) : s.indexOf(j)) && + ('number' == typeof L[2] + ? ((Z[U] = s.slice(0, V)), (s = s.slice(V + L[2]))) + : ((Z[U] = s.slice(V)), (s = s.slice(0, V)))) + : (V = j.exec(s)) && ((Z[U] = V[1]), (s = s.slice(0, V.index))), + (Z[U] = Z[U] || (w && L[3] && o[U]) || ''), + L[4] && (Z[U] = Z[U].toLowerCase())) + : (s = L(s, Z)); + (i && (Z.query = i(Z.query)), + w && + o.slashes && + '/' !== Z.pathname.charAt(0) && + ('' !== Z.pathname || '' !== o.pathname) && + (Z.pathname = (function resolve(s, o) { + if ('' === s) return o; + for ( + var i = (o || '/').split('/').slice(0, -1).concat(s.split('/')), + u = i.length, + _ = i[u - 1], + w = !1, + x = 0; + u--; + ) + '.' === i[u] + ? i.splice(u, 1) + : '..' === i[u] + ? (i.splice(u, 1), x++) + : x && (0 === u && (w = !0), i.splice(u, 1), x--); + return (w && i.unshift(''), ('.' !== _ && '..' !== _) || i.push(''), i.join('/')); + })(Z.pathname, o.pathname)), + '/' !== Z.pathname.charAt(0) && + isSpecial(Z.protocol) && + (Z.pathname = '/' + Z.pathname), + u(Z.port, Z.protocol) || ((Z.host = Z.hostname), (Z.port = '')), + (Z.username = Z.password = ''), + Z.auth && + (~(V = Z.auth.indexOf(':')) + ? ((Z.username = Z.auth.slice(0, V)), + (Z.username = encodeURIComponent(decodeURIComponent(Z.username))), + (Z.password = Z.auth.slice(V + 1)), + (Z.password = encodeURIComponent(decodeURIComponent(Z.password)))) + : (Z.username = encodeURIComponent(decodeURIComponent(Z.auth))), + (Z.auth = Z.password ? Z.username + ':' + Z.password : Z.username)), + (Z.origin = + 'file:' !== Z.protocol && isSpecial(Z.protocol) && Z.host + ? Z.protocol + '//' + Z.host + : 'null'), + (Z.href = Z.toString())); + } + ((Url.prototype = { + set: function set(s, o, i) { + var w = this; + switch (s) { + case 'query': + ('string' == typeof o && o.length && (o = (i || _.parse)(o)), (w[s] = o)); + break; + case 'port': + ((w[s] = o), + u(o, w.protocol) + ? o && (w.host = w.hostname + ':' + o) + : ((w.host = w.hostname), (w[s] = ''))); + break; + case 'hostname': + ((w[s] = o), w.port && (o += ':' + w.port), (w.host = o)); + break; + case 'host': + ((w[s] = o), + j.test(o) + ? ((o = o.split(':')), (w.port = o.pop()), (w.hostname = o.join(':'))) + : ((w.hostname = o), (w.port = ''))); + break; + case 'protocol': + ((w.protocol = o.toLowerCase()), (w.slashes = !i)); + break; + case 'pathname': + case 'hash': + if (o) { + var x = 'pathname' === s ? '/' : '#'; + w[s] = o.charAt(0) !== x ? x + o : o; + } else w[s] = o; + break; + case 'username': + case 'password': + w[s] = encodeURIComponent(o); + break; + case 'auth': + var C = o.indexOf(':'); + ~C + ? ((w.username = o.slice(0, C)), + (w.username = encodeURIComponent(decodeURIComponent(w.username))), + (w.password = o.slice(C + 1)), + (w.password = encodeURIComponent(decodeURIComponent(w.password)))) + : (w.username = encodeURIComponent(decodeURIComponent(o))); + } + for (var L = 0; L < $.length; L++) { + var B = $[L]; + B[4] && (w[B[1]] = w[B[1]].toLowerCase()); + } + return ( + (w.auth = w.password ? w.username + ':' + w.password : w.username), + (w.origin = + 'file:' !== w.protocol && isSpecial(w.protocol) && w.host + ? w.protocol + '//' + w.host + : 'null'), + (w.href = w.toString()), + w + ); + }, + toString: function toString(s) { + (s && 'function' == typeof s) || (s = _.stringify); + var o, + i = this, + u = i.host, + w = i.protocol; + w && ':' !== w.charAt(w.length - 1) && (w += ':'); + var x = w + ((i.protocol && i.slashes) || isSpecial(i.protocol) ? '//' : ''); + return ( + i.username + ? ((x += i.username), i.password && (x += ':' + i.password), (x += '@')) + : i.password + ? ((x += ':' + i.password), (x += '@')) + : 'file:' !== i.protocol && + isSpecial(i.protocol) && + !u && + '/' !== i.pathname && + (x += '@'), + (':' === u[u.length - 1] || (j.test(i.hostname) && !i.port)) && (u += ':'), + (x += u + i.pathname), + (o = 'object' == typeof i.query ? s(i.query) : i.query) && + (x += '?' !== o.charAt(0) ? '?' + o : o), + i.hash && (x += i.hash), + x + ); + } + }), + (Url.extractProtocol = extractProtocol), + (Url.location = lolcation), + (Url.trimLeft = trimLeft), + (Url.qs = _), + (s.exports = Url)); + }, + 77154: (s, o, i) => { + 'use strict'; + var u = i(96540); + var _ = + 'function' == typeof Object.is + ? Object.is + : function n(s, o) { + return (s === o && (0 !== s || 1 / s == 1 / o)) || (s != s && o != o); + }, + w = u.useSyncExternalStore, + x = u.useRef, + C = u.useEffect, + j = u.useMemo, + L = u.useDebugValue; + o.useSyncExternalStoreWithSelector = function (s, o, i, u, B) { + var $ = x(null); + if (null === $.current) { + var V = { hasValue: !1, value: null }; + $.current = V; + } else V = $.current; + $ = j( + function () { + function a(o) { + if (!x) { + if (((x = !0), (s = o), (o = u(o)), void 0 !== B && V.hasValue)) { + var i = V.value; + if (B(i, o)) return (w = i); + } + return (w = o); + } + if (((i = w), _(s, o))) return i; + var C = u(o); + return void 0 !== B && B(i, C) ? i : ((s = o), (w = C)); + } + var s, + w, + x = !1, + C = void 0 === i ? null : i; + return [ + function () { + return a(o()); + }, + null === C + ? void 0 + : function () { + return a(C()); + } + ]; + }, + [o, i, u, B] + ); + var U = w(s, $[0], $[1]); + return ( + C( + function () { + ((V.hasValue = !0), (V.value = U)); + }, + [U] + ), + L(U), + U + ); + }; + }, + 78418: (s, o, i) => { + 'use strict'; + s.exports = i(77154); + }, + 94643: (s, o, i) => { + function config(s) { + try { + if (!i.g.localStorage) return !1; + } catch (s) { + return !1; + } + var o = i.g.localStorage[s]; + return null != o && 'true' === String(o).toLowerCase(); + } + s.exports = function deprecate(s, o) { + if (config('noDeprecation')) return s; + var i = !1; + return function deprecated() { + if (!i) { + if (config('throwDeprecation')) throw new Error(o); + (config('traceDeprecation') ? console.trace(o) : console.warn(o), (i = !0)); + } + return s.apply(this, arguments); + }; + }; + }, + 26657: (s, o, i) => { + 'use strict'; + var u = i(75208), + _ = function isClosingTag(s) { + return /<\/+[^>]+>/.test(s); + }, + w = function isSelfClosingTag(s) { + return /<[^>]+\/>/.test(s); + }; + function getType(s) { + return _(s) + ? 'ClosingTag' + : (function isOpeningTag(s) { + return ( + (function isTag(s) { + return /<[^>!]+>/.test(s); + })(s) && + !_(s) && + !w(s) + ); + })(s) + ? 'OpeningTag' + : w(s) + ? 'SelfClosingTag' + : 'Text'; + } + s.exports = function (s) { + var o = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}, + i = o.indentor, + _ = o.textNodesOnSameLine, + w = 0, + x = []; + i = i || ' '; + var C = (function lexer(s) { + return (function splitOnTags(s) { + return s.split(/(<\/?[^>]+>)/g).filter(function (s) { + return '' !== s.trim(); + }); + })(s).map(function (s) { + return { value: s, type: getType(s) }; + }); + })(s).map(function (s, o, C) { + var j = s.value, + L = s.type; + 'ClosingTag' === L && w--; + var B = u(i, w), + $ = B + j; + if (('OpeningTag' === L && w++, _)) { + var V = C[o - 1], + U = C[o - 2]; + 'ClosingTag' === L && + 'Text' === V.type && + 'OpeningTag' === U.type && + (($ = '' + B + U.value + V.value + j), x.push(o - 2, o - 1)); + } + return $; + }); + return ( + x.forEach(function (s) { + return (C[s] = null); + }), + C.filter(function (s) { + return !!s; + }).join('\n') + ); + }; + }, + 31499: (s) => { + var o = { '&': '&', '"': '"', "'": ''', '<': '<', '>': '>' }; + s.exports = function escapeForXML(s) { + return s && s.replace + ? s.replace(/([&"<>'])/g, function (s, i) { + return o[i]; + }) + : s; + }; + }, + 19123: (s, o, i) => { + var u = i(65606), + _ = i(31499), + w = i(88310).Stream; + function resolve(s, o, i) { + var u, + w = (function create_indent(s, o) { + return new Array(o || 0).join(s || ''); + })(o, (i = i || 0)), + x = s; + if ('object' == typeof s && (x = s[(u = Object.keys(s)[0])]) && x._elem) + return ( + (x._elem.name = u), + (x._elem.icount = i), + (x._elem.indent = o), + (x._elem.indents = w), + (x._elem.interrupt = x), + x._elem + ); + var C, + j = [], + L = []; + function get_attributes(s) { + Object.keys(s).forEach(function (o) { + j.push( + (function attribute(s, o) { + return s + '="' + _(o) + '"'; + })(o, s[o]) + ); + }); + } + switch (typeof x) { + case 'object': + if (null === x) break; + (x._attr && get_attributes(x._attr), + x._cdata && + L.push(('/g, ']]]]>') + ']]>'), + x.forEach && + ((C = !1), + L.push(''), + x.forEach(function (s) { + 'object' == typeof s + ? '_attr' == Object.keys(s)[0] + ? get_attributes(s._attr) + : L.push(resolve(s, o, i + 1)) + : (L.pop(), (C = !0), L.push(_(s))); + }), + C || L.push(''))); + break; + default: + L.push(_(x)); + } + return { + name: u, + interrupt: !1, + attributes: j, + content: L, + icount: i, + indents: w, + indent: o + }; + } + function format(s, o, i) { + if ('object' != typeof o) return s(!1, o); + var u = o.interrupt ? 1 : o.content.length; + function proceed() { + for (; o.content.length; ) { + var _ = o.content.shift(); + if (void 0 !== _) { + if (interrupt(_)) return; + format(s, _); + } + } + (s( + !1, + (u > 1 ? o.indents : '') + + (o.name ? '' : '') + + (o.indent && !i ? '\n' : '') + ), + i && i()); + } + function interrupt(o) { + return ( + !!o.interrupt && + ((o.interrupt.append = s), + (o.interrupt.end = proceed), + (o.interrupt = !1), + s(!0), + !0) + ); + } + if ( + (s( + !1, + o.indents + + (o.name ? '<' + o.name : '') + + (o.attributes.length ? ' ' + o.attributes.join(' ') : '') + + (u ? (o.name ? '>' : '') : o.name ? '/>' : '') + + (o.indent && u > 1 ? '\n' : '') + ), + !u) + ) + return s(!1, o.indent ? '\n' : ''); + interrupt(o) || proceed(); + } + ((s.exports = function xml(s, o) { + 'object' != typeof o && (o = { indent: o }); + var i = o.stream ? new w() : null, + _ = '', + x = !1, + C = o.indent ? (!0 === o.indent ? ' ' : o.indent) : '', + j = !0; + function delay(s) { + j ? u.nextTick(s) : s(); + } + function append(s, o) { + if ((void 0 !== o && (_ += o), s && !x && ((i = i || new w()), (x = !0)), s && x)) { + var u = _; + (delay(function () { + i.emit('data', u); + }), + (_ = '')); + } + } + function add(s, o) { + format(append, resolve(s, C, C ? 1 : 0), o); + } + function end() { + if (i) { + var s = _; + delay(function () { + (i.emit('data', s), i.emit('end'), (i.readable = !1), i.emit('close')); + }); + } + } + return ( + delay(function () { + j = !1; + }), + o.declaration && + (function addXmlDeclaration(s) { + var o = { version: '1.0', encoding: s.encoding || 'UTF-8' }; + (s.standalone && (o.standalone = s.standalone), + add({ '?xml': { _attr: o } }), + (_ = _.replace('/>', '?>'))); + })(o.declaration), + s && s.forEach + ? s.forEach(function (o, i) { + var u; + (i + 1 === s.length && (u = end), add(o, u)); + }) + : add(s, end), + i ? ((i.readable = !0), i) : _ + ); + }), + (s.exports.element = s.exports.Element = + function element() { + var s = { + _elem: resolve(Array.prototype.slice.call(arguments)), + push: function (s) { + if (!this.append) throw new Error('not assigned to a parent!'); + var o = this, + i = this._elem.indent; + format( + this.append, + resolve(s, i, this._elem.icount + (i ? 1 : 0)), + function () { + o.append(!0); + } + ); + }, + close: function (s) { + (void 0 !== s && this.push(s), this.end && this.end()); + } + }; + return s; + })); + }, + 86215: function (s, o) { + var i, u, _; + ((u = []), + (i = (function () { + 'use strict'; + var isNativeSmoothScrollEnabledOn = function (s) { + return ( + s && + 'getComputedStyle' in window && + 'smooth' === window.getComputedStyle(s)['scroll-behavior'] + ); + }; + if ('undefined' == typeof window || !('document' in window)) return {}; + var makeScroller = function (s, o, i) { + var u; + ((o = o || 999), i || 0 === i || (i = 9)); + var setScrollTimeoutId = function (s) { + u = s; + }, + stopScroll = function () { + (clearTimeout(u), setScrollTimeoutId(0)); + }, + getTopWithEdgeOffset = function (o) { + return Math.max(0, s.getTopOf(o) - i); + }, + scrollToY = function (i, u, _) { + if ( + (stopScroll(), + 0 === u || (u && u < 0) || isNativeSmoothScrollEnabledOn(s.body)) + ) + (s.toY(i), _ && _()); + else { + var w = s.getY(), + x = Math.max(0, i) - w, + C = new Date().getTime(); + ((u = u || Math.min(Math.abs(x), o)), + (function loopScroll() { + setScrollTimeoutId( + setTimeout(function () { + var o = Math.min(1, (new Date().getTime() - C) / u), + i = Math.max( + 0, + Math.floor(w + x * (o < 0.5 ? 2 * o * o : o * (4 - 2 * o) - 1)) + ); + (s.toY(i), + o < 1 && s.getHeight() + i < s.body.scrollHeight + ? loopScroll() + : (setTimeout(stopScroll, 99), _ && _())); + }, 9) + ); + })()); + } + }, + scrollToElem = function (s, o, i) { + scrollToY(getTopWithEdgeOffset(s), o, i); + }, + scrollIntoView = function (o, u, _) { + var w = o.getBoundingClientRect().height, + x = s.getTopOf(o) + w, + C = s.getHeight(), + j = s.getY(), + L = j + C; + getTopWithEdgeOffset(o) < j || w + i > C + ? scrollToElem(o, u, _) + : x + i > L + ? scrollToY(x - C + i, u, _) + : _ && _(); + }, + scrollToCenterOf = function (o, i, u, _) { + scrollToY( + Math.max( + 0, + s.getTopOf(o) - + s.getHeight() / 2 + + (u || o.getBoundingClientRect().height / 2) + ), + i, + _ + ); + }; + return { + setup: function (s, u) { + return ( + (0 === s || s) && (o = s), + (0 === u || u) && (i = u), + { defaultDuration: o, edgeOffset: i } + ); + }, + to: scrollToElem, + toY: scrollToY, + intoView: scrollIntoView, + center: scrollToCenterOf, + stop: stopScroll, + moving: function () { + return !!u; + }, + getY: s.getY, + getTopOf: s.getTopOf + }; + }, + s = document.documentElement, + getDocY = function () { + return window.scrollY || s.scrollTop; + }, + o = makeScroller({ + body: document.scrollingElement || document.body, + toY: function (s) { + window.scrollTo(0, s); + }, + getY: getDocY, + getHeight: function () { + return window.innerHeight || s.clientHeight; + }, + getTopOf: function (o) { + return o.getBoundingClientRect().top + getDocY() - s.offsetTop; + } + }); + if ( + ((o.createScroller = function (o, i, u) { + return makeScroller( + { + body: o, + toY: function (s) { + o.scrollTop = s; + }, + getY: function () { + return o.scrollTop; + }, + getHeight: function () { + return Math.min(o.clientHeight, window.innerHeight || s.clientHeight); + }, + getTopOf: function (s) { + return s.offsetTop; + } + }, + i, + u + ); + }), + 'addEventListener' in window && + !window.noZensmooth && + !isNativeSmoothScrollEnabledOn(document.body)) + ) { + var i = 'history' in window && 'pushState' in history, + u = i && 'scrollRestoration' in history; + (u && (history.scrollRestoration = 'auto'), + window.addEventListener( + 'load', + function () { + (u && + (setTimeout(function () { + history.scrollRestoration = 'manual'; + }, 9), + window.addEventListener( + 'popstate', + function (s) { + s.state && 'zenscrollY' in s.state && o.toY(s.state.zenscrollY); + }, + !1 + )), + window.location.hash && + setTimeout(function () { + var s = o.setup().edgeOffset; + if (s) { + var i = document.getElementById(window.location.href.split('#')[1]); + if (i) { + var u = Math.max(0, o.getTopOf(i) - s), + _ = o.getY() - u; + 0 <= _ && _ < 9 && window.scrollTo(0, u); + } + } + }, 9)); + }, + !1 + )); + var _ = new RegExp('(^|\\s)noZensmooth(\\s|$)'); + window.addEventListener( + 'click', + function (s) { + for (var w = s.target; w && 'A' !== w.tagName; ) w = w.parentNode; + if ( + !(!w || 1 !== s.which || s.shiftKey || s.metaKey || s.ctrlKey || s.altKey) + ) { + if (u) { + var x = + history.state && 'object' == typeof history.state ? history.state : {}; + x.zenscrollY = o.getY(); + try { + history.replaceState(x, ''); + } catch (s) {} + } + var C = w.getAttribute('href') || ''; + if (0 === C.indexOf('#') && !_.test(w.className)) { + var j = 0, + L = document.getElementById(C.substring(1)); + if ('#' !== C) { + if (!L) return; + j = o.getTopOf(L); + } + s.preventDefault(); + var onDone = function () { + window.location = C; + }, + B = o.setup().edgeOffset; + (B && + ((j = Math.max(0, j - B)), + i && + (onDone = function () { + history.pushState({}, '', C); + })), + o.toY(j, null, onDone)); + } + } + }, + !1 + ); + } + return o; + })()), + void 0 === (_ = 'function' == typeof i ? i.apply(o, u) : i) || (s.exports = _)); + }, + 15340: () => {}, + 79838: () => {}, + 48675: (s, o, i) => { + s.exports = i(20850); + }, + 7666: (s, o, i) => { + var u = i(84851), + _ = i(953); + function _extends() { + var o; + return ( + (s.exports = _extends = + u + ? _((o = u)).call(o) + : function (s) { + for (var o = 1; o < arguments.length; o++) { + var i = arguments[o]; + for (var u in i) ({}).hasOwnProperty.call(i, u) && (s[u] = i[u]); + } + return s; + }), + (s.exports.__esModule = !0), + (s.exports.default = s.exports), + _extends.apply(null, arguments) + ); + } + ((s.exports = _extends), (s.exports.__esModule = !0), (s.exports.default = s.exports)); + }, + 46942: (s, o) => { + var i; + !(function () { + 'use strict'; + var u = {}.hasOwnProperty; + function classNames() { + for (var s = '', o = 0; o < arguments.length; o++) { + var i = arguments[o]; + i && (s = appendClass(s, parseValue(i))); + } + return s; + } + function parseValue(s) { + if ('string' == typeof s || 'number' == typeof s) return s; + if ('object' != typeof s) return ''; + if (Array.isArray(s)) return classNames.apply(null, s); + if ( + s.toString !== Object.prototype.toString && + !s.toString.toString().includes('[native code]') + ) + return s.toString(); + var o = ''; + for (var i in s) u.call(s, i) && s[i] && (o = appendClass(o, i)); + return o; + } + function appendClass(s, o) { + return o ? (s ? s + ' ' + o : s + o) : s; + } + s.exports + ? ((classNames.default = classNames), (s.exports = classNames)) + : void 0 === + (i = function () { + return classNames; + }.apply(o, [])) || (s.exports = i); + })(); + }, + 68623: (s, o, i) => { + 'use strict'; + var u = i(694); + s.exports = u; + }, + 93700: (s, o, i) => { + 'use strict'; + var u = i(19709); + s.exports = u; + }, + 462: (s, o, i) => { + 'use strict'; + var u = i(40975); + s.exports = u; + }, + 37257: (s, o, i) => { + 'use strict'; + (i(96605), i(64502), i(36371), i(99363), i(7057)); + var u = i(92046); + s.exports = u.AggregateError; + }, + 32567: (s, o, i) => { + 'use strict'; + i(79307); + var u = i(61747); + s.exports = u('Function', 'bind'); + }, + 23034: (s, o, i) => { + 'use strict'; + var u = i(88280), + _ = i(32567), + w = Function.prototype; + s.exports = function (s) { + var o = s.bind; + return s === w || (u(w, s) && o === w.bind) ? _ : o; + }; + }, + 9748: (s, o, i) => { + 'use strict'; + i(71340); + var u = i(92046); + s.exports = u.Object.assign; + }, + 20850: (s, o, i) => { + 'use strict'; + s.exports = i(46076); + }, + 953: (s, o, i) => { + 'use strict'; + s.exports = i(53375); + }, + 84851: (s, o, i) => { + 'use strict'; + s.exports = i(85401); + }, + 46076: (s, o, i) => { + 'use strict'; + i(91599); + var u = i(68623); + s.exports = u; + }, + 53375: (s, o, i) => { + 'use strict'; + var u = i(93700); + s.exports = u; + }, + 85401: (s, o, i) => { + 'use strict'; + var u = i(462); + s.exports = u; + }, + 82159: (s, o, i) => { + 'use strict'; + var u = i(62250), + _ = i(4640), + w = TypeError; + s.exports = function (s) { + if (u(s)) return s; + throw new w(_(s) + ' is not a function'); + }; + }, + 10043: (s, o, i) => { + 'use strict'; + var u = i(54018), + _ = String, + w = TypeError; + s.exports = function (s) { + if (u(s)) return s; + throw new w("Can't set " + _(s) + ' as a prototype'); + }; + }, + 42156: (s) => { + 'use strict'; + s.exports = function () {}; + }, + 36624: (s, o, i) => { + 'use strict'; + var u = i(46285), + _ = String, + w = TypeError; + s.exports = function (s) { + if (u(s)) return s; + throw new w(_(s) + ' is not an object'); + }; + }, + 74436: (s, o, i) => { + 'use strict'; + var u = i(4993), + _ = i(34849), + w = i(20575), + createMethod = function (s) { + return function (o, i, x) { + var C = u(o), + j = w(C); + if (0 === j) return !s && -1; + var L, + B = _(x, j); + if (s && i != i) { + for (; j > B; ) if ((L = C[B++]) != L) return !0; + } else for (; j > B; B++) if ((s || B in C) && C[B] === i) return s || B || 0; + return !s && -1; + }; + }; + s.exports = { includes: createMethod(!0), indexOf: createMethod(!1) }; + }, + 93427: (s, o, i) => { + 'use strict'; + var u = i(1907); + s.exports = u([].slice); + }, + 45807: (s, o, i) => { + 'use strict'; + var u = i(1907), + _ = u({}.toString), + w = u(''.slice); + s.exports = function (s) { + return w(_(s), 8, -1); + }; + }, + 73948: (s, o, i) => { + 'use strict'; + var u = i(52623), + _ = i(62250), + w = i(45807), + x = i(76264)('toStringTag'), + C = Object, + j = + 'Arguments' === + w( + (function () { + return arguments; + })() + ); + s.exports = u + ? w + : function (s) { + var o, i, u; + return void 0 === s + ? 'Undefined' + : null === s + ? 'Null' + : 'string' == + typeof (i = (function (s, o) { + try { + return s[o]; + } catch (s) {} + })((o = C(s)), x)) + ? i + : j + ? w(o) + : 'Object' === (u = w(o)) && _(o.callee) + ? 'Arguments' + : u; + }; + }, + 19595: (s, o, i) => { + 'use strict'; + var u = i(49724), + _ = i(11042), + w = i(13846), + x = i(74284); + s.exports = function (s, o, i) { + for (var C = _(o), j = x.f, L = w.f, B = 0; B < C.length; B++) { + var $ = C[B]; + u(s, $) || (i && u(i, $)) || j(s, $, L(o, $)); + } + }; + }, + 57382: (s, o, i) => { + 'use strict'; + var u = i(98828); + s.exports = !u(function () { + function F() {} + return ( + (F.prototype.constructor = null), + Object.getPrototypeOf(new F()) !== F.prototype + ); + }); + }, + 59550: (s) => { + 'use strict'; + s.exports = function (s, o) { + return { value: s, done: o }; + }; + }, + 61626: (s, o, i) => { + 'use strict'; + var u = i(39447), + _ = i(74284), + w = i(75817); + s.exports = u + ? function (s, o, i) { + return _.f(s, o, w(1, i)); + } + : function (s, o, i) { + return ((s[o] = i), s); + }; + }, + 75817: (s) => { + 'use strict'; + s.exports = function (s, o) { + return { enumerable: !(1 & s), configurable: !(2 & s), writable: !(4 & s), value: o }; + }; + }, + 68055: (s, o, i) => { + 'use strict'; + var u = i(61626); + s.exports = function (s, o, i, _) { + return (_ && _.enumerable ? (s[o] = i) : u(s, o, i), s); + }; + }, + 2532: (s, o, i) => { + 'use strict'; + var u = i(45951), + _ = Object.defineProperty; + s.exports = function (s, o) { + try { + _(u, s, { value: o, configurable: !0, writable: !0 }); + } catch (i) { + u[s] = o; + } + return o; + }; + }, + 39447: (s, o, i) => { + 'use strict'; + var u = i(98828); + s.exports = !u(function () { + return ( + 7 !== + Object.defineProperty({}, 1, { + get: function () { + return 7; + } + })[1] + ); + }); + }, + 49552: (s, o, i) => { + 'use strict'; + var u = i(45951), + _ = i(46285), + w = u.document, + x = _(w) && _(w.createElement); + s.exports = function (s) { + return x ? w.createElement(s) : {}; + }; + }, + 19287: (s) => { + 'use strict'; + s.exports = { + CSSRuleList: 0, + CSSStyleDeclaration: 0, + CSSValueList: 0, + ClientRectList: 0, + DOMRectList: 0, + DOMStringList: 0, + DOMTokenList: 1, + DataTransferItemList: 0, + FileList: 0, + HTMLAllCollection: 0, + HTMLCollection: 0, + HTMLFormElement: 0, + HTMLSelectElement: 0, + MediaList: 0, + MimeTypeArray: 0, + NamedNodeMap: 0, + NodeList: 1, + PaintRequestList: 0, + Plugin: 0, + PluginArray: 0, + SVGLengthList: 0, + SVGNumberList: 0, + SVGPathSegList: 0, + SVGPointList: 0, + SVGStringList: 0, + SVGTransformList: 0, + SourceBufferList: 0, + StyleSheetList: 0, + TextTrackCueList: 0, + TextTrackList: 0, + TouchList: 0 + }; + }, + 80376: (s) => { + 'use strict'; + s.exports = [ + 'constructor', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'toLocaleString', + 'toString', + 'valueOf' + ]; + }, + 96794: (s, o, i) => { + 'use strict'; + var u = i(45951).navigator, + _ = u && u.userAgent; + s.exports = _ ? String(_) : ''; + }, + 20798: (s, o, i) => { + 'use strict'; + var u, + _, + w = i(45951), + x = i(96794), + C = w.process, + j = w.Deno, + L = (C && C.versions) || (j && j.version), + B = L && L.v8; + (B && (_ = (u = B.split('.'))[0] > 0 && u[0] < 4 ? 1 : +(u[0] + u[1])), + !_ && + x && + (!(u = x.match(/Edge\/(\d+)/)) || u[1] >= 74) && + (u = x.match(/Chrome\/(\d+)/)) && + (_ = +u[1]), + (s.exports = _)); + }, + 85762: (s, o, i) => { + 'use strict'; + var u = i(1907), + _ = Error, + w = u(''.replace), + x = String(new _('zxcasd').stack), + C = /\n\s*at [^:]*:[^\n]*/, + j = C.test(x); + s.exports = function (s, o) { + if (j && 'string' == typeof s && !_.prepareStackTrace) for (; o--; ) s = w(s, C, ''); + return s; + }; + }, + 85884: (s, o, i) => { + 'use strict'; + var u = i(61626), + _ = i(85762), + w = i(23888), + x = Error.captureStackTrace; + s.exports = function (s, o, i, C) { + w && (x ? x(s, o) : u(s, 'stack', _(i, C))); + }; + }, + 23888: (s, o, i) => { + 'use strict'; + var u = i(98828), + _ = i(75817); + s.exports = !u(function () { + var s = new Error('a'); + return !('stack' in s) || (Object.defineProperty(s, 'stack', _(1, 7)), 7 !== s.stack); + }); + }, + 11091: (s, o, i) => { + 'use strict'; + var u = i(45951), + _ = i(76024), + w = i(92361), + x = i(62250), + C = i(13846).f, + j = i(7463), + L = i(92046), + B = i(28311), + $ = i(61626), + V = i(49724); + i(36128); + var wrapConstructor = function (s) { + var Wrapper = function (o, i, u) { + if (this instanceof Wrapper) { + switch (arguments.length) { + case 0: + return new s(); + case 1: + return new s(o); + case 2: + return new s(o, i); + } + return new s(o, i, u); + } + return _(s, this, arguments); + }; + return ((Wrapper.prototype = s.prototype), Wrapper); + }; + s.exports = function (s, o) { + var i, + _, + U, + z, + Y, + Z, + ee, + ie, + ae, + le = s.target, + ce = s.global, + pe = s.stat, + de = s.proto, + fe = ce ? u : pe ? u[le] : u[le] && u[le].prototype, + ye = ce ? L : L[le] || $(L, le, {})[le], + be = ye.prototype; + for (z in o) + ((_ = !(i = j(ce ? z : le + (pe ? '.' : '#') + z, s.forced)) && fe && V(fe, z)), + (Z = ye[z]), + _ && (ee = s.dontCallGetSet ? (ae = C(fe, z)) && ae.value : fe[z]), + (Y = _ && ee ? ee : o[z]), + (i || de || typeof Z != typeof Y) && + ((ie = + s.bind && _ + ? B(Y, u) + : s.wrap && _ + ? wrapConstructor(Y) + : de && x(Y) + ? w(Y) + : Y), + (s.sham || (Y && Y.sham) || (Z && Z.sham)) && $(ie, 'sham', !0), + $(ye, z, ie), + de && + (V(L, (U = le + 'Prototype')) || $(L, U, {}), + $(L[U], z, Y), + s.real && be && (i || !be[z]) && $(be, z, Y)))); + }; + }, + 98828: (s) => { + 'use strict'; + s.exports = function (s) { + try { + return !!s(); + } catch (s) { + return !0; + } + }; + }, + 76024: (s, o, i) => { + 'use strict'; + var u = i(41505), + _ = Function.prototype, + w = _.apply, + x = _.call; + s.exports = + ('object' == typeof Reflect && Reflect.apply) || + (u + ? x.bind(w) + : function () { + return x.apply(w, arguments); + }); + }, + 28311: (s, o, i) => { + 'use strict'; + var u = i(92361), + _ = i(82159), + w = i(41505), + x = u(u.bind); + s.exports = function (s, o) { + return ( + _(s), + void 0 === o + ? s + : w + ? x(s, o) + : function () { + return s.apply(o, arguments); + } + ); + }; + }, + 41505: (s, o, i) => { + 'use strict'; + var u = i(98828); + s.exports = !u(function () { + var s = function () {}.bind(); + return 'function' != typeof s || s.hasOwnProperty('prototype'); + }); + }, + 44673: (s, o, i) => { + 'use strict'; + var u = i(1907), + _ = i(82159), + w = i(46285), + x = i(49724), + C = i(93427), + j = i(41505), + L = Function, + B = u([].concat), + $ = u([].join), + V = {}; + s.exports = j + ? L.bind + : function bind(s) { + var o = _(this), + i = o.prototype, + u = C(arguments, 1), + j = function bound() { + var i = B(u, C(arguments)); + return this instanceof j + ? (function (s, o, i) { + if (!x(V, o)) { + for (var u = [], _ = 0; _ < o; _++) u[_] = 'a[' + _ + ']'; + V[o] = L('C,a', 'return new C(' + $(u, ',') + ')'); + } + return V[o](s, i); + })(o, i.length, i) + : o.apply(s, i); + }; + return (w(i) && (j.prototype = i), j); + }; + }, + 13930: (s, o, i) => { + 'use strict'; + var u = i(41505), + _ = Function.prototype.call; + s.exports = u + ? _.bind(_) + : function () { + return _.apply(_, arguments); + }; + }, + 36833: (s, o, i) => { + 'use strict'; + var u = i(39447), + _ = i(49724), + w = Function.prototype, + x = u && Object.getOwnPropertyDescriptor, + C = _(w, 'name'), + j = C && 'something' === function something() {}.name, + L = C && (!u || (u && x(w, 'name').configurable)); + s.exports = { EXISTS: C, PROPER: j, CONFIGURABLE: L }; + }, + 51871: (s, o, i) => { + 'use strict'; + var u = i(1907), + _ = i(82159); + s.exports = function (s, o, i) { + try { + return u(_(Object.getOwnPropertyDescriptor(s, o)[i])); + } catch (s) {} + }; + }, + 92361: (s, o, i) => { + 'use strict'; + var u = i(45807), + _ = i(1907); + s.exports = function (s) { + if ('Function' === u(s)) return _(s); + }; + }, + 1907: (s, o, i) => { + 'use strict'; + var u = i(41505), + _ = Function.prototype, + w = _.call, + x = u && _.bind.bind(w, w); + s.exports = u + ? x + : function (s) { + return function () { + return w.apply(s, arguments); + }; + }; + }, + 61747: (s, o, i) => { + 'use strict'; + var u = i(45951), + _ = i(92046); + s.exports = function (s, o) { + var i = _[s + 'Prototype'], + w = i && i[o]; + if (w) return w; + var x = u[s], + C = x && x.prototype; + return C && C[o]; + }; + }, + 85582: (s, o, i) => { + 'use strict'; + var u = i(92046), + _ = i(45951), + w = i(62250), + aFunction = function (s) { + return w(s) ? s : void 0; + }; + s.exports = function (s, o) { + return arguments.length < 2 + ? aFunction(u[s]) || aFunction(_[s]) + : (u[s] && u[s][o]) || (_[s] && _[s][o]); + }; + }, + 73448: (s, o, i) => { + 'use strict'; + var u = i(73948), + _ = i(29367), + w = i(87136), + x = i(93742), + C = i(76264)('iterator'); + s.exports = function (s) { + if (!w(s)) return _(s, C) || _(s, '@@iterator') || x[u(s)]; + }; + }, + 10300: (s, o, i) => { + 'use strict'; + var u = i(13930), + _ = i(82159), + w = i(36624), + x = i(4640), + C = i(73448), + j = TypeError; + s.exports = function (s, o) { + var i = arguments.length < 2 ? C(s) : o; + if (_(i)) return w(u(i, s)); + throw new j(x(s) + ' is not iterable'); + }; + }, + 29367: (s, o, i) => { + 'use strict'; + var u = i(82159), + _ = i(87136); + s.exports = function (s, o) { + var i = s[o]; + return _(i) ? void 0 : u(i); + }; + }, + 45951: function (s, o, i) { + 'use strict'; + var check = function (s) { + return s && s.Math === Math && s; + }; + s.exports = + check('object' == typeof globalThis && globalThis) || + check('object' == typeof window && window) || + check('object' == typeof self && self) || + check('object' == typeof i.g && i.g) || + check('object' == typeof this && this) || + (function () { + return this; + })() || + Function('return this')(); + }, + 49724: (s, o, i) => { + 'use strict'; + var u = i(1907), + _ = i(39298), + w = u({}.hasOwnProperty); + s.exports = + Object.hasOwn || + function hasOwn(s, o) { + return w(_(s), o); + }; + }, + 38530: (s) => { + 'use strict'; + s.exports = {}; + }, + 62416: (s, o, i) => { + 'use strict'; + var u = i(85582); + s.exports = u('document', 'documentElement'); + }, + 73648: (s, o, i) => { + 'use strict'; + var u = i(39447), + _ = i(98828), + w = i(49552); + s.exports = + !u && + !_(function () { + return ( + 7 !== + Object.defineProperty(w('div'), 'a', { + get: function () { + return 7; + } + }).a + ); + }); + }, + 16946: (s, o, i) => { + 'use strict'; + var u = i(1907), + _ = i(98828), + w = i(45807), + x = Object, + C = u(''.split); + s.exports = _(function () { + return !x('z').propertyIsEnumerable(0); + }) + ? function (s) { + return 'String' === w(s) ? C(s, '') : x(s); + } + : x; + }, + 34084: (s, o, i) => { + 'use strict'; + var u = i(62250), + _ = i(46285), + w = i(79192); + s.exports = function (s, o, i) { + var x, C; + return ( + w && + u((x = o.constructor)) && + x !== i && + _((C = x.prototype)) && + C !== i.prototype && + w(s, C), + s + ); + }; + }, + 39259: (s, o, i) => { + 'use strict'; + var u = i(46285), + _ = i(61626); + s.exports = function (s, o) { + u(o) && 'cause' in o && _(s, 'cause', o.cause); + }; + }, + 64932: (s, o, i) => { + 'use strict'; + var u, + _, + w, + x = i(40551), + C = i(45951), + j = i(46285), + L = i(61626), + B = i(49724), + $ = i(36128), + V = i(92522), + U = i(38530), + z = 'Object already initialized', + Y = C.TypeError, + Z = C.WeakMap; + if (x || $.state) { + var ee = $.state || ($.state = new Z()); + ((ee.get = ee.get), + (ee.has = ee.has), + (ee.set = ee.set), + (u = function (s, o) { + if (ee.has(s)) throw new Y(z); + return ((o.facade = s), ee.set(s, o), o); + }), + (_ = function (s) { + return ee.get(s) || {}; + }), + (w = function (s) { + return ee.has(s); + })); + } else { + var ie = V('state'); + ((U[ie] = !0), + (u = function (s, o) { + if (B(s, ie)) throw new Y(z); + return ((o.facade = s), L(s, ie, o), o); + }), + (_ = function (s) { + return B(s, ie) ? s[ie] : {}; + }), + (w = function (s) { + return B(s, ie); + })); + } + s.exports = { + set: u, + get: _, + has: w, + enforce: function (s) { + return w(s) ? _(s) : u(s, {}); + }, + getterFor: function (s) { + return function (o) { + var i; + if (!j(o) || (i = _(o)).type !== s) + throw new Y('Incompatible receiver, ' + s + ' required'); + return i; + }; + } + }; + }, + 37812: (s, o, i) => { + 'use strict'; + var u = i(76264), + _ = i(93742), + w = u('iterator'), + x = Array.prototype; + s.exports = function (s) { + return void 0 !== s && (_.Array === s || x[w] === s); + }; + }, + 62250: (s) => { + 'use strict'; + var o = 'object' == typeof document && document.all; + s.exports = + void 0 === o && void 0 !== o + ? function (s) { + return 'function' == typeof s || s === o; + } + : function (s) { + return 'function' == typeof s; + }; + }, + 7463: (s, o, i) => { + 'use strict'; + var u = i(98828), + _ = i(62250), + w = /#|\.prototype\./, + isForced = function (s, o) { + var i = C[x(s)]; + return i === L || (i !== j && (_(o) ? u(o) : !!o)); + }, + x = (isForced.normalize = function (s) { + return String(s).replace(w, '.').toLowerCase(); + }), + C = (isForced.data = {}), + j = (isForced.NATIVE = 'N'), + L = (isForced.POLYFILL = 'P'); + s.exports = isForced; + }, + 87136: (s) => { + 'use strict'; + s.exports = function (s) { + return null == s; + }; + }, + 46285: (s, o, i) => { + 'use strict'; + var u = i(62250); + s.exports = function (s) { + return 'object' == typeof s ? null !== s : u(s); + }; + }, + 54018: (s, o, i) => { + 'use strict'; + var u = i(46285); + s.exports = function (s) { + return u(s) || null === s; + }; + }, + 7376: (s) => { + 'use strict'; + s.exports = !0; + }, + 25594: (s, o, i) => { + 'use strict'; + var u = i(85582), + _ = i(62250), + w = i(88280), + x = i(51175), + C = Object; + s.exports = x + ? function (s) { + return 'symbol' == typeof s; + } + : function (s) { + var o = u('Symbol'); + return _(o) && w(o.prototype, C(s)); + }; + }, + 24823: (s, o, i) => { + 'use strict'; + var u = i(28311), + _ = i(13930), + w = i(36624), + x = i(4640), + C = i(37812), + j = i(20575), + L = i(88280), + B = i(10300), + $ = i(73448), + V = i(40154), + U = TypeError, + Result = function (s, o) { + ((this.stopped = s), (this.result = o)); + }, + z = Result.prototype; + s.exports = function (s, o, i) { + var Y, + Z, + ee, + ie, + ae, + le, + ce, + pe = i && i.that, + de = !(!i || !i.AS_ENTRIES), + fe = !(!i || !i.IS_RECORD), + ye = !(!i || !i.IS_ITERATOR), + be = !(!i || !i.INTERRUPTED), + _e = u(o, pe), + stop = function (s) { + return (Y && V(Y, 'normal', s), new Result(!0, s)); + }, + callFn = function (s) { + return de + ? (w(s), be ? _e(s[0], s[1], stop) : _e(s[0], s[1])) + : be + ? _e(s, stop) + : _e(s); + }; + if (fe) Y = s.iterator; + else if (ye) Y = s; + else { + if (!(Z = $(s))) throw new U(x(s) + ' is not iterable'); + if (C(Z)) { + for (ee = 0, ie = j(s); ie > ee; ee++) + if ((ae = callFn(s[ee])) && L(z, ae)) return ae; + return new Result(!1); + } + Y = B(s, Z); + } + for (le = fe ? s.next : Y.next; !(ce = _(le, Y)).done; ) { + try { + ae = callFn(ce.value); + } catch (s) { + V(Y, 'throw', s); + } + if ('object' == typeof ae && ae && L(z, ae)) return ae; + } + return new Result(!1); + }; + }, + 40154: (s, o, i) => { + 'use strict'; + var u = i(13930), + _ = i(36624), + w = i(29367); + s.exports = function (s, o, i) { + var x, C; + _(s); + try { + if (!(x = w(s, 'return'))) { + if ('throw' === o) throw i; + return i; + } + x = u(x, s); + } catch (s) { + ((C = !0), (x = s)); + } + if ('throw' === o) throw i; + if (C) throw x; + return (_(x), i); + }; + }, + 47181: (s, o, i) => { + 'use strict'; + var u = i(95116).IteratorPrototype, + _ = i(58075), + w = i(75817), + x = i(14840), + C = i(93742), + returnThis = function () { + return this; + }; + s.exports = function (s, o, i, j) { + var L = o + ' Iterator'; + return ( + (s.prototype = _(u, { next: w(+!j, i) })), + x(s, L, !1, !0), + (C[L] = returnThis), + s + ); + }; + }, + 60183: (s, o, i) => { + 'use strict'; + var u = i(11091), + _ = i(13930), + w = i(7376), + x = i(36833), + C = i(62250), + j = i(47181), + L = i(15972), + B = i(79192), + $ = i(14840), + V = i(61626), + U = i(68055), + z = i(76264), + Y = i(93742), + Z = i(95116), + ee = x.PROPER, + ie = x.CONFIGURABLE, + ae = Z.IteratorPrototype, + le = Z.BUGGY_SAFARI_ITERATORS, + ce = z('iterator'), + pe = 'keys', + de = 'values', + fe = 'entries', + returnThis = function () { + return this; + }; + s.exports = function (s, o, i, x, z, Z, ye) { + j(i, o, x); + var be, + _e, + we, + getIterationMethod = function (s) { + if (s === z && Re) return Re; + if (!le && s && s in Pe) return Pe[s]; + switch (s) { + case pe: + return function keys() { + return new i(this, s); + }; + case de: + return function values() { + return new i(this, s); + }; + case fe: + return function entries() { + return new i(this, s); + }; + } + return function () { + return new i(this); + }; + }, + Se = o + ' Iterator', + xe = !1, + Pe = s.prototype, + Te = Pe[ce] || Pe['@@iterator'] || (z && Pe[z]), + Re = (!le && Te) || getIterationMethod(z), + qe = ('Array' === o && Pe.entries) || Te; + if ( + (qe && + (be = L(qe.call(new s()))) !== Object.prototype && + be.next && + (w || L(be) === ae || (B ? B(be, ae) : C(be[ce]) || U(be, ce, returnThis)), + $(be, Se, !0, !0), + w && (Y[Se] = returnThis)), + ee && + z === de && + Te && + Te.name !== de && + (!w && ie + ? V(Pe, 'name', de) + : ((xe = !0), + (Re = function values() { + return _(Te, this); + }))), + z) + ) + if ( + ((_e = { + values: getIterationMethod(de), + keys: Z ? Re : getIterationMethod(pe), + entries: getIterationMethod(fe) + }), + ye) + ) + for (we in _e) (le || xe || !(we in Pe)) && U(Pe, we, _e[we]); + else u({ target: o, proto: !0, forced: le || xe }, _e); + return ((w && !ye) || Pe[ce] === Re || U(Pe, ce, Re, { name: z }), (Y[o] = Re), _e); + }; + }, + 95116: (s, o, i) => { + 'use strict'; + var u, + _, + w, + x = i(98828), + C = i(62250), + j = i(46285), + L = i(58075), + B = i(15972), + $ = i(68055), + V = i(76264), + U = i(7376), + z = V('iterator'), + Y = !1; + ([].keys && + ('next' in (w = [].keys()) ? (_ = B(B(w))) !== Object.prototype && (u = _) : (Y = !0)), + !j(u) || + x(function () { + var s = {}; + return u[z].call(s) !== s; + }) + ? (u = {}) + : U && (u = L(u)), + C(u[z]) || + $(u, z, function () { + return this; + }), + (s.exports = { IteratorPrototype: u, BUGGY_SAFARI_ITERATORS: Y })); + }, + 93742: (s) => { + 'use strict'; + s.exports = {}; + }, + 20575: (s, o, i) => { + 'use strict'; + var u = i(3121); + s.exports = function (s) { + return u(s.length); + }; + }, + 41176: (s) => { + 'use strict'; + var o = Math.ceil, + i = Math.floor; + s.exports = + Math.trunc || + function trunc(s) { + var u = +s; + return (u > 0 ? i : o)(u); + }; + }, + 32096: (s, o, i) => { + 'use strict'; + var u = i(90160); + s.exports = function (s, o) { + return void 0 === s ? (arguments.length < 2 ? '' : o) : u(s); + }; + }, + 29538: (s, o, i) => { + 'use strict'; + var u = i(39447), + _ = i(1907), + w = i(13930), + x = i(98828), + C = i(2875), + j = i(87170), + L = i(22574), + B = i(39298), + $ = i(16946), + V = Object.assign, + U = Object.defineProperty, + z = _([].concat); + s.exports = + !V || + x(function () { + if ( + u && + 1 !== + V( + { b: 1 }, + V( + U({}, 'a', { + enumerable: !0, + get: function () { + U(this, 'b', { value: 3, enumerable: !1 }); + } + }), + { b: 2 } + ) + ).b + ) + return !0; + var s = {}, + o = {}, + i = Symbol('assign detection'), + _ = 'abcdefghijklmnopqrst'; + return ( + (s[i] = 7), + _.split('').forEach(function (s) { + o[s] = s; + }), + 7 !== V({}, s)[i] || C(V({}, o)).join('') !== _ + ); + }) + ? function assign(s, o) { + for (var i = B(s), _ = arguments.length, x = 1, V = j.f, U = L.f; _ > x; ) + for ( + var Y, + Z = $(arguments[x++]), + ee = V ? z(C(Z), V(Z)) : C(Z), + ie = ee.length, + ae = 0; + ie > ae; + ) + ((Y = ee[ae++]), (u && !w(U, Z, Y)) || (i[Y] = Z[Y])); + return i; + } + : V; + }, + 58075: (s, o, i) => { + 'use strict'; + var u, + _ = i(36624), + w = i(42220), + x = i(80376), + C = i(38530), + j = i(62416), + L = i(49552), + B = i(92522), + $ = 'prototype', + V = 'script', + U = B('IE_PROTO'), + EmptyConstructor = function () {}, + scriptTag = function (s) { + return '<' + V + '>' + s + ''; + }, + NullProtoObjectViaActiveX = function (s) { + (s.write(scriptTag('')), s.close()); + var o = s.parentWindow.Object; + return ((s = null), o); + }, + NullProtoObject = function () { + try { + u = new ActiveXObject('htmlfile'); + } catch (s) {} + var s, o, i; + NullProtoObject = + 'undefined' != typeof document + ? document.domain && u + ? NullProtoObjectViaActiveX(u) + : ((o = L('iframe')), + (i = 'java' + V + ':'), + (o.style.display = 'none'), + j.appendChild(o), + (o.src = String(i)), + (s = o.contentWindow.document).open(), + s.write(scriptTag('document.F=Object')), + s.close(), + s.F) + : NullProtoObjectViaActiveX(u); + for (var _ = x.length; _--; ) delete NullProtoObject[$][x[_]]; + return NullProtoObject(); + }; + ((C[U] = !0), + (s.exports = + Object.create || + function create(s, o) { + var i; + return ( + null !== s + ? ((EmptyConstructor[$] = _(s)), + (i = new EmptyConstructor()), + (EmptyConstructor[$] = null), + (i[U] = s)) + : (i = NullProtoObject()), + void 0 === o ? i : w.f(i, o) + ); + })); + }, + 42220: (s, o, i) => { + 'use strict'; + var u = i(39447), + _ = i(58661), + w = i(74284), + x = i(36624), + C = i(4993), + j = i(2875); + o.f = + u && !_ + ? Object.defineProperties + : function defineProperties(s, o) { + x(s); + for (var i, u = C(o), _ = j(o), L = _.length, B = 0; L > B; ) + w.f(s, (i = _[B++]), u[i]); + return s; + }; + }, + 74284: (s, o, i) => { + 'use strict'; + var u = i(39447), + _ = i(73648), + w = i(58661), + x = i(36624), + C = i(70470), + j = TypeError, + L = Object.defineProperty, + B = Object.getOwnPropertyDescriptor, + $ = 'enumerable', + V = 'configurable', + U = 'writable'; + o.f = u + ? w + ? function defineProperty(s, o, i) { + if ( + (x(s), + (o = C(o)), + x(i), + 'function' == typeof s && 'prototype' === o && 'value' in i && U in i && !i[U]) + ) { + var u = B(s, o); + u && + u[U] && + ((s[o] = i.value), + (i = { + configurable: V in i ? i[V] : u[V], + enumerable: $ in i ? i[$] : u[$], + writable: !1 + })); + } + return L(s, o, i); + } + : L + : function defineProperty(s, o, i) { + if ((x(s), (o = C(o)), x(i), _)) + try { + return L(s, o, i); + } catch (s) {} + if ('get' in i || 'set' in i) throw new j('Accessors not supported'); + return ('value' in i && (s[o] = i.value), s); + }; + }, + 13846: (s, o, i) => { + 'use strict'; + var u = i(39447), + _ = i(13930), + w = i(22574), + x = i(75817), + C = i(4993), + j = i(70470), + L = i(49724), + B = i(73648), + $ = Object.getOwnPropertyDescriptor; + o.f = u + ? $ + : function getOwnPropertyDescriptor(s, o) { + if (((s = C(s)), (o = j(o)), B)) + try { + return $(s, o); + } catch (s) {} + if (L(s, o)) return x(!_(w.f, s, o), s[o]); + }; + }, + 24443: (s, o, i) => { + 'use strict'; + var u = i(23045), + _ = i(80376).concat('length', 'prototype'); + o.f = + Object.getOwnPropertyNames || + function getOwnPropertyNames(s) { + return u(s, _); + }; + }, + 87170: (s, o) => { + 'use strict'; + o.f = Object.getOwnPropertySymbols; + }, + 15972: (s, o, i) => { + 'use strict'; + var u = i(49724), + _ = i(62250), + w = i(39298), + x = i(92522), + C = i(57382), + j = x('IE_PROTO'), + L = Object, + B = L.prototype; + s.exports = C + ? L.getPrototypeOf + : function (s) { + var o = w(s); + if (u(o, j)) return o[j]; + var i = o.constructor; + return _(i) && o instanceof i ? i.prototype : o instanceof L ? B : null; + }; + }, + 88280: (s, o, i) => { + 'use strict'; + var u = i(1907); + s.exports = u({}.isPrototypeOf); + }, + 23045: (s, o, i) => { + 'use strict'; + var u = i(1907), + _ = i(49724), + w = i(4993), + x = i(74436).indexOf, + C = i(38530), + j = u([].push); + s.exports = function (s, o) { + var i, + u = w(s), + L = 0, + B = []; + for (i in u) !_(C, i) && _(u, i) && j(B, i); + for (; o.length > L; ) _(u, (i = o[L++])) && (~x(B, i) || j(B, i)); + return B; + }; + }, + 2875: (s, o, i) => { + 'use strict'; + var u = i(23045), + _ = i(80376); + s.exports = + Object.keys || + function keys(s) { + return u(s, _); + }; + }, + 22574: (s, o) => { + 'use strict'; + var i = {}.propertyIsEnumerable, + u = Object.getOwnPropertyDescriptor, + _ = u && !i.call({ 1: 2 }, 1); + o.f = _ + ? function propertyIsEnumerable(s) { + var o = u(this, s); + return !!o && o.enumerable; + } + : i; + }, + 79192: (s, o, i) => { + 'use strict'; + var u = i(51871), + _ = i(46285), + w = i(74239), + x = i(10043); + s.exports = + Object.setPrototypeOf || + ('__proto__' in {} + ? (function () { + var s, + o = !1, + i = {}; + try { + ((s = u(Object.prototype, '__proto__', 'set'))(i, []), + (o = i instanceof Array)); + } catch (s) {} + return function setPrototypeOf(i, u) { + return (w(i), x(u), _(i) ? (o ? s(i, u) : (i.__proto__ = u), i) : i); + }; + })() + : void 0); + }, + 54878: (s, o, i) => { + 'use strict'; + var u = i(52623), + _ = i(73948); + s.exports = u + ? {}.toString + : function toString() { + return '[object ' + _(this) + ']'; + }; + }, + 60581: (s, o, i) => { + 'use strict'; + var u = i(13930), + _ = i(62250), + w = i(46285), + x = TypeError; + s.exports = function (s, o) { + var i, C; + if ('string' === o && _((i = s.toString)) && !w((C = u(i, s)))) return C; + if (_((i = s.valueOf)) && !w((C = u(i, s)))) return C; + if ('string' !== o && _((i = s.toString)) && !w((C = u(i, s)))) return C; + throw new x("Can't convert object to primitive value"); + }; + }, + 11042: (s, o, i) => { + 'use strict'; + var u = i(85582), + _ = i(1907), + w = i(24443), + x = i(87170), + C = i(36624), + j = _([].concat); + s.exports = + u('Reflect', 'ownKeys') || + function ownKeys(s) { + var o = w.f(C(s)), + i = x.f; + return i ? j(o, i(s)) : o; + }; + }, + 92046: (s) => { + 'use strict'; + s.exports = {}; + }, + 54829: (s, o, i) => { + 'use strict'; + var u = i(74284).f; + s.exports = function (s, o, i) { + i in s || + u(s, i, { + configurable: !0, + get: function () { + return o[i]; + }, + set: function (s) { + o[i] = s; + } + }); + }; + }, + 74239: (s, o, i) => { + 'use strict'; + var u = i(87136), + _ = TypeError; + s.exports = function (s) { + if (u(s)) throw new _("Can't call method on " + s); + return s; + }; + }, + 14840: (s, o, i) => { + 'use strict'; + var u = i(52623), + _ = i(74284).f, + w = i(61626), + x = i(49724), + C = i(54878), + j = i(76264)('toStringTag'); + s.exports = function (s, o, i, L) { + var B = i ? s : s && s.prototype; + B && + (x(B, j) || _(B, j, { configurable: !0, value: o }), L && !u && w(B, 'toString', C)); + }; + }, + 92522: (s, o, i) => { + 'use strict'; + var u = i(85816), + _ = i(6499), + w = u('keys'); + s.exports = function (s) { + return w[s] || (w[s] = _(s)); + }; + }, + 36128: (s, o, i) => { + 'use strict'; + var u = i(7376), + _ = i(45951), + w = i(2532), + x = '__core-js_shared__', + C = (s.exports = _[x] || w(x, {})); + (C.versions || (C.versions = [])).push({ + version: '3.39.0', + mode: u ? 'pure' : 'global', + copyright: '© 2014-2024 Denis Pushkarev (zloirock.ru)', + license: 'https://github.com/zloirock/core-js/blob/v3.39.0/LICENSE', + source: 'https://github.com/zloirock/core-js' + }); + }, + 85816: (s, o, i) => { + 'use strict'; + var u = i(36128); + s.exports = function (s, o) { + return u[s] || (u[s] = o || {}); + }; + }, + 11470: (s, o, i) => { + 'use strict'; + var u = i(1907), + _ = i(65482), + w = i(90160), + x = i(74239), + C = u(''.charAt), + j = u(''.charCodeAt), + L = u(''.slice), + createMethod = function (s) { + return function (o, i) { + var u, + B, + $ = w(x(o)), + V = _(i), + U = $.length; + return V < 0 || V >= U + ? s + ? '' + : void 0 + : (u = j($, V)) < 55296 || + u > 56319 || + V + 1 === U || + (B = j($, V + 1)) < 56320 || + B > 57343 + ? s + ? C($, V) + : u + : s + ? L($, V, V + 2) + : B - 56320 + ((u - 55296) << 10) + 65536; + }; + }; + s.exports = { codeAt: createMethod(!1), charAt: createMethod(!0) }; + }, + 19846: (s, o, i) => { + 'use strict'; + var u = i(20798), + _ = i(98828), + w = i(45951).String; + s.exports = + !!Object.getOwnPropertySymbols && + !_(function () { + var s = Symbol('symbol detection'); + return !w(s) || !(Object(s) instanceof Symbol) || (!Symbol.sham && u && u < 41); + }); + }, + 34849: (s, o, i) => { + 'use strict'; + var u = i(65482), + _ = Math.max, + w = Math.min; + s.exports = function (s, o) { + var i = u(s); + return i < 0 ? _(i + o, 0) : w(i, o); + }; + }, + 4993: (s, o, i) => { + 'use strict'; + var u = i(16946), + _ = i(74239); + s.exports = function (s) { + return u(_(s)); + }; + }, + 65482: (s, o, i) => { + 'use strict'; + var u = i(41176); + s.exports = function (s) { + var o = +s; + return o != o || 0 === o ? 0 : u(o); + }; + }, + 3121: (s, o, i) => { + 'use strict'; + var u = i(65482), + _ = Math.min; + s.exports = function (s) { + var o = u(s); + return o > 0 ? _(o, 9007199254740991) : 0; + }; + }, + 39298: (s, o, i) => { + 'use strict'; + var u = i(74239), + _ = Object; + s.exports = function (s) { + return _(u(s)); + }; + }, + 46028: (s, o, i) => { + 'use strict'; + var u = i(13930), + _ = i(46285), + w = i(25594), + x = i(29367), + C = i(60581), + j = i(76264), + L = TypeError, + B = j('toPrimitive'); + s.exports = function (s, o) { + if (!_(s) || w(s)) return s; + var i, + j = x(s, B); + if (j) { + if ((void 0 === o && (o = 'default'), (i = u(j, s, o)), !_(i) || w(i))) return i; + throw new L("Can't convert object to primitive value"); + } + return (void 0 === o && (o = 'number'), C(s, o)); + }; + }, + 70470: (s, o, i) => { + 'use strict'; + var u = i(46028), + _ = i(25594); + s.exports = function (s) { + var o = u(s, 'string'); + return _(o) ? o : o + ''; + }; + }, + 52623: (s, o, i) => { + 'use strict'; + var u = {}; + ((u[i(76264)('toStringTag')] = 'z'), (s.exports = '[object z]' === String(u))); + }, + 90160: (s, o, i) => { + 'use strict'; + var u = i(73948), + _ = String; + s.exports = function (s) { + if ('Symbol' === u(s)) throw new TypeError('Cannot convert a Symbol value to a string'); + return _(s); + }; + }, + 4640: (s) => { + 'use strict'; + var o = String; + s.exports = function (s) { + try { + return o(s); + } catch (s) { + return 'Object'; + } + }; + }, + 6499: (s, o, i) => { + 'use strict'; + var u = i(1907), + _ = 0, + w = Math.random(), + x = u((1).toString); + s.exports = function (s) { + return 'Symbol(' + (void 0 === s ? '' : s) + ')_' + x(++_ + w, 36); + }; + }, + 51175: (s, o, i) => { + 'use strict'; + var u = i(19846); + s.exports = u && !Symbol.sham && 'symbol' == typeof Symbol.iterator; + }, + 58661: (s, o, i) => { + 'use strict'; + var u = i(39447), + _ = i(98828); + s.exports = + u && + _(function () { + return ( + 42 !== + Object.defineProperty(function () {}, 'prototype', { value: 42, writable: !1 }) + .prototype + ); + }); + }, + 40551: (s, o, i) => { + 'use strict'; + var u = i(45951), + _ = i(62250), + w = u.WeakMap; + s.exports = _(w) && /native code/.test(String(w)); + }, + 76264: (s, o, i) => { + 'use strict'; + var u = i(45951), + _ = i(85816), + w = i(49724), + x = i(6499), + C = i(19846), + j = i(51175), + L = u.Symbol, + B = _('wks'), + $ = j ? L.for || L : (L && L.withoutSetter) || x; + s.exports = function (s) { + return (w(B, s) || (B[s] = C && w(L, s) ? L[s] : $('Symbol.' + s)), B[s]); + }; + }, + 19358: (s, o, i) => { + 'use strict'; + var u = i(85582), + _ = i(49724), + w = i(61626), + x = i(88280), + C = i(79192), + j = i(19595), + L = i(54829), + B = i(34084), + $ = i(32096), + V = i(39259), + U = i(85884), + z = i(39447), + Y = i(7376); + s.exports = function (s, o, i, Z) { + var ee = 'stackTraceLimit', + ie = Z ? 2 : 1, + ae = s.split('.'), + le = ae[ae.length - 1], + ce = u.apply(null, ae); + if (ce) { + var pe = ce.prototype; + if ((!Y && _(pe, 'cause') && delete pe.cause, !i)) return ce; + var de = u('Error'), + fe = o(function (s, o) { + var i = $(Z ? o : s, void 0), + u = Z ? new ce(s) : new ce(); + return ( + void 0 !== i && w(u, 'message', i), + U(u, fe, u.stack, 2), + this && x(pe, this) && B(u, this, fe), + arguments.length > ie && V(u, arguments[ie]), + u + ); + }); + if ( + ((fe.prototype = pe), + 'Error' !== le + ? C + ? C(fe, de) + : j(fe, de, { name: !0 }) + : z && ee in ce && (L(fe, ce, ee), L(fe, ce, 'prepareStackTrace')), + j(fe, ce), + !Y) + ) + try { + (pe.name !== le && w(pe, 'name', le), (pe.constructor = fe)); + } catch (s) {} + return fe; + } + }; + }, + 36371: (s, o, i) => { + 'use strict'; + var u = i(11091), + _ = i(85582), + w = i(76024), + x = i(98828), + C = i(19358), + j = 'AggregateError', + L = _(j), + B = + !x(function () { + return 1 !== L([1]).errors[0]; + }) && + x(function () { + return 7 !== L([1], j, { cause: 7 }).cause; + }); + u( + { global: !0, constructor: !0, arity: 2, forced: B }, + { + AggregateError: C( + j, + function (s) { + return function AggregateError(o, i) { + return w(s, this, arguments); + }; + }, + B, + !0 + ) + } + ); + }, + 82048: (s, o, i) => { + 'use strict'; + var u = i(11091), + _ = i(88280), + w = i(15972), + x = i(79192), + C = i(19595), + j = i(58075), + L = i(61626), + B = i(75817), + $ = i(39259), + V = i(85884), + U = i(24823), + z = i(32096), + Y = i(76264)('toStringTag'), + Z = Error, + ee = [].push, + ie = function AggregateError(s, o) { + var i, + u = _(ae, this); + (x ? (i = x(new Z(), u ? w(this) : ae)) : ((i = u ? this : j(ae)), L(i, Y, 'Error')), + void 0 !== o && L(i, 'message', z(o)), + V(i, ie, i.stack, 1), + arguments.length > 2 && $(i, arguments[2])); + var C = []; + return (U(s, ee, { that: C }), L(i, 'errors', C), i); + }; + x ? x(ie, Z) : C(ie, Z, { name: !0 }); + var ae = (ie.prototype = j(Z.prototype, { + constructor: B(1, ie), + message: B(1, ''), + name: B(1, 'AggregateError') + })); + u({ global: !0, constructor: !0, arity: 2 }, { AggregateError: ie }); + }, + 64502: (s, o, i) => { + 'use strict'; + i(82048); + }, + 99363: (s, o, i) => { + 'use strict'; + var u = i(4993), + _ = i(42156), + w = i(93742), + x = i(64932), + C = i(74284).f, + j = i(60183), + L = i(59550), + B = i(7376), + $ = i(39447), + V = 'Array Iterator', + U = x.set, + z = x.getterFor(V); + s.exports = j( + Array, + 'Array', + function (s, o) { + U(this, { type: V, target: u(s), index: 0, kind: o }); + }, + function () { + var s = z(this), + o = s.target, + i = s.index++; + if (!o || i >= o.length) return ((s.target = null), L(void 0, !0)); + switch (s.kind) { + case 'keys': + return L(i, !1); + case 'values': + return L(o[i], !1); + } + return L([i, o[i]], !1); + }, + 'values' + ); + var Y = (w.Arguments = w.Array); + if ((_('keys'), _('values'), _('entries'), !B && $ && 'values' !== Y.name)) + try { + C(Y, 'name', { value: 'values' }); + } catch (s) {} + }, + 96605: (s, o, i) => { + 'use strict'; + var u = i(11091), + _ = i(45951), + w = i(76024), + x = i(19358), + C = 'WebAssembly', + j = _[C], + L = 7 !== new Error('e', { cause: 7 }).cause, + exportGlobalErrorCauseWrapper = function (s, o) { + var i = {}; + ((i[s] = x(s, o, L)), u({ global: !0, constructor: !0, arity: 1, forced: L }, i)); + }, + exportWebAssemblyErrorCauseWrapper = function (s, o) { + if (j && j[s]) { + var i = {}; + ((i[s] = x(C + '.' + s, o, L)), + u({ target: C, stat: !0, constructor: !0, arity: 1, forced: L }, i)); + } + }; + (exportGlobalErrorCauseWrapper('Error', function (s) { + return function Error(o) { + return w(s, this, arguments); + }; + }), + exportGlobalErrorCauseWrapper('EvalError', function (s) { + return function EvalError(o) { + return w(s, this, arguments); + }; + }), + exportGlobalErrorCauseWrapper('RangeError', function (s) { + return function RangeError(o) { + return w(s, this, arguments); + }; + }), + exportGlobalErrorCauseWrapper('ReferenceError', function (s) { + return function ReferenceError(o) { + return w(s, this, arguments); + }; + }), + exportGlobalErrorCauseWrapper('SyntaxError', function (s) { + return function SyntaxError(o) { + return w(s, this, arguments); + }; + }), + exportGlobalErrorCauseWrapper('TypeError', function (s) { + return function TypeError(o) { + return w(s, this, arguments); + }; + }), + exportGlobalErrorCauseWrapper('URIError', function (s) { + return function URIError(o) { + return w(s, this, arguments); + }; + }), + exportWebAssemblyErrorCauseWrapper('CompileError', function (s) { + return function CompileError(o) { + return w(s, this, arguments); + }; + }), + exportWebAssemblyErrorCauseWrapper('LinkError', function (s) { + return function LinkError(o) { + return w(s, this, arguments); + }; + }), + exportWebAssemblyErrorCauseWrapper('RuntimeError', function (s) { + return function RuntimeError(o) { + return w(s, this, arguments); + }; + })); + }, + 79307: (s, o, i) => { + 'use strict'; + var u = i(11091), + _ = i(44673); + u({ target: 'Function', proto: !0, forced: Function.bind !== _ }, { bind: _ }); + }, + 71340: (s, o, i) => { + 'use strict'; + var u = i(11091), + _ = i(29538); + u({ target: 'Object', stat: !0, arity: 2, forced: Object.assign !== _ }, { assign: _ }); + }, + 7057: (s, o, i) => { + 'use strict'; + var u = i(11470).charAt, + _ = i(90160), + w = i(64932), + x = i(60183), + C = i(59550), + j = 'String Iterator', + L = w.set, + B = w.getterFor(j); + x( + String, + 'String', + function (s) { + L(this, { type: j, string: _(s), index: 0 }); + }, + function next() { + var s, + o = B(this), + i = o.string, + _ = o.index; + return _ >= i.length + ? C(void 0, !0) + : ((s = u(i, _)), (o.index += s.length), C(s, !1)); + } + ); + }, + 91599: (s, o, i) => { + 'use strict'; + i(64502); + }, + 12560: (s, o, i) => { + 'use strict'; + i(99363); + var u = i(19287), + _ = i(45951), + w = i(14840), + x = i(93742); + for (var C in u) (w(_[C], C), (x[C] = x.Array)); + }, + 694: (s, o, i) => { + 'use strict'; + i(91599); + var u = i(37257); + (i(12560), (s.exports = u)); + }, + 19709: (s, o, i) => { + 'use strict'; + var u = i(23034); + s.exports = u; + }, + 40975: (s, o, i) => { + 'use strict'; + var u = i(9748); + s.exports = u; + } + }, + u = {}; + function __webpack_require__(s) { + var o = u[s]; + if (void 0 !== o) return o.exports; + var _ = (u[s] = { id: s, loaded: !1, exports: {} }); + return (i[s].call(_.exports, _, _.exports, __webpack_require__), (_.loaded = !0), _.exports); + } + ((__webpack_require__.n = (s) => { + var o = s && s.__esModule ? () => s.default : () => s; + return (__webpack_require__.d(o, { a: o }), o); + }), + (o = Object.getPrototypeOf ? (s) => Object.getPrototypeOf(s) : (s) => s.__proto__), + (__webpack_require__.t = function (i, u) { + if ((1 & u && (i = this(i)), 8 & u)) return i; + if ('object' == typeof i && i) { + if (4 & u && i.__esModule) return i; + if (16 & u && 'function' == typeof i.then) return i; + } + var _ = Object.create(null); + __webpack_require__.r(_); + var w = {}; + s = s || [null, o({}), o([]), o(o)]; + for (var x = 2 & u && i; 'object' == typeof x && !~s.indexOf(x); x = o(x)) + Object.getOwnPropertyNames(x).forEach((s) => (w[s] = () => i[s])); + return ((w.default = () => i), __webpack_require__.d(_, w), _); + }), + (__webpack_require__.d = (s, o) => { + for (var i in o) + __webpack_require__.o(o, i) && + !__webpack_require__.o(s, i) && + Object.defineProperty(s, i, { enumerable: !0, get: o[i] }); + }), + (__webpack_require__.g = (function () { + if ('object' == typeof globalThis) return globalThis; + try { + return this || new Function('return this')(); + } catch (s) { + if ('object' == typeof window) return window; + } + })()), + (__webpack_require__.o = (s, o) => Object.prototype.hasOwnProperty.call(s, o)), + (__webpack_require__.r = (s) => { + ('undefined' != typeof Symbol && + Symbol.toStringTag && + Object.defineProperty(s, Symbol.toStringTag, { value: 'Module' }), + Object.defineProperty(s, '__esModule', { value: !0 })); + }), + (__webpack_require__.nmd = (s) => ((s.paths = []), s.children || (s.children = []), s))); + var _ = {}; + return ( + (() => { + 'use strict'; + __webpack_require__.d(_, { default: () => WI }); + var s = {}; + (__webpack_require__.r(s), + __webpack_require__.d(s, { + CLEAR: () => ot, + CLEAR_BY: () => it, + NEW_AUTH_ERR: () => st, + NEW_SPEC_ERR: () => rt, + NEW_SPEC_ERR_BATCH: () => nt, + NEW_THROWN_ERR: () => et, + NEW_THROWN_ERR_BATCH: () => tt, + clear: () => clear, + clearBy: () => clearBy, + newAuthErr: () => newAuthErr, + newSpecErr: () => newSpecErr, + newSpecErrBatch: () => newSpecErrBatch, + newThrownErr: () => newThrownErr, + newThrownErrBatch: () => newThrownErrBatch + })); + var o = {}; + (__webpack_require__.r(o), + __webpack_require__.d(o, { + AUTHORIZE: () => Nt, + AUTHORIZE_OAUTH2: () => Lt, + CONFIGURE_AUTH: () => Ft, + LOGOUT: () => Rt, + PRE_AUTHORIZE_OAUTH2: () => Dt, + RESTORE_AUTHORIZATION: () => qt, + SHOW_AUTH_POPUP: () => Tt, + VALIDATE: () => Bt, + authPopup: () => authPopup, + authorize: () => authorize, + authorizeAccessCodeWithBasicAuthentication: () => + authorizeAccessCodeWithBasicAuthentication, + authorizeAccessCodeWithFormParams: () => authorizeAccessCodeWithFormParams, + authorizeApplication: () => authorizeApplication, + authorizeOauth2: () => authorizeOauth2, + authorizeOauth2WithPersistOption: () => authorizeOauth2WithPersistOption, + authorizePassword: () => authorizePassword, + authorizeRequest: () => authorizeRequest, + authorizeWithPersistOption: () => authorizeWithPersistOption, + configureAuth: () => configureAuth, + logout: () => logout, + logoutWithPersistOption: () => logoutWithPersistOption, + persistAuthorizationIfNeeded: () => persistAuthorizationIfNeeded, + preAuthorizeImplicit: () => preAuthorizeImplicit, + restoreAuthorization: () => restoreAuthorization, + showDefinitions: () => showDefinitions + })); + var i = {}; + (__webpack_require__.r(i), + __webpack_require__.d(i, { + authorized: () => Ht, + definitionsForRequirements: () => definitionsForRequirements, + definitionsToAuthorize: () => Kt, + getConfigs: () => Jt, + getDefinitionsByNames: () => getDefinitionsByNames, + isAuthorized: () => isAuthorized, + shownDefinitions: () => Wt + })); + var u = {}; + (__webpack_require__.r(u), + __webpack_require__.d(u, { + TOGGLE_CONFIGS: () => yn, + UPDATE_CONFIGS: () => gn, + downloadConfig: () => downloadConfig, + getConfigByUrl: () => getConfigByUrl, + loaded: () => actions_loaded, + toggle: () => toggle, + update: () => update + })); + var w = {}; + (__webpack_require__.r(w), __webpack_require__.d(w, { get: () => get })); + var x = {}; + (__webpack_require__.r(x), __webpack_require__.d(x, { transform: () => transform })); + var C = {}; + (__webpack_require__.r(C), + __webpack_require__.d(C, { transform: () => parameter_oneof_transform })); + var j = {}; + (__webpack_require__.r(j), + __webpack_require__.d(j, { allErrors: () => Mn, lastError: () => Tn })); + var L = {}; + (__webpack_require__.r(L), + __webpack_require__.d(L, { + SHOW: () => Fn, + UPDATE_FILTER: () => Ln, + UPDATE_LAYOUT: () => Dn, + UPDATE_MODE: () => Bn, + changeMode: () => changeMode, + show: () => actions_show, + updateFilter: () => updateFilter, + updateLayout: () => updateLayout + })); + var B = {}; + (__webpack_require__.r(B), + __webpack_require__.d(B, { + current: () => current, + currentFilter: () => currentFilter, + isShown: () => isShown, + showSummary: () => $n, + whatMode: () => whatMode + })); + var $ = {}; + (__webpack_require__.r($), + __webpack_require__.d($, { taggedOperations: () => taggedOperations })); + var V = {}; + (__webpack_require__.r(V), + __webpack_require__.d(V, { + requestSnippetGenerator_curl_bash: () => requestSnippetGenerator_curl_bash, + requestSnippetGenerator_curl_cmd: () => requestSnippetGenerator_curl_cmd, + requestSnippetGenerator_curl_powershell: () => requestSnippetGenerator_curl_powershell + })); + var U = {}; + (__webpack_require__.r(U), + __webpack_require__.d(U, { + getActiveLanguage: () => zn, + getDefaultExpanded: () => Wn, + getGenerators: () => Un, + getSnippetGenerators: () => getSnippetGenerators + })); + var z = {}; + (__webpack_require__.r(z), + __webpack_require__.d(z, { + JsonSchemaArrayItemFile: () => JsonSchemaArrayItemFile, + JsonSchemaArrayItemText: () => JsonSchemaArrayItemText, + JsonSchemaForm: () => JsonSchemaForm, + JsonSchema_array: () => JsonSchema_array, + JsonSchema_boolean: () => JsonSchema_boolean, + JsonSchema_object: () => JsonSchema_object, + JsonSchema_string: () => JsonSchema_string + })); + var Y = {}; + (__webpack_require__.r(Y), + __webpack_require__.d(Y, { + allowTryItOutFor: () => allowTryItOutFor, + basePath: () => Ks, + canExecuteScheme: () => canExecuteScheme, + consumes: () => $s, + consumesOptionsFor: () => consumesOptionsFor, + contentTypeValues: () => contentTypeValues, + currentProducesFor: () => currentProducesFor, + definitions: () => Ws, + externalDocs: () => Rs, + findDefinition: () => findDefinition, + getOAS3RequiredRequestBodyContentType: () => getOAS3RequiredRequestBodyContentType, + getParameter: () => getParameter, + hasHost: () => to, + host: () => Hs, + info: () => Ns, + isMediaTypeSchemaPropertiesEqual: () => isMediaTypeSchemaPropertiesEqual, + isOAS3: () => Ts, + lastError: () => ks, + mutatedRequestFor: () => mutatedRequestFor, + mutatedRequests: () => eo, + operationScheme: () => operationScheme, + operationWithMeta: () => operationWithMeta, + operations: () => qs, + operationsWithRootInherited: () => Gs, + operationsWithTags: () => Xs, + parameterInclusionSettingFor: () => parameterInclusionSettingFor, + parameterValues: () => parameterValues, + parameterWithMeta: () => parameterWithMeta, + parameterWithMetaByIdentity: () => parameterWithMetaByIdentity, + parametersIncludeIn: () => parametersIncludeIn, + parametersIncludeType: () => parametersIncludeType, + paths: () => Bs, + produces: () => Vs, + producesOptionsFor: () => producesOptionsFor, + requestFor: () => requestFor, + requests: () => Qs, + responseFor: () => responseFor, + responses: () => Zs, + schemes: () => Js, + security: () => Us, + securityDefinitions: () => zs, + semver: () => Ls, + spec: () => spec, + specJS: () => Is, + specJson: () => js, + specJsonWithResolvedSubtrees: () => Ms, + specResolved: () => Ps, + specResolvedSubtree: () => specResolvedSubtree, + specSource: () => As, + specStr: () => Os, + tagDetails: () => tagDetails, + taggedOperations: () => selectors_taggedOperations, + tags: () => Ys, + url: () => Cs, + validOperationMethods: () => Fs, + validateBeforeExecute: () => validateBeforeExecute, + validationErrors: () => validationErrors, + version: () => Ds + })); + var Z = {}; + (__webpack_require__.r(Z), + __webpack_require__.d(Z, { + CLEAR_REQUEST: () => wo, + CLEAR_RESPONSE: () => Eo, + CLEAR_VALIDATE_PARAMS: () => So, + LOG_REQUEST: () => _o, + SET_MUTATED_REQUEST: () => bo, + SET_REQUEST: () => vo, + SET_RESPONSE: () => yo, + SET_SCHEME: () => Oo, + UPDATE_EMPTY_PARAM_INCLUSION: () => mo, + UPDATE_JSON: () => ho, + UPDATE_OPERATION_META_VALUE: () => xo, + UPDATE_PARAM: () => fo, + UPDATE_RESOLVED: () => ko, + UPDATE_RESOLVED_SUBTREE: () => Co, + UPDATE_SPEC: () => uo, + UPDATE_URL: () => po, + VALIDATE_PARAMS: () => go, + changeConsumesValue: () => changeConsumesValue, + changeParam: () => changeParam, + changeParamByIdentity: () => changeParamByIdentity, + changeProducesValue: () => changeProducesValue, + clearRequest: () => clearRequest, + clearResponse: () => clearResponse, + clearValidateParams: () => clearValidateParams, + execute: () => actions_execute, + executeRequest: () => executeRequest, + invalidateResolvedSubtreeCache: () => invalidateResolvedSubtreeCache, + logRequest: () => logRequest, + parseToJson: () => parseToJson, + requestResolvedSubtree: () => requestResolvedSubtree, + resolveSpec: () => resolveSpec, + setMutatedRequest: () => setMutatedRequest, + setRequest: () => setRequest, + setResponse: () => setResponse, + setScheme: () => setScheme, + updateEmptyParamInclusion: () => updateEmptyParamInclusion, + updateJsonSpec: () => updateJsonSpec, + updateResolved: () => updateResolved, + updateResolvedSubtree: () => updateResolvedSubtree, + updateSpec: () => updateSpec, + updateUrl: () => updateUrl, + validateParams: () => validateParams + })); + var ee = {}; + (__webpack_require__.r(ee), + __webpack_require__.d(ee, { + executeRequest: () => wrap_actions_executeRequest, + updateJsonSpec: () => wrap_actions_updateJsonSpec, + updateSpec: () => wrap_actions_updateSpec, + validateParams: () => wrap_actions_validateParams + })); + var ie = {}; + (__webpack_require__.r(ie), + __webpack_require__.d(ie, { + JsonPatchError: () => Ro, + _areEquals: () => _areEquals, + applyOperation: () => applyOperation, + applyPatch: () => applyPatch, + applyReducer: () => applyReducer, + deepClone: () => Do, + getValueByPointer: () => getValueByPointer, + validate: () => validate, + validator: () => validator + })); + var ae = {}; + (__webpack_require__.r(ae), + __webpack_require__.d(ae, { + compare: () => compare, + generate: () => generate, + observe: () => observe, + unobserve: () => unobserve + })); + var le = {}; + (__webpack_require__.r(le), + __webpack_require__.d(le, { + hasElementSourceMap: () => hasElementSourceMap, + includesClasses: () => includesClasses, + includesSymbols: () => includesSymbols, + isAnnotationElement: () => zu, + isArrayElement: () => qu, + isBooleanElement: () => Bu, + isCommentElement: () => Wu, + isElement: () => Nu, + isLinkElement: () => Vu, + isMemberElement: () => $u, + isNullElement: () => Lu, + isNumberElement: () => Du, + isObjectElement: () => Fu, + isParseResultElement: () => Ku, + isPrimitiveElement: () => isPrimitiveElement, + isRefElement: () => Uu, + isSourceMapElement: () => Hu, + isStringElement: () => Ru + })); + var ce = {}; + (__webpack_require__.r(ce), + __webpack_require__.d(ce, { + isJSONReferenceElement: () => Nf, + isJSONSchemaElement: () => Tf, + isLinkDescriptionElement: () => Df, + isMediaElement: () => Rf + })); + var pe = {}; + (__webpack_require__.r(pe), + __webpack_require__.d(pe, { + isBooleanJsonSchemaElement: () => isBooleanJsonSchemaElement, + isCallbackElement: () => Im, + isComponentsElement: () => Pm, + isContactElement: () => Mm, + isExampleElement: () => Tm, + isExternalDocumentationElement: () => Nm, + isHeaderElement: () => Rm, + isInfoElement: () => Dm, + isLicenseElement: () => Lm, + isLinkElement: () => Bm, + isMediaTypeElement: () => eg, + isOpenApi3_0Element: () => qm, + isOpenapiElement: () => Fm, + isOperationElement: () => $m, + isParameterElement: () => Vm, + isPathItemElement: () => Um, + isPathsElement: () => zm, + isReferenceElement: () => Wm, + isRequestBodyElement: () => Km, + isResponseElement: () => Hm, + isResponsesElement: () => Jm, + isSchemaElement: () => Gm, + isSecurityRequirementElement: () => Ym, + isSecuritySchemeElement: () => Xm, + isServerElement: () => Zm, + isServerVariableElement: () => Qm, + isServersElement: () => rg + })); + var de = {}; + (__webpack_require__.r(de), + __webpack_require__.d(de, { + isBooleanJsonSchemaElement: () => predicates_isBooleanJsonSchemaElement, + isCallbackElement: () => T_, + isComponentsElement: () => N_, + isContactElement: () => R_, + isExampleElement: () => D_, + isExternalDocumentationElement: () => L_, + isHeaderElement: () => B_, + isInfoElement: () => F_, + isJsonSchemaDialectElement: () => q_, + isLicenseElement: () => $_, + isLinkElement: () => V_, + isMediaTypeElement: () => sE, + isOpenApi3_1Element: () => z_, + isOpenapiElement: () => U_, + isOperationElement: () => W_, + isParameterElement: () => K_, + isPathItemElement: () => H_, + isPathItemElementExternal: () => isPathItemElementExternal, + isPathsElement: () => J_, + isReferenceElement: () => G_, + isReferenceElementExternal: () => isReferenceElementExternal, + isRequestBodyElement: () => Y_, + isResponseElement: () => X_, + isResponsesElement: () => Z_, + isSchemaElement: () => Q_, + isSecurityRequirementElement: () => eE, + isSecuritySchemeElement: () => tE, + isServerElement: () => rE, + isServerVariableElement: () => nE + })); + var fe = {}; + (__webpack_require__.r(fe), + __webpack_require__.d(fe, { + cookie: () => parameter_builders_cookie, + header: () => parameter_builders_header, + path: () => parameter_builders_path, + query: () => parameter_builders_query + })); + var ye = {}; + (__webpack_require__.r(ye), + __webpack_require__.d(ye, { + Button: () => Button, + Col: () => Col, + Collapse: () => Collapse, + Container: () => Container, + Input: () => Input, + Link: () => layout_utils_Link, + Row: () => Row, + Select: () => Select, + TextArea: () => TextArea + })); + var be = {}; + (__webpack_require__.r(be), + __webpack_require__.d(be, { + basePath: () => KO, + consumes: () => HO, + definitions: () => VO, + findDefinition: () => $O, + hasHost: () => UO, + host: () => WO, + produces: () => JO, + schemes: () => GO, + securityDefinitions: () => zO, + validOperationMethods: () => wrap_selectors_validOperationMethods + })); + var _e = {}; + (__webpack_require__.r(_e), + __webpack_require__.d(_e, { definitionsToAuthorize: () => YO })); + var we = {}; + (__webpack_require__.r(we), + __webpack_require__.d(we, { + callbacksOperations: () => QO, + findSchema: () => findSchema, + isOAS3: () => selectors_isOAS3, + isOAS30: () => selectors_isOAS30, + isSwagger2: () => selectors_isSwagger2, + servers: () => ZO + })); + var Se = {}; + (__webpack_require__.r(Se), + __webpack_require__.d(Se, { + CLEAR_REQUEST_BODY_VALIDATE_ERROR: () => bA, + CLEAR_REQUEST_BODY_VALUE: () => _A, + SET_REQUEST_BODY_VALIDATE_ERROR: () => vA, + UPDATE_ACTIVE_EXAMPLES_MEMBER: () => fA, + UPDATE_REQUEST_BODY_INCLUSION: () => dA, + UPDATE_REQUEST_BODY_VALUE: () => pA, + UPDATE_REQUEST_BODY_VALUE_RETAIN_FLAG: () => hA, + UPDATE_REQUEST_CONTENT_TYPE: () => mA, + UPDATE_RESPONSE_CONTENT_TYPE: () => gA, + UPDATE_SELECTED_SERVER: () => uA, + UPDATE_SERVER_VARIABLE_VALUE: () => yA, + clearRequestBodyValidateError: () => clearRequestBodyValidateError, + clearRequestBodyValue: () => clearRequestBodyValue, + initRequestBodyValidateError: () => initRequestBodyValidateError, + setActiveExamplesMember: () => setActiveExamplesMember, + setRequestBodyInclusion: () => setRequestBodyInclusion, + setRequestBodyValidateError: () => setRequestBodyValidateError, + setRequestBodyValue: () => setRequestBodyValue, + setRequestContentType: () => setRequestContentType, + setResponseContentType: () => setResponseContentType, + setRetainRequestBodyValueFlag: () => setRetainRequestBodyValueFlag, + setSelectedServer: () => setSelectedServer, + setServerVariableValue: () => setServerVariableValue + })); + var xe = {}; + (__webpack_require__.r(xe), + __webpack_require__.d(xe, { + activeExamplesMember: () => jA, + hasUserEditedBody: () => CA, + requestBodyErrors: () => AA, + requestBodyInclusionSetting: () => OA, + requestBodyValue: () => xA, + requestContentType: () => IA, + responseContentType: () => PA, + selectDefaultRequestBodyValue: () => selectDefaultRequestBodyValue, + selectedServer: () => SA, + serverEffectiveValue: () => NA, + serverVariableValue: () => MA, + serverVariables: () => TA, + shouldRetainRequestBodyValue: () => kA, + validOperationMethods: () => DA, + validateBeforeExecute: () => RA, + validateShallowRequired: () => validateShallowRequired + })); + var Pe = __webpack_require__(96540); + function formatProdErrorMessage(s) { + return `Minified Redux error #${s}; visit https://redux.js.org/Errors?code=${s} for the full message or use the non-minified dev environment for full errors. `; + } + var Te = (() => ('function' == typeof Symbol && Symbol.observable) || '@@observable')(), + randomString = () => Math.random().toString(36).substring(7).split('').join('.'), + Re = { + INIT: `@@redux/INIT${randomString()}`, + REPLACE: `@@redux/REPLACE${randomString()}`, + PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}` + }; + function isPlainObject(s) { + if ('object' != typeof s || null === s) return !1; + let o = s; + for (; null !== Object.getPrototypeOf(o); ) o = Object.getPrototypeOf(o); + return Object.getPrototypeOf(s) === o || null === Object.getPrototypeOf(s); + } + function createStore(s, o, i) { + if ('function' != typeof s) throw new Error(formatProdErrorMessage(2)); + if ( + ('function' == typeof o && 'function' == typeof i) || + ('function' == typeof i && 'function' == typeof arguments[3]) + ) + throw new Error(formatProdErrorMessage(0)); + if (('function' == typeof o && void 0 === i && ((i = o), (o = void 0)), void 0 !== i)) { + if ('function' != typeof i) throw new Error(formatProdErrorMessage(1)); + return i(createStore)(s, o); + } + let u = s, + _ = o, + w = new Map(), + x = w, + C = 0, + j = !1; + function ensureCanMutateNextListeners() { + x === w && + ((x = new Map()), + w.forEach((s, o) => { + x.set(o, s); + })); + } + function getState() { + if (j) throw new Error(formatProdErrorMessage(3)); + return _; + } + function subscribe(s) { + if ('function' != typeof s) throw new Error(formatProdErrorMessage(4)); + if (j) throw new Error(formatProdErrorMessage(5)); + let o = !0; + ensureCanMutateNextListeners(); + const i = C++; + return ( + x.set(i, s), + function unsubscribe() { + if (o) { + if (j) throw new Error(formatProdErrorMessage(6)); + ((o = !1), ensureCanMutateNextListeners(), x.delete(i), (w = null)); + } + } + ); + } + function dispatch(s) { + if (!isPlainObject(s)) throw new Error(formatProdErrorMessage(7)); + if (void 0 === s.type) throw new Error(formatProdErrorMessage(8)); + if ('string' != typeof s.type) throw new Error(formatProdErrorMessage(17)); + if (j) throw new Error(formatProdErrorMessage(9)); + try { + ((j = !0), (_ = u(_, s))); + } finally { + j = !1; + } + return ( + (w = x).forEach((s) => { + s(); + }), + s + ); + } + dispatch({ type: Re.INIT }); + return { + dispatch, + subscribe, + getState, + replaceReducer: function replaceReducer(s) { + if ('function' != typeof s) throw new Error(formatProdErrorMessage(10)); + ((u = s), dispatch({ type: Re.REPLACE })); + }, + [Te]: function observable() { + const s = subscribe; + return { + subscribe(o) { + if ('object' != typeof o || null === o) + throw new Error(formatProdErrorMessage(11)); + function observeState() { + const s = o; + s.next && s.next(getState()); + } + observeState(); + return { unsubscribe: s(observeState) }; + }, + [Te]() { + return this; + } + }; + } + }; + } + function bindActionCreator(s, o) { + return function (...i) { + return o(s.apply(this, i)); + }; + } + function compose(...s) { + return 0 === s.length + ? (s) => s + : 1 === s.length + ? s[0] + : s.reduce( + (s, o) => + (...i) => + s(o(...i)) + ); + } + var qe = __webpack_require__(9404), + $e = __webpack_require__.n(qe), + ze = __webpack_require__(81919), + We = __webpack_require__.n(ze), + He = __webpack_require__(89593), + Ye = __webpack_require__(20334), + Xe = __webpack_require__(55364), + Qe = __webpack_require__.n(Xe); + const et = 'err_new_thrown_err', + tt = 'err_new_thrown_err_batch', + rt = 'err_new_spec_err', + nt = 'err_new_spec_err_batch', + st = 'err_new_auth_err', + ot = 'err_clear', + it = 'err_clear_by'; + function newThrownErr(s) { + return { type: et, payload: (0, Ye.serializeError)(s) }; + } + function newThrownErrBatch(s) { + return { type: tt, payload: s }; + } + function newSpecErr(s) { + return { type: rt, payload: s }; + } + function newSpecErrBatch(s) { + return { type: nt, payload: s }; + } + function newAuthErr(s) { + return { type: st, payload: s }; + } + function clear(s = {}) { + return { type: ot, payload: s }; + } + function clearBy(s = () => !0) { + return { type: it, payload: s }; + } + const at = (function makeWindow() { + var s = { + location: {}, + history: {}, + open: () => {}, + close: () => {}, + File: function () {}, + FormData: function () {} + }; + if ('undefined' == typeof window) return s; + try { + s = window; + for (var o of ['File', 'Blob', 'FormData']) o in window && (s[o] = window[o]); + } catch (s) { + console.error(s); + } + return s; + })(); + var lt = __webpack_require__(16750), + ct = (__webpack_require__(84058), __webpack_require__(55808), __webpack_require__(50104)), + ut = __webpack_require__.n(ct), + pt = __webpack_require__(7309), + ht = __webpack_require__.n(pt), + dt = __webpack_require__(42426), + mt = __webpack_require__.n(dt), + gt = __webpack_require__(75288), + yt = __webpack_require__.n(gt), + vt = __webpack_require__(1882), + bt = __webpack_require__.n(vt), + _t = __webpack_require__(2205), + Et = __webpack_require__.n(_t), + wt = __webpack_require__(53209), + St = __webpack_require__.n(wt), + xt = __webpack_require__(62802), + kt = __webpack_require__.n(xt); + const Ct = $e().Set.of( + 'type', + 'format', + 'items', + 'default', + 'maximum', + 'exclusiveMaximum', + 'minimum', + 'exclusiveMinimum', + 'maxLength', + 'minLength', + 'pattern', + 'maxItems', + 'minItems', + 'uniqueItems', + 'enum', + 'multipleOf' + ); + function getParameterSchema(s, { isOAS3: o } = {}) { + if (!$e().Map.isMap(s)) return { schema: $e().Map(), parameterContentMediaType: null }; + if (!o) + return 'body' === s.get('in') + ? { schema: s.get('schema', $e().Map()), parameterContentMediaType: null } + : { schema: s.filter((s, o) => Ct.includes(o)), parameterContentMediaType: null }; + if (s.get('content')) { + const o = s.get('content', $e().Map({})).keySeq().first(); + return { + schema: s.getIn(['content', o, 'schema'], $e().Map()), + parameterContentMediaType: o + }; + } + return { + schema: s.get('schema') ? s.get('schema', $e().Map()) : $e().Map(), + parameterContentMediaType: null + }; + } + var Ot = __webpack_require__(48287).Buffer; + const At = 'default', + isImmutable = (s) => $e().Iterable.isIterable(s); + function objectify(s) { + return isObject(s) ? (isImmutable(s) ? s.toJS() : s) : {}; + } + function fromJSOrdered(s) { + if (isImmutable(s)) return s; + if (s instanceof at.File) return s; + if (!isObject(s)) return s; + if (Array.isArray(s)) return $e().Seq(s).map(fromJSOrdered).toList(); + if (bt()(s.entries)) { + const o = (function createObjWithHashedKeys(s) { + if (!bt()(s.entries)) return s; + const o = {}, + i = '_**[]', + u = {}; + for (let _ of s.entries()) + if (o[_[0]] || (u[_[0]] && u[_[0]].containsMultiple)) { + if (!u[_[0]]) { + ((u[_[0]] = { containsMultiple: !0, length: 1 }), + (o[`${_[0]}${i}${u[_[0]].length}`] = o[_[0]]), + delete o[_[0]]); + } + ((u[_[0]].length += 1), (o[`${_[0]}${i}${u[_[0]].length}`] = _[1])); + } else o[_[0]] = _[1]; + return o; + })(s); + return $e().OrderedMap(o).map(fromJSOrdered); + } + return $e().OrderedMap(s).map(fromJSOrdered); + } + function normalizeArray(s) { + return Array.isArray(s) ? s : [s]; + } + function isFn(s) { + return 'function' == typeof s; + } + function isObject(s) { + return !!s && 'object' == typeof s; + } + function isFunc(s) { + return 'function' == typeof s; + } + function isArray(s) { + return Array.isArray(s); + } + const jt = ut(); + function objMap(s, o) { + return Object.keys(s).reduce((i, u) => ((i[u] = o(s[u], u)), i), {}); + } + function objReduce(s, o) { + return Object.keys(s).reduce((i, u) => { + let _ = o(s[u], u); + return (_ && 'object' == typeof _ && Object.assign(i, _), i); + }, {}); + } + function systemThunkMiddleware(s) { + return ({ dispatch: o, getState: i }) => + (o) => + (i) => + 'function' == typeof i ? i(s()) : o(i); + } + function validateValueBySchema(s, o, i, u, _) { + if (!o) return []; + let w = [], + x = o.get('nullable'), + C = o.get('required'), + j = o.get('maximum'), + L = o.get('minimum'), + B = o.get('type'), + $ = o.get('format'), + V = o.get('maxLength'), + U = o.get('minLength'), + z = o.get('uniqueItems'), + Y = o.get('maxItems'), + Z = o.get('minItems'), + ee = o.get('pattern'); + const ie = i || !0 === C, + ae = null != s, + le = ie || (ae && 'array' === B) || !(!ie && !ae), + ce = x && null === s; + if (ie && !ae && !ce && !u && !B) return (w.push('Required field is not provided'), w); + if (ce || !B || !le) return []; + let pe = 'string' === B && s, + de = 'array' === B && Array.isArray(s) && s.length, + fe = 'array' === B && $e().List.isList(s) && s.count(); + const ye = [ + pe, + de, + fe, + 'array' === B && 'string' == typeof s && s, + 'file' === B && s instanceof at.File, + 'boolean' === B && (s || !1 === s), + 'number' === B && (s || 0 === s), + 'integer' === B && (s || 0 === s), + 'object' === B && 'object' == typeof s && null !== s, + 'object' === B && 'string' == typeof s && s + ].some((s) => !!s); + if (ie && !ye && !u) return (w.push('Required field is not provided'), w); + if ('object' === B && (null === _ || 'application/json' === _)) { + let i = s; + if ('string' == typeof s) + try { + i = JSON.parse(s); + } catch (s) { + return (w.push('Parameter string value must be valid JSON'), w); + } + (o && + o.has('required') && + isFunc(C.isList) && + C.isList() && + C.forEach((s) => { + void 0 === i[s] && w.push({ propKey: s, error: 'Required property not found' }); + }), + o && + o.has('properties') && + o.get('properties').forEach((s, o) => { + const x = validateValueBySchema(i[o], s, !1, u, _); + w.push(...x.map((s) => ({ propKey: o, error: s }))); + })); + } + if (ee) { + let o = ((s, o) => { + if (!new RegExp(o).test(s)) return 'Value must follow pattern ' + o; + })(s, ee); + o && w.push(o); + } + if (Z && 'array' === B) { + let o = ((s, o) => { + if ((!s && o >= 1) || (s && s.length < o)) + return `Array must contain at least ${o} item${1 === o ? '' : 's'}`; + })(s, Z); + o && w.push(o); + } + if (Y && 'array' === B) { + let o = ((s, o) => { + if (s && s.length > o) + return `Array must not contain more then ${o} item${1 === o ? '' : 's'}`; + })(s, Y); + o && w.push({ needRemove: !0, error: o }); + } + if (z && 'array' === B) { + let o = ((s, o) => { + if (s && ('true' === o || !0 === o)) { + const o = (0, qe.fromJS)(s), + i = o.toSet(); + if (s.length > i.size) { + let s = (0, qe.Set)(); + if ( + (o.forEach((i, u) => { + o.filter((s) => (isFunc(s.equals) ? s.equals(i) : s === i)).size > 1 && + (s = s.add(u)); + }), + 0 !== s.size) + ) + return s.map((s) => ({ index: s, error: 'No duplicates allowed.' })).toArray(); + } + } + })(s, z); + o && w.push(...o); + } + if (V || 0 === V) { + let o = ((s, o) => { + if (s.length > o) + return `Value must be no longer than ${o} character${1 !== o ? 's' : ''}`; + })(s, V); + o && w.push(o); + } + if (U) { + let o = ((s, o) => { + if (s.length < o) return `Value must be at least ${o} character${1 !== o ? 's' : ''}`; + })(s, U); + o && w.push(o); + } + if (j || 0 === j) { + let o = ((s, o) => { + if (s > o) return `Value must be less than ${o}`; + })(s, j); + o && w.push(o); + } + if (L || 0 === L) { + let o = ((s, o) => { + if (s < o) return `Value must be greater than ${o}`; + })(s, L); + o && w.push(o); + } + if ('string' === B) { + let o; + if ( + ((o = + 'date-time' === $ + ? ((s) => { + if (isNaN(Date.parse(s))) return 'Value must be a DateTime'; + })(s) + : 'uuid' === $ + ? ((s) => { + if ( + ((s = s.toString().toLowerCase()), + !/^[{(]?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[)}]?$/.test( + s + )) + ) + return 'Value must be a Guid'; + })(s) + : ((s) => { + if (s && 'string' != typeof s) return 'Value must be a string'; + })(s)), + !o) + ) + return w; + w.push(o); + } else if ('boolean' === B) { + let o = ((s) => { + if ('true' !== s && 'false' !== s && !0 !== s && !1 !== s) + return 'Value must be a boolean'; + })(s); + if (!o) return w; + w.push(o); + } else if ('number' === B) { + let o = ((s) => { + if (!/^-?\d+(\.?\d+)?$/.test(s)) return 'Value must be a number'; + })(s); + if (!o) return w; + w.push(o); + } else if ('integer' === B) { + let o = ((s) => { + if (!/^-?\d+$/.test(s)) return 'Value must be an integer'; + })(s); + if (!o) return w; + w.push(o); + } else if ('array' === B) { + if (!de && !fe) return w; + s && + s.forEach((s, i) => { + const x = validateValueBySchema(s, o.get('items'), !1, u, _); + w.push(...x.map((s) => ({ index: i, error: s }))); + }); + } else if ('file' === B) { + let o = ((s) => { + if (s && !(s instanceof at.File)) return 'Value must be a file'; + })(s); + if (!o) return w; + w.push(o); + } + return w; + } + const utils_btoa = (s) => { + let o; + return ( + (o = s instanceof Ot ? s : Ot.from(s.toString(), 'utf-8')), + o.toString('base64') + ); + }, + It = { + operationsSorter: { + alpha: (s, o) => s.get('path').localeCompare(o.get('path')), + method: (s, o) => s.get('method').localeCompare(o.get('method')) + }, + tagsSorter: { alpha: (s, o) => s.localeCompare(o) } + }, + buildFormData = (s) => { + let o = []; + for (let i in s) { + let u = s[i]; + void 0 !== u && + '' !== u && + o.push([i, '=', encodeURIComponent(u).replace(/%20/g, '+')].join('')); + } + return o.join('&'); + }, + shallowEqualKeys = (s, o, i) => !!ht()(i, (i) => yt()(s[i], o[i])); + function sanitizeUrl(s) { + return 'string' != typeof s || '' === s ? '' : (0, lt.J)(s); + } + function requiresValidationURL(s) { + return !( + !s || + s.indexOf('localhost') >= 0 || + s.indexOf('127.0.0.1') >= 0 || + 'none' === s + ); + } + const createDeepLinkPath = (s) => + 'string' == typeof s || s instanceof String ? s.trim().replace(/\s/g, '%20') : '', + escapeDeepLinkPath = (s) => Et()(createDeepLinkPath(s).replace(/%20/g, '_')), + getExtensions = (s) => s.filter((s, o) => /^x-/.test(o)), + getCommonExtensions = (s) => + s.filter((s, o) => /^pattern|maxLength|minLength|maximum|minimum/.test(o)); + function deeplyStripKey(s, o, i = () => !0) { + if ('object' != typeof s || Array.isArray(s) || null === s || !o) return s; + const u = Object.assign({}, s); + return ( + Object.keys(u).forEach((s) => { + s === o && i(u[s], s) ? delete u[s] : (u[s] = deeplyStripKey(u[s], o, i)); + }), + u + ); + } + function stringify(s) { + if ('string' == typeof s) return s; + if ((s && s.toJS && (s = s.toJS()), 'object' == typeof s && null !== s)) + try { + return JSON.stringify(s, null, 2); + } catch (o) { + return String(s); + } + return null == s ? '' : s.toString(); + } + function paramToIdentifier(s, { returnAll: o = !1, allowHashes: i = !0 } = {}) { + if (!$e().Map.isMap(s)) + throw new Error('paramToIdentifier: received a non-Im.Map parameter as input'); + const u = s.get('name'), + _ = s.get('in'); + let w = []; + return ( + s && s.hashCode && _ && u && i && w.push(`${_}.${u}.hash-${s.hashCode()}`), + _ && u && w.push(`${_}.${u}`), + w.push(u), + o ? w : w[0] || '' + ); + } + function paramToValue(s, o) { + return paramToIdentifier(s, { returnAll: !0 }) + .map((s) => o[s]) + .filter((s) => void 0 !== s)[0]; + } + function b64toB64UrlEncoded(s) { + return s.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); + } + const isEmptyValue = (s) => !s || !(!isImmutable(s) || !s.isEmpty()), + idFn = (s) => s; + function createStoreWithMiddleware(s, o, i) { + let u = [systemThunkMiddleware(i)]; + return createStore( + s, + o, + (at.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose)( + (function applyMiddleware(...s) { + return (o) => (i, u) => { + const _ = o(i, u); + let dispatch = () => { + throw new Error(formatProdErrorMessage(15)); + }; + const w = { getState: _.getState, dispatch: (s, ...o) => dispatch(s, ...o) }, + x = s.map((s) => s(w)); + return ((dispatch = compose(...x)(_.dispatch)), { ..._, dispatch }); + }; + })(...u) + ) + ); + } + class Store { + constructor(s = {}) { + (We()( + this, + { + state: {}, + plugins: [], + system: { configs: {}, fn: {}, components: {}, rootInjects: {}, statePlugins: {} }, + boundSystem: {}, + toolbox: {} + }, + s + ), + (this.getSystem = this._getSystem.bind(this)), + (this.store = (function configureStore(s, o, i) { + return createStoreWithMiddleware(s, o, i); + })(idFn, (0, qe.fromJS)(this.state), this.getSystem)), + this.buildSystem(!1), + this.register(this.plugins)); + } + getStore() { + return this.store; + } + register(s, o = !0) { + var i = combinePlugins(s, this.getSystem()); + (systemExtend(this.system, i), o && this.buildSystem()); + callAfterLoad.call(this.system, s, this.getSystem()) && this.buildSystem(); + } + buildSystem(s = !0) { + let o = this.getStore().dispatch, + i = this.getStore().getState; + ((this.boundSystem = Object.assign( + {}, + this.getRootInjects(), + this.getWrappedAndBoundActions(o), + this.getWrappedAndBoundSelectors(i, this.getSystem), + this.getStateThunks(i), + this.getFn(), + this.getConfigs() + )), + s && this.rebuildReducer()); + } + _getSystem() { + return this.boundSystem; + } + getRootInjects() { + return Object.assign( + { + getSystem: this.getSystem, + getStore: this.getStore.bind(this), + getComponents: this.getComponents.bind(this), + getState: this.getStore().getState, + getConfigs: this._getConfigs.bind(this), + Im: $e(), + React: Pe + }, + this.system.rootInjects || {} + ); + } + _getConfigs() { + return this.system.configs; + } + getConfigs() { + return { configs: this.system.configs }; + } + setConfigs(s) { + this.system.configs = s; + } + rebuildReducer() { + this.store.replaceReducer( + (function buildReducer(s) { + return (function allReducers(s) { + let o = Object.keys(s).reduce( + (o, i) => ( + (o[i] = (function makeReducer(s) { + return (o = new qe.Map(), i) => { + if (!s) return o; + let u = s[i.type]; + if (u) { + const s = wrapWithTryCatch(u)(o, i); + return null === s ? o : s; + } + return o; + }; + })(s[i])), + o + ), + {} + ); + if (!Object.keys(o).length) return idFn; + return (0, He.H)(o); + })(objMap(s, (s) => s.reducers)); + })(this.system.statePlugins) + ); + } + getType(s) { + let o = s[0].toUpperCase() + s.slice(1); + return objReduce(this.system.statePlugins, (i, u) => { + let _ = i[s]; + if (_) return { [u + o]: _ }; + }); + } + getSelectors() { + return this.getType('selectors'); + } + getActions() { + return objMap(this.getType('actions'), (s) => + objReduce(s, (s, o) => { + if (isFn(s)) return { [o]: s }; + }) + ); + } + getWrappedAndBoundActions(s) { + return objMap(this.getBoundActions(s), (s, o) => { + let i = this.system.statePlugins[o.slice(0, -7)].wrapActions; + return i + ? objMap(s, (s, o) => { + let u = i[o]; + return u + ? (Array.isArray(u) || (u = [u]), + u.reduce((s, o) => { + let newAction = (...i) => o(s, this.getSystem())(...i); + if (!isFn(newAction)) + throw new TypeError( + 'wrapActions needs to return a function that returns a new function (ie the wrapped action)' + ); + return wrapWithTryCatch(newAction); + }, s || Function.prototype)) + : s; + }) + : s; + }); + } + getWrappedAndBoundSelectors(s, o) { + return objMap(this.getBoundSelectors(s, o), (o, i) => { + let u = [i.slice(0, -9)], + _ = this.system.statePlugins[u].wrapSelectors; + return _ + ? objMap(o, (o, i) => { + let w = _[i]; + return w + ? (Array.isArray(w) || (w = [w]), + w.reduce((o, i) => { + let wrappedSelector = (..._) => + i(o, this.getSystem())(s().getIn(u), ..._); + if (!isFn(wrappedSelector)) + throw new TypeError( + 'wrapSelector needs to return a function that returns a new function (ie the wrapped action)' + ); + return wrappedSelector; + }, o || Function.prototype)) + : o; + }) + : o; + }); + } + getStates(s) { + return Object.keys(this.system.statePlugins).reduce( + (o, i) => ((o[i] = s.get(i)), o), + {} + ); + } + getStateThunks(s) { + return Object.keys(this.system.statePlugins).reduce( + (o, i) => ((o[i] = () => s().get(i)), o), + {} + ); + } + getFn() { + return { fn: this.system.fn }; + } + getComponents(s) { + const o = this.system.components[s]; + return Array.isArray(o) + ? o.reduce((s, o) => o(s, this.getSystem())) + : void 0 !== s + ? this.system.components[s] + : this.system.components; + } + getBoundSelectors(s, o) { + return objMap(this.getSelectors(), (i, u) => { + let _ = [u.slice(0, -9)]; + return objMap(i, (i) => (...u) => { + let w = wrapWithTryCatch(i).apply(null, [s().getIn(_), ...u]); + return ('function' == typeof w && (w = wrapWithTryCatch(w)(o())), w); + }); + }); + } + getBoundActions(s) { + s = s || this.getStore().dispatch; + const o = this.getActions(), + process = (s) => + 'function' != typeof s + ? objMap(s, (s) => process(s)) + : (...o) => { + var i = null; + try { + i = s(...o); + } catch (s) { + i = { type: et, error: !0, payload: (0, Ye.serializeError)(s) }; + } finally { + return i; + } + }; + return objMap(o, (o) => + (function bindActionCreators(s, o) { + if ('function' == typeof s) return bindActionCreator(s, o); + if ('object' != typeof s || null === s) throw new Error(formatProdErrorMessage(16)); + const i = {}; + for (const u in s) { + const _ = s[u]; + 'function' == typeof _ && (i[u] = bindActionCreator(_, o)); + } + return i; + })(process(o), s) + ); + } + getMapStateToProps() { + return () => Object.assign({}, this.getSystem()); + } + getMapDispatchToProps(s) { + return (o) => We()({}, this.getWrappedAndBoundActions(o), this.getFn(), s); + } + } + function combinePlugins(s, o) { + return isObject(s) && !isArray(s) + ? Qe()({}, s) + : isFunc(s) + ? combinePlugins(s(o), o) + : isArray(s) + ? s + .map((s) => combinePlugins(s, o)) + .reduce(systemExtend, { components: o.getComponents() }) + : {}; + } + function callAfterLoad(s, o, { hasLoaded: i } = {}) { + let u = i; + return ( + isObject(s) && + !isArray(s) && + 'function' == typeof s.afterLoad && + ((u = !0), wrapWithTryCatch(s.afterLoad).call(this, o)), + isFunc(s) + ? callAfterLoad.call(this, s(o), o, { hasLoaded: u }) + : isArray(s) + ? s.map((s) => callAfterLoad.call(this, s, o, { hasLoaded: u })) + : u + ); + } + function systemExtend(s = {}, o = {}) { + if (!isObject(s)) return {}; + if (!isObject(o)) return s; + o.wrapComponents && + (objMap(o.wrapComponents, (i, u) => { + const _ = s.components && s.components[u]; + _ && Array.isArray(_) + ? ((s.components[u] = _.concat([i])), delete o.wrapComponents[u]) + : _ && ((s.components[u] = [_, i]), delete o.wrapComponents[u]); + }), + Object.keys(o.wrapComponents).length || delete o.wrapComponents); + const { statePlugins: i } = s; + if (isObject(i)) + for (let s in i) { + const u = i[s]; + if (!isObject(u)) continue; + const { wrapActions: _, wrapSelectors: w } = u; + if (isObject(_)) + for (let i in _) { + let u = _[i]; + (Array.isArray(u) || ((u = [u]), (_[i] = u)), + o && + o.statePlugins && + o.statePlugins[s] && + o.statePlugins[s].wrapActions && + o.statePlugins[s].wrapActions[i] && + (o.statePlugins[s].wrapActions[i] = _[i].concat( + o.statePlugins[s].wrapActions[i] + ))); + } + if (isObject(w)) + for (let i in w) { + let u = w[i]; + (Array.isArray(u) || ((u = [u]), (w[i] = u)), + o && + o.statePlugins && + o.statePlugins[s] && + o.statePlugins[s].wrapSelectors && + o.statePlugins[s].wrapSelectors[i] && + (o.statePlugins[s].wrapSelectors[i] = w[i].concat( + o.statePlugins[s].wrapSelectors[i] + ))); + } + } + return We()(s, o); + } + function wrapWithTryCatch(s, { logErrors: o = !0 } = {}) { + return 'function' != typeof s + ? s + : function (...i) { + try { + return s.call(this, ...i); + } catch (s) { + return (o && console.error(s), null); + } + }; + } + var Pt = __webpack_require__(61160), + Mt = __webpack_require__.n(Pt); + const Tt = 'show_popup', + Nt = 'authorize', + Rt = 'logout', + Dt = 'pre_authorize_oauth2', + Lt = 'authorize_oauth2', + Bt = 'validate', + Ft = 'configure_auth', + qt = 'restore_authorization'; + function showDefinitions(s) { + return { type: Tt, payload: s }; + } + function authorize(s) { + return { type: Nt, payload: s }; + } + const authorizeWithPersistOption = + (s) => + ({ authActions: o }) => { + (o.authorize(s), o.persistAuthorizationIfNeeded()); + }; + function logout(s) { + return { type: Rt, payload: s }; + } + const logoutWithPersistOption = + (s) => + ({ authActions: o }) => { + (o.logout(s), o.persistAuthorizationIfNeeded()); + }, + preAuthorizeImplicit = + (s) => + ({ authActions: o, errActions: i }) => { + let { auth: u, token: _, isValid: w } = s, + { schema: x, name: C } = u, + j = x.get('flow'); + (delete at.swaggerUIRedirectOauth2, + 'accessCode' === j || + w || + i.newAuthErr({ + authId: C, + source: 'auth', + level: 'warning', + message: + "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server" + }), + _.error + ? i.newAuthErr({ + authId: C, + source: 'auth', + level: 'error', + message: JSON.stringify(_) + }) + : o.authorizeOauth2WithPersistOption({ auth: u, token: _ })); + }; + function authorizeOauth2(s) { + return { type: Lt, payload: s }; + } + const authorizeOauth2WithPersistOption = + (s) => + ({ authActions: o }) => { + (o.authorizeOauth2(s), o.persistAuthorizationIfNeeded()); + }, + authorizePassword = + (s) => + ({ authActions: o }) => { + let { + schema: i, + name: u, + username: _, + password: w, + passwordType: x, + clientId: C, + clientSecret: j + } = s, + L = { grant_type: 'password', scope: s.scopes.join(' '), username: _, password: w }, + B = {}; + switch (x) { + case 'request-body': + !(function setClientIdAndSecret(s, o, i) { + o && Object.assign(s, { client_id: o }); + i && Object.assign(s, { client_secret: i }); + })(L, C, j); + break; + case 'basic': + B.Authorization = 'Basic ' + utils_btoa(C + ':' + j); + break; + default: + console.warn( + `Warning: invalid passwordType ${x} was passed, not including client id and secret` + ); + } + return o.authorizeRequest({ + body: buildFormData(L), + url: i.get('tokenUrl'), + name: u, + headers: B, + query: {}, + auth: s + }); + }; + const authorizeApplication = + (s) => + ({ authActions: o }) => { + let { schema: i, scopes: u, name: _, clientId: w, clientSecret: x } = s, + C = { Authorization: 'Basic ' + utils_btoa(w + ':' + x) }, + j = { grant_type: 'client_credentials', scope: u.join(' ') }; + return o.authorizeRequest({ + body: buildFormData(j), + name: _, + url: i.get('tokenUrl'), + auth: s, + headers: C + }); + }, + authorizeAccessCodeWithFormParams = + ({ auth: s, redirectUrl: o }) => + ({ authActions: i }) => { + let { schema: u, name: _, clientId: w, clientSecret: x, codeVerifier: C } = s, + j = { + grant_type: 'authorization_code', + code: s.code, + client_id: w, + client_secret: x, + redirect_uri: o, + code_verifier: C + }; + return i.authorizeRequest({ + body: buildFormData(j), + name: _, + url: u.get('tokenUrl'), + auth: s + }); + }, + authorizeAccessCodeWithBasicAuthentication = + ({ auth: s, redirectUrl: o }) => + ({ authActions: i }) => { + let { schema: u, name: _, clientId: w, clientSecret: x, codeVerifier: C } = s, + j = { Authorization: 'Basic ' + utils_btoa(w + ':' + x) }, + L = { + grant_type: 'authorization_code', + code: s.code, + client_id: w, + redirect_uri: o, + code_verifier: C + }; + return i.authorizeRequest({ + body: buildFormData(L), + name: _, + url: u.get('tokenUrl'), + auth: s, + headers: j + }); + }, + authorizeRequest = + (s) => + ({ + fn: o, + getConfigs: i, + authActions: u, + errActions: _, + oas3Selectors: w, + specSelectors: x, + authSelectors: C + }) => { + let j, + { body: L, query: B = {}, headers: $ = {}, name: V, url: U, auth: z } = s, + { additionalQueryStringParams: Y } = C.getConfigs() || {}; + if (x.isOAS3()) { + let s = w.serverEffectiveValue(w.selectedServer()); + j = Mt()(U, s, !0); + } else j = Mt()(U, x.url(), !0); + 'object' == typeof Y && (j.query = Object.assign({}, j.query, Y)); + const Z = j.toString(); + let ee = Object.assign( + { + Accept: 'application/json, text/plain, */*', + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest' + }, + $ + ); + o.fetch({ + url: Z, + method: 'post', + headers: ee, + query: B, + body: L, + requestInterceptor: i().requestInterceptor, + responseInterceptor: i().responseInterceptor + }) + .then(function (s) { + let o = JSON.parse(s.data), + i = o && (o.error || ''), + w = o && (o.parseError || ''); + s.ok + ? i || w + ? _.newAuthErr({ + authId: V, + level: 'error', + source: 'auth', + message: JSON.stringify(o) + }) + : u.authorizeOauth2WithPersistOption({ auth: z, token: o }) + : _.newAuthErr({ + authId: V, + level: 'error', + source: 'auth', + message: s.statusText + }); + }) + .catch((s) => { + let o = new Error(s).message; + if (s.response && s.response.data) { + const i = s.response.data; + try { + const s = 'string' == typeof i ? JSON.parse(i) : i; + (s.error && (o += `, error: ${s.error}`), + s.error_description && (o += `, description: ${s.error_description}`)); + } catch (s) {} + } + _.newAuthErr({ authId: V, level: 'error', source: 'auth', message: o }); + }); + }; + function configureAuth(s) { + return { type: Ft, payload: s }; + } + function restoreAuthorization(s) { + return { type: qt, payload: s }; + } + const persistAuthorizationIfNeeded = + () => + ({ authSelectors: s, getConfigs: o }) => { + if (!o().persistAuthorization) return; + const i = s.authorized().toJS(); + localStorage.setItem('authorized', JSON.stringify(i)); + }, + authPopup = (s, o) => () => { + ((at.swaggerUIRedirectOauth2 = o), at.open(s)); + }, + $t = { + [Tt]: (s, { payload: o }) => s.set('showDefinitions', o), + [Nt]: (s, { payload: o }) => { + let i = (0, qe.fromJS)(o), + u = s.get('authorized') || (0, qe.Map)(); + return ( + i.entrySeq().forEach(([o, i]) => { + if (!isFunc(i.getIn)) return s.set('authorized', u); + let _ = i.getIn(['schema', 'type']); + if ('apiKey' === _ || 'http' === _) u = u.set(o, i); + else if ('basic' === _) { + let s = i.getIn(['value', 'username']), + _ = i.getIn(['value', 'password']); + ((u = u.setIn([o, 'value'], { + username: s, + header: 'Basic ' + utils_btoa(s + ':' + _) + })), + (u = u.setIn([o, 'schema'], i.get('schema')))); + } + }), + s.set('authorized', u) + ); + }, + [Lt]: (s, { payload: o }) => { + let i, + { auth: u, token: _ } = o; + ((u.token = Object.assign({}, _)), (i = (0, qe.fromJS)(u))); + let w = s.get('authorized') || (0, qe.Map)(); + return ((w = w.set(i.get('name'), i)), s.set('authorized', w)); + }, + [Rt]: (s, { payload: o }) => { + let i = s.get('authorized').withMutations((s) => { + o.forEach((o) => { + s.delete(o); + }); + }); + return s.set('authorized', i); + }, + [Ft]: (s, { payload: o }) => s.set('configs', o), + [qt]: (s, { payload: o }) => s.set('authorized', (0, qe.fromJS)(o.authorized)) + }; + function assertIsFunction(s, o = 'expected a function, instead received ' + typeof s) { + if ('function' != typeof s) throw new TypeError(o); + } + var ensureIsArray = (s) => (Array.isArray(s) ? s : [s]); + function getDependencies(s) { + const o = Array.isArray(s[0]) ? s[0] : s; + return ( + (function assertIsArrayOfFunctions( + s, + o = 'expected all items to be functions, instead received the following types: ' + ) { + if (!s.every((s) => 'function' == typeof s)) { + const i = s + .map((s) => + 'function' == typeof s ? `function ${s.name || 'unnamed'}()` : typeof s + ) + .join(', '); + throw new TypeError(`${o}[${i}]`); + } + })( + o, + 'createSelector expects all input-selectors to be functions, but received the following types: ' + ), + o + ); + } + (Symbol(), Object.getPrototypeOf({})); + var Vt = + 'undefined' != typeof WeakRef + ? WeakRef + : class { + constructor(s) { + this.value = s; + } + deref() { + return this.value; + } + }; + function weakMapMemoize(s, o = {}) { + let i = { s: 0, v: void 0, o: null, p: null }; + const { resultEqualityCheck: u } = o; + let _, + w = 0; + function memoized() { + let o = i; + const { length: x } = arguments; + for (let s = 0, i = x; s < i; s++) { + const i = arguments[s]; + if ('function' == typeof i || ('object' == typeof i && null !== i)) { + let s = o.o; + null === s && (o.o = s = new WeakMap()); + const u = s.get(i); + void 0 === u ? ((o = { s: 0, v: void 0, o: null, p: null }), s.set(i, o)) : (o = u); + } else { + let s = o.p; + null === s && (o.p = s = new Map()); + const u = s.get(i); + void 0 === u ? ((o = { s: 0, v: void 0, o: null, p: null }), s.set(i, o)) : (o = u); + } + } + const C = o; + let j; + if (1 === o.s) j = o.v; + else if (((j = s.apply(null, arguments)), w++, u)) { + const s = _?.deref?.() ?? _; + null != s && u(s, j) && ((j = s), 0 !== w && w--); + _ = ('object' == typeof j && null !== j) || 'function' == typeof j ? new Vt(j) : j; + } + return ((C.s = 1), (C.v = j), j); + } + return ( + (memoized.clearCache = () => { + ((i = { s: 0, v: void 0, o: null, p: null }), memoized.resetResultsCount()); + }), + (memoized.resultsCount = () => w), + (memoized.resetResultsCount = () => { + w = 0; + }), + memoized + ); + } + function createSelectorCreator(s, ...o) { + const i = 'function' == typeof s ? { memoize: s, memoizeOptions: o } : s, + createSelector2 = (...s) => { + let o, + u = 0, + _ = 0, + w = {}, + x = s.pop(); + ('object' == typeof x && ((w = x), (x = s.pop())), + assertIsFunction( + x, + `createSelector expects an output function after the inputs, but received: [${typeof x}]` + )); + const C = { ...i, ...w }, + { + memoize: j, + memoizeOptions: L = [], + argsMemoize: B = weakMapMemoize, + argsMemoizeOptions: $ = [], + devModeChecks: V = {} + } = C, + U = ensureIsArray(L), + z = ensureIsArray($), + Y = getDependencies(s), + Z = j( + function recomputationWrapper() { + return (u++, x.apply(null, arguments)); + }, + ...U + ); + const ee = B( + function dependenciesChecker() { + _++; + const s = (function collectInputSelectorResults(s, o) { + const i = [], + { length: u } = s; + for (let _ = 0; _ < u; _++) i.push(s[_].apply(null, o)); + return i; + })(Y, arguments); + return ((o = Z.apply(null, s)), o); + }, + ...z + ); + return Object.assign(ee, { + resultFunc: x, + memoizedResultFunc: Z, + dependencies: Y, + dependencyRecomputations: () => _, + resetDependencyRecomputations: () => { + _ = 0; + }, + lastResult: () => o, + recomputations: () => u, + resetRecomputations: () => { + u = 0; + }, + memoize: j, + argsMemoize: B + }); + }; + return ( + Object.assign(createSelector2, { withTypes: () => createSelector2 }), + createSelector2 + ); + } + var Ut = createSelectorCreator(weakMapMemoize), + zt = Object.assign( + (s, o = Ut) => { + !(function assertIsObject(s, o = 'expected an object, instead received ' + typeof s) { + if ('object' != typeof s) throw new TypeError(o); + })( + s, + 'createStructuredSelector expects first argument to be an object where each property is a selector, instead received a ' + + typeof s + ); + const i = Object.keys(s); + return o( + i.map((o) => s[o]), + (...s) => s.reduce((s, o, u) => ((s[i[u]] = o), s), {}) + ); + }, + { withTypes: () => zt } + ); + const state = (s) => s, + Wt = Ut(state, (s) => s.get('showDefinitions')), + Kt = Ut(state, () => ({ specSelectors: s }) => { + let o = s.securityDefinitions() || (0, qe.Map)({}), + i = (0, qe.List)(); + return ( + o.entrySeq().forEach(([s, o]) => { + let u = (0, qe.Map)(); + ((u = u.set(s, o)), (i = i.push(u))); + }), + i + ); + }), + getDefinitionsByNames = + (s, o) => + ({ specSelectors: s }) => { + console.warn( + 'WARNING: getDefinitionsByNames is deprecated and will be removed in the next major version.' + ); + let i = s.securityDefinitions(), + u = (0, qe.List)(); + return ( + o.valueSeq().forEach((s) => { + let o = (0, qe.Map)(); + (s.entrySeq().forEach(([s, u]) => { + let _, + w = i.get(s); + ('oauth2' === w.get('type') && + u.size && + ((_ = w.get('scopes')), + _.keySeq().forEach((s) => { + u.contains(s) || (_ = _.delete(s)); + }), + (w = w.set('allowedScopes', _))), + (o = o.set(s, w))); + }), + (u = u.push(o))); + }), + u + ); + }, + definitionsForRequirements = + (s, o = (0, qe.List)()) => + ({ authSelectors: s }) => { + const i = s.definitionsToAuthorize() || (0, qe.List)(); + let u = (0, qe.List)(); + return ( + i.forEach((s) => { + let i = o.find((o) => o.get(s.keySeq().first())); + i && + (s.forEach((o, u) => { + if ('oauth2' === o.get('type')) { + const _ = i.get(u); + let w = o.get('scopes'); + qe.List.isList(_) && + qe.Map.isMap(w) && + (w.keySeq().forEach((s) => { + _.contains(s) || (w = w.delete(s)); + }), + (s = s.set(u, o.set('scopes', w)))); + } + }), + (u = u.push(s))); + }), + u + ); + }, + Ht = Ut(state, (s) => s.get('authorized') || (0, qe.Map)()), + isAuthorized = + (s, o) => + ({ authSelectors: s }) => { + let i = s.authorized(); + return qe.List.isList(o) + ? !!o.toJS().filter( + (s) => + -1 === + Object.keys(s) + .map((s) => !!i.get(s)) + .indexOf(!1) + ).length + : null; + }, + Jt = Ut(state, (s) => s.get('configs')), + execute = + (s, { authSelectors: o, specSelectors: i }) => + ({ path: u, method: _, operation: w, extras: x }) => { + let C = { + authorized: o.authorized() && o.authorized().toJS(), + definitions: i.securityDefinitions() && i.securityDefinitions().toJS(), + specSecurity: i.security() && i.security().toJS() + }; + return s({ path: u, method: _, operation: w, securities: C, ...x }); + }, + loaded = (s, o) => (i) => { + const { getConfigs: u, authActions: _ } = o, + w = u(); + if ((s(i), w.persistAuthorization)) { + const s = localStorage.getItem('authorized'); + s && _.restoreAuthorization({ authorized: JSON.parse(s) }); + } + }, + wrap_actions_authorize = (s, o) => (i) => { + s(i); + if (o.getConfigs().persistAuthorization) + try { + const [{ schema: s, value: o }] = Object.values(i), + u = 'apiKey' === s.get('type'), + _ = 'cookie' === s.get('in'); + u && _ && (document.cookie = `${s.get('name')}=${o}; SameSite=None; Secure`); + } catch (s) { + console.error('Error persisting cookie based apiKey in document.cookie.', s); + } + }, + wrap_actions_logout = (s, o) => (i) => { + const u = o.getConfigs(), + _ = o.authSelectors.authorized(); + try { + u.persistAuthorization && + Array.isArray(i) && + i.forEach((s) => { + const o = _.get(s, {}), + i = 'apiKey' === o.getIn(['schema', 'type']), + u = 'cookie' === o.getIn(['schema', 'in']); + if (i && u) { + const s = o.getIn(['schema', 'name']); + document.cookie = `${s}=; Max-Age=-99999999`; + } + }); + } catch (s) { + console.error('Error deleting cookie based apiKey from document.cookie.', s); + } + s(i); + }; + var Gt = __webpack_require__(90179), + Yt = __webpack_require__.n(Gt); + class LockAuthIcon extends Pe.Component { + mapStateToProps(s, o) { + return { state: s, ownProps: Yt()(o, Object.keys(o.getSystem())) }; + } + render() { + const { getComponent: s, ownProps: o } = this.props, + i = s('LockIcon'); + return Pe.createElement(i, o); + } + } + const Xt = LockAuthIcon; + class UnlockAuthIcon extends Pe.Component { + mapStateToProps(s, o) { + return { state: s, ownProps: Yt()(o, Object.keys(o.getSystem())) }; + } + render() { + const { getComponent: s, ownProps: o } = this.props, + i = s('UnlockIcon'); + return Pe.createElement(i, o); + } + } + const Zt = UnlockAuthIcon; + function auth() { + return { + afterLoad(s) { + ((this.rootInjects = this.rootInjects || {}), + (this.rootInjects.initOAuth = s.authActions.configureAuth), + (this.rootInjects.preauthorizeApiKey = preauthorizeApiKey.bind(null, s)), + (this.rootInjects.preauthorizeBasic = preauthorizeBasic.bind(null, s))); + }, + components: { + LockAuthIcon: Xt, + UnlockAuthIcon: Zt, + LockAuthOperationIcon: Xt, + UnlockAuthOperationIcon: Zt + }, + statePlugins: { + auth: { + reducers: $t, + actions: o, + selectors: i, + wrapActions: { authorize: wrap_actions_authorize, logout: wrap_actions_logout } + }, + configs: { wrapActions: { loaded } }, + spec: { wrapActions: { execute } } + } + }; + } + function preauthorizeBasic(s, o, i, u) { + const { + authActions: { authorize: _ }, + specSelectors: { specJson: w, isOAS3: x } + } = s, + C = x() ? ['components', 'securitySchemes'] : ['securityDefinitions'], + j = w().getIn([...C, o]); + return j ? _({ [o]: { value: { username: i, password: u }, schema: j.toJS() } }) : null; + } + function preauthorizeApiKey(s, o, i) { + const { + authActions: { authorize: u }, + specSelectors: { specJson: _, isOAS3: w } + } = s, + x = w() ? ['components', 'securitySchemes'] : ['securityDefinitions'], + C = _().getIn([...x, o]); + return C ? u({ [o]: { value: i, schema: C.toJS() } }) : null; + } + function isNothing(s) { + return null == s; + } + var Qt = function repeat(s, o) { + var i, + u = ''; + for (i = 0; i < o; i += 1) u += s; + return u; + }, + er = function isNegativeZero(s) { + return 0 === s && Number.NEGATIVE_INFINITY === 1 / s; + }, + tr = { + isNothing, + isObject: function js_yaml_isObject(s) { + return 'object' == typeof s && null !== s; + }, + toArray: function toArray(s) { + return Array.isArray(s) ? s : isNothing(s) ? [] : [s]; + }, + repeat: Qt, + isNegativeZero: er, + extend: function extend(s, o) { + var i, u, _, w; + if (o) + for (i = 0, u = (w = Object.keys(o)).length; i < u; i += 1) s[(_ = w[i])] = o[_]; + return s; + } + }; + function formatError(s, o) { + var i = '', + u = s.reason || '(unknown reason)'; + return s.mark + ? (s.mark.name && (i += 'in "' + s.mark.name + '" '), + (i += '(' + (s.mark.line + 1) + ':' + (s.mark.column + 1) + ')'), + !o && s.mark.snippet && (i += '\n\n' + s.mark.snippet), + u + ' ' + i) + : u; + } + function YAMLException$1(s, o) { + (Error.call(this), + (this.name = 'YAMLException'), + (this.reason = s), + (this.mark = o), + (this.message = formatError(this, !1)), + Error.captureStackTrace + ? Error.captureStackTrace(this, this.constructor) + : (this.stack = new Error().stack || '')); + } + ((YAMLException$1.prototype = Object.create(Error.prototype)), + (YAMLException$1.prototype.constructor = YAMLException$1), + (YAMLException$1.prototype.toString = function toString(s) { + return this.name + ': ' + formatError(this, s); + })); + var rr = YAMLException$1; + function getLine(s, o, i, u, _) { + var w = '', + x = '', + C = Math.floor(_ / 2) - 1; + return ( + u - o > C && (o = u - C + (w = ' ... ').length), + i - u > C && (i = u + C - (x = ' ...').length), + { str: w + s.slice(o, i).replace(/\t/g, '→') + x, pos: u - o + w.length } + ); + } + function padStart(s, o) { + return tr.repeat(' ', o - s.length) + s; + } + var nr = function makeSnippet(s, o) { + if (((o = Object.create(o || null)), !s.buffer)) return null; + (o.maxLength || (o.maxLength = 79), + 'number' != typeof o.indent && (o.indent = 1), + 'number' != typeof o.linesBefore && (o.linesBefore = 3), + 'number' != typeof o.linesAfter && (o.linesAfter = 2)); + for (var i, u = /\r?\n|\r|\0/g, _ = [0], w = [], x = -1; (i = u.exec(s.buffer)); ) + (w.push(i.index), + _.push(i.index + i[0].length), + s.position <= i.index && x < 0 && (x = _.length - 2)); + x < 0 && (x = _.length - 1); + var C, + j, + L = '', + B = Math.min(s.line + o.linesAfter, w.length).toString().length, + $ = o.maxLength - (o.indent + B + 3); + for (C = 1; C <= o.linesBefore && !(x - C < 0); C++) + ((j = getLine(s.buffer, _[x - C], w[x - C], s.position - (_[x] - _[x - C]), $)), + (L = + tr.repeat(' ', o.indent) + + padStart((s.line - C + 1).toString(), B) + + ' | ' + + j.str + + '\n' + + L)); + for ( + j = getLine(s.buffer, _[x], w[x], s.position, $), + L += + tr.repeat(' ', o.indent) + + padStart((s.line + 1).toString(), B) + + ' | ' + + j.str + + '\n', + L += tr.repeat('-', o.indent + B + 3 + j.pos) + '^\n', + C = 1; + C <= o.linesAfter && !(x + C >= w.length); + C++ + ) + ((j = getLine(s.buffer, _[x + C], w[x + C], s.position - (_[x] - _[x + C]), $)), + (L += + tr.repeat(' ', o.indent) + + padStart((s.line + C + 1).toString(), B) + + ' | ' + + j.str + + '\n')); + return L.replace(/\n$/, ''); + }, + sr = [ + 'kind', + 'multi', + 'resolve', + 'construct', + 'instanceOf', + 'predicate', + 'represent', + 'representName', + 'defaultStyle', + 'styleAliases' + ], + ir = ['scalar', 'sequence', 'mapping']; + var ar = function Type$1(s, o) { + if ( + ((o = o || {}), + Object.keys(o).forEach(function (o) { + if (-1 === sr.indexOf(o)) + throw new rr( + 'Unknown option "' + o + '" is met in definition of "' + s + '" YAML type.' + ); + }), + (this.options = o), + (this.tag = s), + (this.kind = o.kind || null), + (this.resolve = + o.resolve || + function () { + return !0; + }), + (this.construct = + o.construct || + function (s) { + return s; + }), + (this.instanceOf = o.instanceOf || null), + (this.predicate = o.predicate || null), + (this.represent = o.represent || null), + (this.representName = o.representName || null), + (this.defaultStyle = o.defaultStyle || null), + (this.multi = o.multi || !1), + (this.styleAliases = (function compileStyleAliases(s) { + var o = {}; + return ( + null !== s && + Object.keys(s).forEach(function (i) { + s[i].forEach(function (s) { + o[String(s)] = i; + }); + }), + o + ); + })(o.styleAliases || null)), + -1 === ir.indexOf(this.kind)) + ) + throw new rr( + 'Unknown kind "' + this.kind + '" is specified for "' + s + '" YAML type.' + ); + }; + function compileList(s, o) { + var i = []; + return ( + s[o].forEach(function (s) { + var o = i.length; + (i.forEach(function (i, u) { + i.tag === s.tag && i.kind === s.kind && i.multi === s.multi && (o = u); + }), + (i[o] = s)); + }), + i + ); + } + function Schema$1(s) { + return this.extend(s); + } + Schema$1.prototype.extend = function extend(s) { + var o = [], + i = []; + if (s instanceof ar) i.push(s); + else if (Array.isArray(s)) i = i.concat(s); + else { + if (!s || (!Array.isArray(s.implicit) && !Array.isArray(s.explicit))) + throw new rr( + 'Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })' + ); + (s.implicit && (o = o.concat(s.implicit)), s.explicit && (i = i.concat(s.explicit))); + } + (o.forEach(function (s) { + if (!(s instanceof ar)) + throw new rr( + 'Specified list of YAML types (or a single Type object) contains a non-Type object.' + ); + if (s.loadKind && 'scalar' !== s.loadKind) + throw new rr( + 'There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.' + ); + if (s.multi) + throw new rr( + 'There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.' + ); + }), + i.forEach(function (s) { + if (!(s instanceof ar)) + throw new rr( + 'Specified list of YAML types (or a single Type object) contains a non-Type object.' + ); + })); + var u = Object.create(Schema$1.prototype); + return ( + (u.implicit = (this.implicit || []).concat(o)), + (u.explicit = (this.explicit || []).concat(i)), + (u.compiledImplicit = compileList(u, 'implicit')), + (u.compiledExplicit = compileList(u, 'explicit')), + (u.compiledTypeMap = (function compileMap() { + var s, + o, + i = { + scalar: {}, + sequence: {}, + mapping: {}, + fallback: {}, + multi: { scalar: [], sequence: [], mapping: [], fallback: [] } + }; + function collectType(s) { + s.multi + ? (i.multi[s.kind].push(s), i.multi.fallback.push(s)) + : (i[s.kind][s.tag] = i.fallback[s.tag] = s); + } + for (s = 0, o = arguments.length; s < o; s += 1) arguments[s].forEach(collectType); + return i; + })(u.compiledImplicit, u.compiledExplicit)), + u + ); + }; + var lr = Schema$1, + cr = new ar('tag:yaml.org,2002:str', { + kind: 'scalar', + construct: function (s) { + return null !== s ? s : ''; + } + }), + ur = new ar('tag:yaml.org,2002:seq', { + kind: 'sequence', + construct: function (s) { + return null !== s ? s : []; + } + }), + pr = new ar('tag:yaml.org,2002:map', { + kind: 'mapping', + construct: function (s) { + return null !== s ? s : {}; + } + }), + dr = new lr({ explicit: [cr, ur, pr] }); + var fr = new ar('tag:yaml.org,2002:null', { + kind: 'scalar', + resolve: function resolveYamlNull(s) { + if (null === s) return !0; + var o = s.length; + return ( + (1 === o && '~' === s) || (4 === o && ('null' === s || 'Null' === s || 'NULL' === s)) + ); + }, + construct: function constructYamlNull() { + return null; + }, + predicate: function isNull(s) { + return null === s; + }, + represent: { + canonical: function () { + return '~'; + }, + lowercase: function () { + return 'null'; + }, + uppercase: function () { + return 'NULL'; + }, + camelcase: function () { + return 'Null'; + }, + empty: function () { + return ''; + } + }, + defaultStyle: 'lowercase' + }); + var mr = new ar('tag:yaml.org,2002:bool', { + kind: 'scalar', + resolve: function resolveYamlBoolean(s) { + if (null === s) return !1; + var o = s.length; + return ( + (4 === o && ('true' === s || 'True' === s || 'TRUE' === s)) || + (5 === o && ('false' === s || 'False' === s || 'FALSE' === s)) + ); + }, + construct: function constructYamlBoolean(s) { + return 'true' === s || 'True' === s || 'TRUE' === s; + }, + predicate: function isBoolean(s) { + return '[object Boolean]' === Object.prototype.toString.call(s); + }, + represent: { + lowercase: function (s) { + return s ? 'true' : 'false'; + }, + uppercase: function (s) { + return s ? 'TRUE' : 'FALSE'; + }, + camelcase: function (s) { + return s ? 'True' : 'False'; + } + }, + defaultStyle: 'lowercase' + }); + function isOctCode(s) { + return 48 <= s && s <= 55; + } + function isDecCode(s) { + return 48 <= s && s <= 57; + } + var gr = new ar('tag:yaml.org,2002:int', { + kind: 'scalar', + resolve: function resolveYamlInteger(s) { + if (null === s) return !1; + var o, + i, + u = s.length, + _ = 0, + w = !1; + if (!u) return !1; + if ((('-' !== (o = s[_]) && '+' !== o) || (o = s[++_]), '0' === o)) { + if (_ + 1 === u) return !0; + if ('b' === (o = s[++_])) { + for (_++; _ < u; _++) + if ('_' !== (o = s[_])) { + if ('0' !== o && '1' !== o) return !1; + w = !0; + } + return w && '_' !== o; + } + if ('x' === o) { + for (_++; _ < u; _++) + if ('_' !== (o = s[_])) { + if ( + !( + (48 <= (i = s.charCodeAt(_)) && i <= 57) || + (65 <= i && i <= 70) || + (97 <= i && i <= 102) + ) + ) + return !1; + w = !0; + } + return w && '_' !== o; + } + if ('o' === o) { + for (_++; _ < u; _++) + if ('_' !== (o = s[_])) { + if (!isOctCode(s.charCodeAt(_))) return !1; + w = !0; + } + return w && '_' !== o; + } + } + if ('_' === o) return !1; + for (; _ < u; _++) + if ('_' !== (o = s[_])) { + if (!isDecCode(s.charCodeAt(_))) return !1; + w = !0; + } + return !(!w || '_' === o); + }, + construct: function constructYamlInteger(s) { + var o, + i = s, + u = 1; + if ( + (-1 !== i.indexOf('_') && (i = i.replace(/_/g, '')), + ('-' !== (o = i[0]) && '+' !== o) || + ('-' === o && (u = -1), (o = (i = i.slice(1))[0])), + '0' === i) + ) + return 0; + if ('0' === o) { + if ('b' === i[1]) return u * parseInt(i.slice(2), 2); + if ('x' === i[1]) return u * parseInt(i.slice(2), 16); + if ('o' === i[1]) return u * parseInt(i.slice(2), 8); + } + return u * parseInt(i, 10); + }, + predicate: function isInteger(s) { + return ( + '[object Number]' === Object.prototype.toString.call(s) && + s % 1 == 0 && + !tr.isNegativeZero(s) + ); + }, + represent: { + binary: function (s) { + return s >= 0 ? '0b' + s.toString(2) : '-0b' + s.toString(2).slice(1); + }, + octal: function (s) { + return s >= 0 ? '0o' + s.toString(8) : '-0o' + s.toString(8).slice(1); + }, + decimal: function (s) { + return s.toString(10); + }, + hexadecimal: function (s) { + return s >= 0 + ? '0x' + s.toString(16).toUpperCase() + : '-0x' + s.toString(16).toUpperCase().slice(1); + } + }, + defaultStyle: 'decimal', + styleAliases: { + binary: [2, 'bin'], + octal: [8, 'oct'], + decimal: [10, 'dec'], + hexadecimal: [16, 'hex'] + } + }), + yr = new RegExp( + '^(?:[-+]?(?:[0-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$' + ); + var vr = /^[-+]?[0-9]+e/; + var br = new ar('tag:yaml.org,2002:float', { + kind: 'scalar', + resolve: function resolveYamlFloat(s) { + return null !== s && !(!yr.test(s) || '_' === s[s.length - 1]); + }, + construct: function constructYamlFloat(s) { + var o, i; + return ( + (i = '-' === (o = s.replace(/_/g, '').toLowerCase())[0] ? -1 : 1), + '+-'.indexOf(o[0]) >= 0 && (o = o.slice(1)), + '.inf' === o + ? 1 === i + ? Number.POSITIVE_INFINITY + : Number.NEGATIVE_INFINITY + : '.nan' === o + ? NaN + : i * parseFloat(o, 10) + ); + }, + predicate: function isFloat(s) { + return ( + '[object Number]' === Object.prototype.toString.call(s) && + (s % 1 != 0 || tr.isNegativeZero(s)) + ); + }, + represent: function representYamlFloat(s, o) { + var i; + if (isNaN(s)) + switch (o) { + case 'lowercase': + return '.nan'; + case 'uppercase': + return '.NAN'; + case 'camelcase': + return '.NaN'; + } + else if (Number.POSITIVE_INFINITY === s) + switch (o) { + case 'lowercase': + return '.inf'; + case 'uppercase': + return '.INF'; + case 'camelcase': + return '.Inf'; + } + else if (Number.NEGATIVE_INFINITY === s) + switch (o) { + case 'lowercase': + return '-.inf'; + case 'uppercase': + return '-.INF'; + case 'camelcase': + return '-.Inf'; + } + else if (tr.isNegativeZero(s)) return '-0.0'; + return ((i = s.toString(10)), vr.test(i) ? i.replace('e', '.e') : i); + }, + defaultStyle: 'lowercase' + }), + _r = dr.extend({ implicit: [fr, mr, gr, br] }), + Er = _r, + wr = new RegExp('^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$'), + Sr = new RegExp( + '^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$' + ); + var xr = new ar('tag:yaml.org,2002:timestamp', { + kind: 'scalar', + resolve: function resolveYamlTimestamp(s) { + return null !== s && (null !== wr.exec(s) || null !== Sr.exec(s)); + }, + construct: function constructYamlTimestamp(s) { + var o, + i, + u, + _, + w, + x, + C, + j, + L = 0, + B = null; + if ((null === (o = wr.exec(s)) && (o = Sr.exec(s)), null === o)) + throw new Error('Date resolve error'); + if (((i = +o[1]), (u = +o[2] - 1), (_ = +o[3]), !o[4])) + return new Date(Date.UTC(i, u, _)); + if (((w = +o[4]), (x = +o[5]), (C = +o[6]), o[7])) { + for (L = o[7].slice(0, 3); L.length < 3; ) L += '0'; + L = +L; + } + return ( + o[9] && ((B = 6e4 * (60 * +o[10] + +(o[11] || 0))), '-' === o[9] && (B = -B)), + (j = new Date(Date.UTC(i, u, _, w, x, C, L))), + B && j.setTime(j.getTime() - B), + j + ); + }, + instanceOf: Date, + represent: function representYamlTimestamp(s) { + return s.toISOString(); + } + }); + var kr = new ar('tag:yaml.org,2002:merge', { + kind: 'scalar', + resolve: function resolveYamlMerge(s) { + return '<<' === s || null === s; + } + }), + Cr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r'; + var Or = new ar('tag:yaml.org,2002:binary', { + kind: 'scalar', + resolve: function resolveYamlBinary(s) { + if (null === s) return !1; + var o, + i, + u = 0, + _ = s.length, + w = Cr; + for (i = 0; i < _; i++) + if (!((o = w.indexOf(s.charAt(i))) > 64)) { + if (o < 0) return !1; + u += 6; + } + return u % 8 == 0; + }, + construct: function constructYamlBinary(s) { + var o, + i, + u = s.replace(/[\r\n=]/g, ''), + _ = u.length, + w = Cr, + x = 0, + C = []; + for (o = 0; o < _; o++) + (o % 4 == 0 && + o && + (C.push((x >> 16) & 255), C.push((x >> 8) & 255), C.push(255 & x)), + (x = (x << 6) | w.indexOf(u.charAt(o)))); + return ( + 0 === (i = (_ % 4) * 6) + ? (C.push((x >> 16) & 255), C.push((x >> 8) & 255), C.push(255 & x)) + : 18 === i + ? (C.push((x >> 10) & 255), C.push((x >> 2) & 255)) + : 12 === i && C.push((x >> 4) & 255), + new Uint8Array(C) + ); + }, + predicate: function isBinary(s) { + return '[object Uint8Array]' === Object.prototype.toString.call(s); + }, + represent: function representYamlBinary(s) { + var o, + i, + u = '', + _ = 0, + w = s.length, + x = Cr; + for (o = 0; o < w; o++) + (o % 3 == 0 && + o && + ((u += x[(_ >> 18) & 63]), + (u += x[(_ >> 12) & 63]), + (u += x[(_ >> 6) & 63]), + (u += x[63 & _])), + (_ = (_ << 8) + s[o])); + return ( + 0 === (i = w % 3) + ? ((u += x[(_ >> 18) & 63]), + (u += x[(_ >> 12) & 63]), + (u += x[(_ >> 6) & 63]), + (u += x[63 & _])) + : 2 === i + ? ((u += x[(_ >> 10) & 63]), + (u += x[(_ >> 4) & 63]), + (u += x[(_ << 2) & 63]), + (u += x[64])) + : 1 === i && + ((u += x[(_ >> 2) & 63]), + (u += x[(_ << 4) & 63]), + (u += x[64]), + (u += x[64])), + u + ); + } + }), + Ar = Object.prototype.hasOwnProperty, + jr = Object.prototype.toString; + var Ir = new ar('tag:yaml.org,2002:omap', { + kind: 'sequence', + resolve: function resolveYamlOmap(s) { + if (null === s) return !0; + var o, + i, + u, + _, + w, + x = [], + C = s; + for (o = 0, i = C.length; o < i; o += 1) { + if (((u = C[o]), (w = !1), '[object Object]' !== jr.call(u))) return !1; + for (_ in u) + if (Ar.call(u, _)) { + if (w) return !1; + w = !0; + } + if (!w) return !1; + if (-1 !== x.indexOf(_)) return !1; + x.push(_); + } + return !0; + }, + construct: function constructYamlOmap(s) { + return null !== s ? s : []; + } + }), + Pr = Object.prototype.toString; + var Mr = new ar('tag:yaml.org,2002:pairs', { + kind: 'sequence', + resolve: function resolveYamlPairs(s) { + if (null === s) return !0; + var o, + i, + u, + _, + w, + x = s; + for (w = new Array(x.length), o = 0, i = x.length; o < i; o += 1) { + if (((u = x[o]), '[object Object]' !== Pr.call(u))) return !1; + if (1 !== (_ = Object.keys(u)).length) return !1; + w[o] = [_[0], u[_[0]]]; + } + return !0; + }, + construct: function constructYamlPairs(s) { + if (null === s) return []; + var o, + i, + u, + _, + w, + x = s; + for (w = new Array(x.length), o = 0, i = x.length; o < i; o += 1) + ((u = x[o]), (_ = Object.keys(u)), (w[o] = [_[0], u[_[0]]])); + return w; + } + }), + Tr = Object.prototype.hasOwnProperty; + var Nr = new ar('tag:yaml.org,2002:set', { + kind: 'mapping', + resolve: function resolveYamlSet(s) { + if (null === s) return !0; + var o, + i = s; + for (o in i) if (Tr.call(i, o) && null !== i[o]) return !1; + return !0; + }, + construct: function constructYamlSet(s) { + return null !== s ? s : {}; + } + }), + Rr = Er.extend({ implicit: [xr, kr], explicit: [Or, Ir, Mr, Nr] }), + Dr = Object.prototype.hasOwnProperty, + Lr = + /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/, + Br = /[\x85\u2028\u2029]/, + Fr = /[,\[\]\{\}]/, + qr = /^(?:!|!!|![a-z\-]+!)$/i, + $r = /^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/i; + function _class(s) { + return Object.prototype.toString.call(s); + } + function is_EOL(s) { + return 10 === s || 13 === s; + } + function is_WHITE_SPACE(s) { + return 9 === s || 32 === s; + } + function is_WS_OR_EOL(s) { + return 9 === s || 32 === s || 10 === s || 13 === s; + } + function is_FLOW_INDICATOR(s) { + return 44 === s || 91 === s || 93 === s || 123 === s || 125 === s; + } + function fromHexCode(s) { + var o; + return 48 <= s && s <= 57 ? s - 48 : 97 <= (o = 32 | s) && o <= 102 ? o - 97 + 10 : -1; + } + function simpleEscapeSequence(s) { + return 48 === s + ? '\0' + : 97 === s + ? '' + : 98 === s + ? '\b' + : 116 === s || 9 === s + ? '\t' + : 110 === s + ? '\n' + : 118 === s + ? '\v' + : 102 === s + ? '\f' + : 114 === s + ? '\r' + : 101 === s + ? '' + : 32 === s + ? ' ' + : 34 === s + ? '"' + : 47 === s + ? '/' + : 92 === s + ? '\\' + : 78 === s + ? '…' + : 95 === s + ? ' ' + : 76 === s + ? '\u2028' + : 80 === s + ? '\u2029' + : ''; + } + function charFromCodepoint(s) { + return s <= 65535 + ? String.fromCharCode(s) + : String.fromCharCode(55296 + ((s - 65536) >> 10), 56320 + ((s - 65536) & 1023)); + } + for (var Vr = new Array(256), Ur = new Array(256), zr = 0; zr < 256; zr++) + ((Vr[zr] = simpleEscapeSequence(zr) ? 1 : 0), (Ur[zr] = simpleEscapeSequence(zr))); + function State$1(s, o) { + ((this.input = s), + (this.filename = o.filename || null), + (this.schema = o.schema || Rr), + (this.onWarning = o.onWarning || null), + (this.legacy = o.legacy || !1), + (this.json = o.json || !1), + (this.listener = o.listener || null), + (this.implicitTypes = this.schema.compiledImplicit), + (this.typeMap = this.schema.compiledTypeMap), + (this.length = s.length), + (this.position = 0), + (this.line = 0), + (this.lineStart = 0), + (this.lineIndent = 0), + (this.firstTabInLine = -1), + (this.documents = [])); + } + function generateError(s, o) { + var i = { + name: s.filename, + buffer: s.input.slice(0, -1), + position: s.position, + line: s.line, + column: s.position - s.lineStart + }; + return ((i.snippet = nr(i)), new rr(o, i)); + } + function throwError(s, o) { + throw generateError(s, o); + } + function throwWarning(s, o) { + s.onWarning && s.onWarning.call(null, generateError(s, o)); + } + var Wr = { + YAML: function handleYamlDirective(s, o, i) { + var u, _, w; + (null !== s.version && throwError(s, 'duplication of %YAML directive'), + 1 !== i.length && throwError(s, 'YAML directive accepts exactly one argument'), + null === (u = /^([0-9]+)\.([0-9]+)$/.exec(i[0])) && + throwError(s, 'ill-formed argument of the YAML directive'), + (_ = parseInt(u[1], 10)), + (w = parseInt(u[2], 10)), + 1 !== _ && throwError(s, 'unacceptable YAML version of the document'), + (s.version = i[0]), + (s.checkLineBreaks = w < 2), + 1 !== w && 2 !== w && throwWarning(s, 'unsupported YAML version of the document')); + }, + TAG: function handleTagDirective(s, o, i) { + var u, _; + (2 !== i.length && throwError(s, 'TAG directive accepts exactly two arguments'), + (u = i[0]), + (_ = i[1]), + qr.test(u) || + throwError(s, 'ill-formed tag handle (first argument) of the TAG directive'), + Dr.call(s.tagMap, u) && + throwError(s, 'there is a previously declared suffix for "' + u + '" tag handle'), + $r.test(_) || + throwError(s, 'ill-formed tag prefix (second argument) of the TAG directive')); + try { + _ = decodeURIComponent(_); + } catch (o) { + throwError(s, 'tag prefix is malformed: ' + _); + } + s.tagMap[u] = _; + } + }; + function captureSegment(s, o, i, u) { + var _, w, x, C; + if (o < i) { + if (((C = s.input.slice(o, i)), u)) + for (_ = 0, w = C.length; _ < w; _ += 1) + 9 === (x = C.charCodeAt(_)) || + (32 <= x && x <= 1114111) || + throwError(s, 'expected valid JSON character'); + else Lr.test(C) && throwError(s, 'the stream contains non-printable characters'); + s.result += C; + } + } + function mergeMappings(s, o, i, u) { + var _, w, x, C; + for ( + tr.isObject(i) || + throwError(s, 'cannot merge mappings; the provided source object is unacceptable'), + x = 0, + C = (_ = Object.keys(i)).length; + x < C; + x += 1 + ) + ((w = _[x]), Dr.call(o, w) || ((o[w] = i[w]), (u[w] = !0))); + } + function storeMappingPair(s, o, i, u, _, w, x, C, j) { + var L, B; + if (Array.isArray(_)) + for (L = 0, B = (_ = Array.prototype.slice.call(_)).length; L < B; L += 1) + (Array.isArray(_[L]) && throwError(s, 'nested arrays are not supported inside keys'), + 'object' == typeof _ && + '[object Object]' === _class(_[L]) && + (_[L] = '[object Object]')); + if ( + ('object' == typeof _ && '[object Object]' === _class(_) && (_ = '[object Object]'), + (_ = String(_)), + null === o && (o = {}), + 'tag:yaml.org,2002:merge' === u) + ) + if (Array.isArray(w)) + for (L = 0, B = w.length; L < B; L += 1) mergeMappings(s, o, w[L], i); + else mergeMappings(s, o, w, i); + else + (s.json || + Dr.call(i, _) || + !Dr.call(o, _) || + ((s.line = x || s.line), + (s.lineStart = C || s.lineStart), + (s.position = j || s.position), + throwError(s, 'duplicated mapping key')), + '__proto__' === _ + ? Object.defineProperty(o, _, { + configurable: !0, + enumerable: !0, + writable: !0, + value: w + }) + : (o[_] = w), + delete i[_]); + return o; + } + function readLineBreak(s) { + var o; + (10 === (o = s.input.charCodeAt(s.position)) + ? s.position++ + : 13 === o + ? (s.position++, 10 === s.input.charCodeAt(s.position) && s.position++) + : throwError(s, 'a line break is expected'), + (s.line += 1), + (s.lineStart = s.position), + (s.firstTabInLine = -1)); + } + function skipSeparationSpace(s, o, i) { + for (var u = 0, _ = s.input.charCodeAt(s.position); 0 !== _; ) { + for (; is_WHITE_SPACE(_); ) + (9 === _ && -1 === s.firstTabInLine && (s.firstTabInLine = s.position), + (_ = s.input.charCodeAt(++s.position))); + if (o && 35 === _) + do { + _ = s.input.charCodeAt(++s.position); + } while (10 !== _ && 13 !== _ && 0 !== _); + if (!is_EOL(_)) break; + for ( + readLineBreak(s), _ = s.input.charCodeAt(s.position), u++, s.lineIndent = 0; + 32 === _; + ) + (s.lineIndent++, (_ = s.input.charCodeAt(++s.position))); + } + return ( + -1 !== i && 0 !== u && s.lineIndent < i && throwWarning(s, 'deficient indentation'), + u + ); + } + function testDocumentSeparator(s) { + var o, + i = s.position; + return !( + (45 !== (o = s.input.charCodeAt(i)) && 46 !== o) || + o !== s.input.charCodeAt(i + 1) || + o !== s.input.charCodeAt(i + 2) || + ((i += 3), 0 !== (o = s.input.charCodeAt(i)) && !is_WS_OR_EOL(o)) + ); + } + function writeFoldedLines(s, o) { + 1 === o ? (s.result += ' ') : o > 1 && (s.result += tr.repeat('\n', o - 1)); + } + function readBlockSequence(s, o) { + var i, + u, + _ = s.tag, + w = s.anchor, + x = [], + C = !1; + if (-1 !== s.firstTabInLine) return !1; + for ( + null !== s.anchor && (s.anchorMap[s.anchor] = x), u = s.input.charCodeAt(s.position); + 0 !== u && + (-1 !== s.firstTabInLine && + ((s.position = s.firstTabInLine), + throwError(s, 'tab characters must not be used in indentation')), + 45 === u) && + is_WS_OR_EOL(s.input.charCodeAt(s.position + 1)); + ) + if (((C = !0), s.position++, skipSeparationSpace(s, !0, -1) && s.lineIndent <= o)) + (x.push(null), (u = s.input.charCodeAt(s.position))); + else if ( + ((i = s.line), + composeNode(s, o, 3, !1, !0), + x.push(s.result), + skipSeparationSpace(s, !0, -1), + (u = s.input.charCodeAt(s.position)), + (s.line === i || s.lineIndent > o) && 0 !== u) + ) + throwError(s, 'bad indentation of a sequence entry'); + else if (s.lineIndent < o) break; + return !!C && ((s.tag = _), (s.anchor = w), (s.kind = 'sequence'), (s.result = x), !0); + } + function readTagProperty(s) { + var o, + i, + u, + _, + w = !1, + x = !1; + if (33 !== (_ = s.input.charCodeAt(s.position))) return !1; + if ( + (null !== s.tag && throwError(s, 'duplication of a tag property'), + 60 === (_ = s.input.charCodeAt(++s.position)) + ? ((w = !0), (_ = s.input.charCodeAt(++s.position))) + : 33 === _ + ? ((x = !0), (i = '!!'), (_ = s.input.charCodeAt(++s.position))) + : (i = '!'), + (o = s.position), + w) + ) { + do { + _ = s.input.charCodeAt(++s.position); + } while (0 !== _ && 62 !== _); + s.position < s.length + ? ((u = s.input.slice(o, s.position)), (_ = s.input.charCodeAt(++s.position))) + : throwError(s, 'unexpected end of the stream within a verbatim tag'); + } else { + for (; 0 !== _ && !is_WS_OR_EOL(_); ) + (33 === _ && + (x + ? throwError(s, 'tag suffix cannot contain exclamation marks') + : ((i = s.input.slice(o - 1, s.position + 1)), + qr.test(i) || throwError(s, 'named tag handle cannot contain such characters'), + (x = !0), + (o = s.position + 1))), + (_ = s.input.charCodeAt(++s.position))); + ((u = s.input.slice(o, s.position)), + Fr.test(u) && throwError(s, 'tag suffix cannot contain flow indicator characters')); + } + u && !$r.test(u) && throwError(s, 'tag name cannot contain such characters: ' + u); + try { + u = decodeURIComponent(u); + } catch (o) { + throwError(s, 'tag name is malformed: ' + u); + } + return ( + w + ? (s.tag = u) + : Dr.call(s.tagMap, i) + ? (s.tag = s.tagMap[i] + u) + : '!' === i + ? (s.tag = '!' + u) + : '!!' === i + ? (s.tag = 'tag:yaml.org,2002:' + u) + : throwError(s, 'undeclared tag handle "' + i + '"'), + !0 + ); + } + function readAnchorProperty(s) { + var o, i; + if (38 !== (i = s.input.charCodeAt(s.position))) return !1; + for ( + null !== s.anchor && throwError(s, 'duplication of an anchor property'), + i = s.input.charCodeAt(++s.position), + o = s.position; + 0 !== i && !is_WS_OR_EOL(i) && !is_FLOW_INDICATOR(i); + ) + i = s.input.charCodeAt(++s.position); + return ( + s.position === o && + throwError(s, 'name of an anchor node must contain at least one character'), + (s.anchor = s.input.slice(o, s.position)), + !0 + ); + } + function composeNode(s, o, i, u, _) { + var w, + x, + C, + j, + L, + B, + $, + V, + U, + z = 1, + Y = !1, + Z = !1; + if ( + (null !== s.listener && s.listener('open', s), + (s.tag = null), + (s.anchor = null), + (s.kind = null), + (s.result = null), + (w = x = C = 4 === i || 3 === i), + u && + skipSeparationSpace(s, !0, -1) && + ((Y = !0), + s.lineIndent > o + ? (z = 1) + : s.lineIndent === o + ? (z = 0) + : s.lineIndent < o && (z = -1)), + 1 === z) + ) + for (; readTagProperty(s) || readAnchorProperty(s); ) + skipSeparationSpace(s, !0, -1) + ? ((Y = !0), + (C = w), + s.lineIndent > o + ? (z = 1) + : s.lineIndent === o + ? (z = 0) + : s.lineIndent < o && (z = -1)) + : (C = !1); + if ( + (C && (C = Y || _), + (1 !== z && 4 !== i) || + ((V = 1 === i || 2 === i ? o : o + 1), + (U = s.position - s.lineStart), + 1 === z + ? (C && + (readBlockSequence(s, U) || + (function readBlockMapping(s, o, i) { + var u, + _, + w, + x, + C, + j, + L, + B = s.tag, + $ = s.anchor, + V = {}, + U = Object.create(null), + z = null, + Y = null, + Z = null, + ee = !1, + ie = !1; + if (-1 !== s.firstTabInLine) return !1; + for ( + null !== s.anchor && (s.anchorMap[s.anchor] = V), + L = s.input.charCodeAt(s.position); + 0 !== L; + ) { + if ( + (ee || + -1 === s.firstTabInLine || + ((s.position = s.firstTabInLine), + throwError(s, 'tab characters must not be used in indentation')), + (u = s.input.charCodeAt(s.position + 1)), + (w = s.line), + (63 !== L && 58 !== L) || !is_WS_OR_EOL(u)) + ) { + if ( + ((x = s.line), + (C = s.lineStart), + (j = s.position), + !composeNode(s, i, 2, !1, !0)) + ) + break; + if (s.line === w) { + for (L = s.input.charCodeAt(s.position); is_WHITE_SPACE(L); ) + L = s.input.charCodeAt(++s.position); + if (58 === L) + (is_WS_OR_EOL((L = s.input.charCodeAt(++s.position))) || + throwError( + s, + 'a whitespace character is expected after the key-value separator within a block mapping' + ), + ee && + (storeMappingPair(s, V, U, z, Y, null, x, C, j), + (z = Y = Z = null)), + (ie = !0), + (ee = !1), + (_ = !1), + (z = s.tag), + (Y = s.result)); + else { + if (!ie) return ((s.tag = B), (s.anchor = $), !0); + throwError( + s, + 'can not read an implicit mapping pair; a colon is missed' + ); + } + } else { + if (!ie) return ((s.tag = B), (s.anchor = $), !0); + throwError( + s, + 'can not read a block mapping entry; a multiline key may not be an implicit key' + ); + } + } else + (63 === L + ? (ee && + (storeMappingPair(s, V, U, z, Y, null, x, C, j), + (z = Y = Z = null)), + (ie = !0), + (ee = !0), + (_ = !0)) + : ee + ? ((ee = !1), (_ = !0)) + : throwError( + s, + 'incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line' + ), + (s.position += 1), + (L = u)); + if ( + ((s.line === w || s.lineIndent > o) && + (ee && ((x = s.line), (C = s.lineStart), (j = s.position)), + composeNode(s, o, 4, !0, _) && (ee ? (Y = s.result) : (Z = s.result)), + ee || + (storeMappingPair(s, V, U, z, Y, Z, x, C, j), (z = Y = Z = null)), + skipSeparationSpace(s, !0, -1), + (L = s.input.charCodeAt(s.position))), + (s.line === w || s.lineIndent > o) && 0 !== L) + ) + throwError(s, 'bad indentation of a mapping entry'); + else if (s.lineIndent < o) break; + } + return ( + ee && storeMappingPair(s, V, U, z, Y, null, x, C, j), + ie && ((s.tag = B), (s.anchor = $), (s.kind = 'mapping'), (s.result = V)), + ie + ); + })(s, U, V))) || + (function readFlowCollection(s, o) { + var i, + u, + _, + w, + x, + C, + j, + L, + B, + $, + V, + U, + z = !0, + Y = s.tag, + Z = s.anchor, + ee = Object.create(null); + if (91 === (U = s.input.charCodeAt(s.position))) ((x = 93), (L = !1), (w = [])); + else { + if (123 !== U) return !1; + ((x = 125), (L = !0), (w = {})); + } + for ( + null !== s.anchor && (s.anchorMap[s.anchor] = w), + U = s.input.charCodeAt(++s.position); + 0 !== U; + ) { + if ( + (skipSeparationSpace(s, !0, o), (U = s.input.charCodeAt(s.position)) === x) + ) + return ( + s.position++, + (s.tag = Y), + (s.anchor = Z), + (s.kind = L ? 'mapping' : 'sequence'), + (s.result = w), + !0 + ); + (z + ? 44 === U && throwError(s, "expected the node content, but found ','") + : throwError(s, 'missed comma between flow collection entries'), + (V = null), + (C = j = !1), + 63 === U && + is_WS_OR_EOL(s.input.charCodeAt(s.position + 1)) && + ((C = j = !0), s.position++, skipSeparationSpace(s, !0, o)), + (i = s.line), + (u = s.lineStart), + (_ = s.position), + composeNode(s, o, 1, !1, !0), + ($ = s.tag), + (B = s.result), + skipSeparationSpace(s, !0, o), + (U = s.input.charCodeAt(s.position)), + (!j && s.line !== i) || + 58 !== U || + ((C = !0), + (U = s.input.charCodeAt(++s.position)), + skipSeparationSpace(s, !0, o), + composeNode(s, o, 1, !1, !0), + (V = s.result)), + L + ? storeMappingPair(s, w, ee, $, B, V, i, u, _) + : C + ? w.push(storeMappingPair(s, null, ee, $, B, V, i, u, _)) + : w.push(B), + skipSeparationSpace(s, !0, o), + 44 === (U = s.input.charCodeAt(s.position)) + ? ((z = !0), (U = s.input.charCodeAt(++s.position))) + : (z = !1)); + } + throwError(s, 'unexpected end of the stream within a flow collection'); + })(s, V) + ? (Z = !0) + : ((x && + (function readBlockScalar(s, o) { + var i, + u, + _, + w, + x, + C = 1, + j = !1, + L = !1, + B = o, + $ = 0, + V = !1; + if (124 === (w = s.input.charCodeAt(s.position))) u = !1; + else { + if (62 !== w) return !1; + u = !0; + } + for (s.kind = 'scalar', s.result = ''; 0 !== w; ) + if (43 === (w = s.input.charCodeAt(++s.position)) || 45 === w) + 1 === C + ? (C = 43 === w ? 3 : 2) + : throwError(s, 'repeat of a chomping mode identifier'); + else { + if (!((_ = 48 <= (x = w) && x <= 57 ? x - 48 : -1) >= 0)) break; + 0 === _ + ? throwError( + s, + 'bad explicit indentation width of a block scalar; it cannot be less than one' + ) + : L + ? throwError(s, 'repeat of an indentation width identifier') + : ((B = o + _ - 1), (L = !0)); + } + if (is_WHITE_SPACE(w)) { + do { + w = s.input.charCodeAt(++s.position); + } while (is_WHITE_SPACE(w)); + if (35 === w) + do { + w = s.input.charCodeAt(++s.position); + } while (!is_EOL(w) && 0 !== w); + } + for (; 0 !== w; ) { + for ( + readLineBreak(s), s.lineIndent = 0, w = s.input.charCodeAt(s.position); + (!L || s.lineIndent < B) && 32 === w; + ) + (s.lineIndent++, (w = s.input.charCodeAt(++s.position))); + if ((!L && s.lineIndent > B && (B = s.lineIndent), is_EOL(w))) $++; + else { + if (s.lineIndent < B) { + 3 === C + ? (s.result += tr.repeat('\n', j ? 1 + $ : $)) + : 1 === C && j && (s.result += '\n'); + break; + } + for ( + u + ? is_WHITE_SPACE(w) + ? ((V = !0), (s.result += tr.repeat('\n', j ? 1 + $ : $))) + : V + ? ((V = !1), (s.result += tr.repeat('\n', $ + 1))) + : 0 === $ + ? j && (s.result += ' ') + : (s.result += tr.repeat('\n', $)) + : (s.result += tr.repeat('\n', j ? 1 + $ : $)), + j = !0, + L = !0, + $ = 0, + i = s.position; + !is_EOL(w) && 0 !== w; + ) + w = s.input.charCodeAt(++s.position); + captureSegment(s, i, s.position, !1); + } + } + return !0; + })(s, V)) || + (function readSingleQuotedScalar(s, o) { + var i, u, _; + if (39 !== (i = s.input.charCodeAt(s.position))) return !1; + for ( + s.kind = 'scalar', s.result = '', s.position++, u = _ = s.position; + 0 !== (i = s.input.charCodeAt(s.position)); + ) + if (39 === i) { + if ( + (captureSegment(s, u, s.position, !0), + 39 !== (i = s.input.charCodeAt(++s.position))) + ) + return !0; + ((u = s.position), s.position++, (_ = s.position)); + } else + is_EOL(i) + ? (captureSegment(s, u, _, !0), + writeFoldedLines(s, skipSeparationSpace(s, !1, o)), + (u = _ = s.position)) + : s.position === s.lineStart && testDocumentSeparator(s) + ? throwError( + s, + 'unexpected end of the document within a single quoted scalar' + ) + : (s.position++, (_ = s.position)); + throwError(s, 'unexpected end of the stream within a single quoted scalar'); + })(s, V) || + (function readDoubleQuotedScalar(s, o) { + var i, u, _, w, x, C, j; + if (34 !== (C = s.input.charCodeAt(s.position))) return !1; + for ( + s.kind = 'scalar', s.result = '', s.position++, i = u = s.position; + 0 !== (C = s.input.charCodeAt(s.position)); + ) { + if (34 === C) + return (captureSegment(s, i, s.position, !0), s.position++, !0); + if (92 === C) { + if ( + (captureSegment(s, i, s.position, !0), + is_EOL((C = s.input.charCodeAt(++s.position)))) + ) + skipSeparationSpace(s, !1, o); + else if (C < 256 && Vr[C]) ((s.result += Ur[C]), s.position++); + else if ( + (x = 120 === (j = C) ? 2 : 117 === j ? 4 : 85 === j ? 8 : 0) > 0 + ) { + for (_ = x, w = 0; _ > 0; _--) + (x = fromHexCode((C = s.input.charCodeAt(++s.position)))) >= 0 + ? (w = (w << 4) + x) + : throwError(s, 'expected hexadecimal character'); + ((s.result += charFromCodepoint(w)), s.position++); + } else throwError(s, 'unknown escape sequence'); + i = u = s.position; + } else + is_EOL(C) + ? (captureSegment(s, i, u, !0), + writeFoldedLines(s, skipSeparationSpace(s, !1, o)), + (i = u = s.position)) + : s.position === s.lineStart && testDocumentSeparator(s) + ? throwError( + s, + 'unexpected end of the document within a double quoted scalar' + ) + : (s.position++, (u = s.position)); + } + throwError(s, 'unexpected end of the stream within a double quoted scalar'); + })(s, V) + ? (Z = !0) + : !(function readAlias(s) { + var o, i, u; + if (42 !== (u = s.input.charCodeAt(s.position))) return !1; + for ( + u = s.input.charCodeAt(++s.position), o = s.position; + 0 !== u && !is_WS_OR_EOL(u) && !is_FLOW_INDICATOR(u); + ) + u = s.input.charCodeAt(++s.position); + return ( + s.position === o && + throwError( + s, + 'name of an alias node must contain at least one character' + ), + (i = s.input.slice(o, s.position)), + Dr.call(s.anchorMap, i) || + throwError(s, 'unidentified alias "' + i + '"'), + (s.result = s.anchorMap[i]), + skipSeparationSpace(s, !0, -1), + !0 + ); + })(s) + ? (function readPlainScalar(s, o, i) { + var u, + _, + w, + x, + C, + j, + L, + B, + $ = s.kind, + V = s.result; + if ( + is_WS_OR_EOL((B = s.input.charCodeAt(s.position))) || + is_FLOW_INDICATOR(B) || + 35 === B || + 38 === B || + 42 === B || + 33 === B || + 124 === B || + 62 === B || + 39 === B || + 34 === B || + 37 === B || + 64 === B || + 96 === B + ) + return !1; + if ( + (63 === B || 45 === B) && + (is_WS_OR_EOL((u = s.input.charCodeAt(s.position + 1))) || + (i && is_FLOW_INDICATOR(u))) + ) + return !1; + for ( + s.kind = 'scalar', s.result = '', _ = w = s.position, x = !1; + 0 !== B; + ) { + if (58 === B) { + if ( + is_WS_OR_EOL((u = s.input.charCodeAt(s.position + 1))) || + (i && is_FLOW_INDICATOR(u)) + ) + break; + } else if (35 === B) { + if (is_WS_OR_EOL(s.input.charCodeAt(s.position - 1))) break; + } else { + if ( + (s.position === s.lineStart && testDocumentSeparator(s)) || + (i && is_FLOW_INDICATOR(B)) + ) + break; + if (is_EOL(B)) { + if ( + ((C = s.line), + (j = s.lineStart), + (L = s.lineIndent), + skipSeparationSpace(s, !1, -1), + s.lineIndent >= o) + ) { + ((x = !0), (B = s.input.charCodeAt(s.position))); + continue; + } + ((s.position = w), + (s.line = C), + (s.lineStart = j), + (s.lineIndent = L)); + break; + } + } + (x && + (captureSegment(s, _, w, !1), + writeFoldedLines(s, s.line - C), + (_ = w = s.position), + (x = !1)), + is_WHITE_SPACE(B) || (w = s.position + 1), + (B = s.input.charCodeAt(++s.position))); + } + return ( + captureSegment(s, _, w, !1), + !!s.result || ((s.kind = $), (s.result = V), !1) + ); + })(s, V, 1 === i) && ((Z = !0), null === s.tag && (s.tag = '?')) + : ((Z = !0), + (null === s.tag && null === s.anchor) || + throwError(s, 'alias node should not have any properties')), + null !== s.anchor && (s.anchorMap[s.anchor] = s.result)) + : 0 === z && (Z = C && readBlockSequence(s, U))), + null === s.tag) + ) + null !== s.anchor && (s.anchorMap[s.anchor] = s.result); + else if ('?' === s.tag) { + for ( + null !== s.result && + 'scalar' !== s.kind && + throwError( + s, + 'unacceptable node kind for ! tag; it should be "scalar", not "' + s.kind + '"' + ), + j = 0, + L = s.implicitTypes.length; + j < L; + j += 1 + ) + if (($ = s.implicitTypes[j]).resolve(s.result)) { + ((s.result = $.construct(s.result)), + (s.tag = $.tag), + null !== s.anchor && (s.anchorMap[s.anchor] = s.result)); + break; + } + } else if ('!' !== s.tag) { + if (Dr.call(s.typeMap[s.kind || 'fallback'], s.tag)) + $ = s.typeMap[s.kind || 'fallback'][s.tag]; + else + for ( + $ = null, j = 0, L = (B = s.typeMap.multi[s.kind || 'fallback']).length; + j < L; + j += 1 + ) + if (s.tag.slice(0, B[j].tag.length) === B[j].tag) { + $ = B[j]; + break; + } + ($ || throwError(s, 'unknown tag !<' + s.tag + '>'), + null !== s.result && + $.kind !== s.kind && + throwError( + s, + 'unacceptable node kind for !<' + + s.tag + + '> tag; it should be "' + + $.kind + + '", not "' + + s.kind + + '"' + ), + $.resolve(s.result, s.tag) + ? ((s.result = $.construct(s.result, s.tag)), + null !== s.anchor && (s.anchorMap[s.anchor] = s.result)) + : throwError(s, 'cannot resolve a node with !<' + s.tag + '> explicit tag')); + } + return ( + null !== s.listener && s.listener('close', s), + null !== s.tag || null !== s.anchor || Z + ); + } + function readDocument(s) { + var o, + i, + u, + _, + w = s.position, + x = !1; + for ( + s.version = null, + s.checkLineBreaks = s.legacy, + s.tagMap = Object.create(null), + s.anchorMap = Object.create(null); + 0 !== (_ = s.input.charCodeAt(s.position)) && + (skipSeparationSpace(s, !0, -1), + (_ = s.input.charCodeAt(s.position)), + !(s.lineIndent > 0 || 37 !== _)); + ) { + for ( + x = !0, _ = s.input.charCodeAt(++s.position), o = s.position; + 0 !== _ && !is_WS_OR_EOL(_); + ) + _ = s.input.charCodeAt(++s.position); + for ( + u = [], + (i = s.input.slice(o, s.position)).length < 1 && + throwError(s, 'directive name must not be less than one character in length'); + 0 !== _; + ) { + for (; is_WHITE_SPACE(_); ) _ = s.input.charCodeAt(++s.position); + if (35 === _) { + do { + _ = s.input.charCodeAt(++s.position); + } while (0 !== _ && !is_EOL(_)); + break; + } + if (is_EOL(_)) break; + for (o = s.position; 0 !== _ && !is_WS_OR_EOL(_); ) + _ = s.input.charCodeAt(++s.position); + u.push(s.input.slice(o, s.position)); + } + (0 !== _ && readLineBreak(s), + Dr.call(Wr, i) + ? Wr[i](s, i, u) + : throwWarning(s, 'unknown document directive "' + i + '"')); + } + (skipSeparationSpace(s, !0, -1), + 0 === s.lineIndent && + 45 === s.input.charCodeAt(s.position) && + 45 === s.input.charCodeAt(s.position + 1) && + 45 === s.input.charCodeAt(s.position + 2) + ? ((s.position += 3), skipSeparationSpace(s, !0, -1)) + : x && throwError(s, 'directives end mark is expected'), + composeNode(s, s.lineIndent - 1, 4, !1, !0), + skipSeparationSpace(s, !0, -1), + s.checkLineBreaks && + Br.test(s.input.slice(w, s.position)) && + throwWarning(s, 'non-ASCII line breaks are interpreted as content'), + s.documents.push(s.result), + s.position === s.lineStart && testDocumentSeparator(s) + ? 46 === s.input.charCodeAt(s.position) && + ((s.position += 3), skipSeparationSpace(s, !0, -1)) + : s.position < s.length - 1 && + throwError(s, 'end of the stream or a document separator is expected')); + } + function loadDocuments(s, o) { + ((o = o || {}), + 0 !== (s = String(s)).length && + (10 !== s.charCodeAt(s.length - 1) && + 13 !== s.charCodeAt(s.length - 1) && + (s += '\n'), + 65279 === s.charCodeAt(0) && (s = s.slice(1)))); + var i = new State$1(s, o), + u = s.indexOf('\0'); + for ( + -1 !== u && ((i.position = u), throwError(i, 'null byte is not allowed in input')), + i.input += '\0'; + 32 === i.input.charCodeAt(i.position); + ) + ((i.lineIndent += 1), (i.position += 1)); + for (; i.position < i.length - 1; ) readDocument(i); + return i.documents; + } + var Kr = { + loadAll: function loadAll$1(s, o, i) { + null !== o && 'object' == typeof o && void 0 === i && ((i = o), (o = null)); + var u = loadDocuments(s, i); + if ('function' != typeof o) return u; + for (var _ = 0, w = u.length; _ < w; _ += 1) o(u[_]); + }, + load: function load$1(s, o) { + var i = loadDocuments(s, o); + if (0 !== i.length) { + if (1 === i.length) return i[0]; + throw new rr('expected a single document in the stream, but found more'); + } + } + }, + Hr = Object.prototype.toString, + Jr = Object.prototype.hasOwnProperty, + Gr = 65279, + Yr = { + 0: '\\0', + 7: '\\a', + 8: '\\b', + 9: '\\t', + 10: '\\n', + 11: '\\v', + 12: '\\f', + 13: '\\r', + 27: '\\e', + 34: '\\"', + 92: '\\\\', + 133: '\\N', + 160: '\\_', + 8232: '\\L', + 8233: '\\P' + }, + Xr = [ + 'y', + 'Y', + 'yes', + 'Yes', + 'YES', + 'on', + 'On', + 'ON', + 'n', + 'N', + 'no', + 'No', + 'NO', + 'off', + 'Off', + 'OFF' + ], + Zr = /^[-+]?[0-9_]+(?::[0-9_]+)+(?:\.[0-9_]*)?$/; + function encodeHex(s) { + var o, i, u; + if (((o = s.toString(16).toUpperCase()), s <= 255)) ((i = 'x'), (u = 2)); + else if (s <= 65535) ((i = 'u'), (u = 4)); + else { + if (!(s <= 4294967295)) + throw new rr('code point within a string may not be greater than 0xFFFFFFFF'); + ((i = 'U'), (u = 8)); + } + return '\\' + i + tr.repeat('0', u - o.length) + o; + } + function State(s) { + ((this.schema = s.schema || Rr), + (this.indent = Math.max(1, s.indent || 2)), + (this.noArrayIndent = s.noArrayIndent || !1), + (this.skipInvalid = s.skipInvalid || !1), + (this.flowLevel = tr.isNothing(s.flowLevel) ? -1 : s.flowLevel), + (this.styleMap = (function compileStyleMap(s, o) { + var i, u, _, w, x, C, j; + if (null === o) return {}; + for (i = {}, _ = 0, w = (u = Object.keys(o)).length; _ < w; _ += 1) + ((x = u[_]), + (C = String(o[x])), + '!!' === x.slice(0, 2) && (x = 'tag:yaml.org,2002:' + x.slice(2)), + (j = s.compiledTypeMap.fallback[x]) && + Jr.call(j.styleAliases, C) && + (C = j.styleAliases[C]), + (i[x] = C)); + return i; + })(this.schema, s.styles || null)), + (this.sortKeys = s.sortKeys || !1), + (this.lineWidth = s.lineWidth || 80), + (this.noRefs = s.noRefs || !1), + (this.noCompatMode = s.noCompatMode || !1), + (this.condenseFlow = s.condenseFlow || !1), + (this.quotingType = '"' === s.quotingType ? 2 : 1), + (this.forceQuotes = s.forceQuotes || !1), + (this.replacer = 'function' == typeof s.replacer ? s.replacer : null), + (this.implicitTypes = this.schema.compiledImplicit), + (this.explicitTypes = this.schema.compiledExplicit), + (this.tag = null), + (this.result = ''), + (this.duplicates = []), + (this.usedDuplicates = null)); + } + function indentString(s, o) { + for (var i, u = tr.repeat(' ', o), _ = 0, w = -1, x = '', C = s.length; _ < C; ) + (-1 === (w = s.indexOf('\n', _)) + ? ((i = s.slice(_)), (_ = C)) + : ((i = s.slice(_, w + 1)), (_ = w + 1)), + i.length && '\n' !== i && (x += u), + (x += i)); + return x; + } + function generateNextLine(s, o) { + return '\n' + tr.repeat(' ', s.indent * o); + } + function isWhitespace(s) { + return 32 === s || 9 === s; + } + function isPrintable(s) { + return ( + (32 <= s && s <= 126) || + (161 <= s && s <= 55295 && 8232 !== s && 8233 !== s) || + (57344 <= s && s <= 65533 && s !== Gr) || + (65536 <= s && s <= 1114111) + ); + } + function isNsCharOrWhitespace(s) { + return isPrintable(s) && s !== Gr && 13 !== s && 10 !== s; + } + function isPlainSafe(s, o, i) { + var u = isNsCharOrWhitespace(s), + _ = u && !isWhitespace(s); + return ( + ((i ? u : u && 44 !== s && 91 !== s && 93 !== s && 123 !== s && 125 !== s) && + 35 !== s && + !(58 === o && !_)) || + (isNsCharOrWhitespace(o) && !isWhitespace(o) && 35 === s) || + (58 === o && _) + ); + } + function codePointAt(s, o) { + var i, + u = s.charCodeAt(o); + return u >= 55296 && + u <= 56319 && + o + 1 < s.length && + (i = s.charCodeAt(o + 1)) >= 56320 && + i <= 57343 + ? 1024 * (u - 55296) + i - 56320 + 65536 + : u; + } + function needIndentIndicator(s) { + return /^\n* /.test(s); + } + function chooseScalarStyle(s, o, i, u, _, w, x, C) { + var j, + L = 0, + B = null, + $ = !1, + V = !1, + U = -1 !== u, + z = -1, + Y = + (function isPlainSafeFirst(s) { + return ( + isPrintable(s) && + s !== Gr && + !isWhitespace(s) && + 45 !== s && + 63 !== s && + 58 !== s && + 44 !== s && + 91 !== s && + 93 !== s && + 123 !== s && + 125 !== s && + 35 !== s && + 38 !== s && + 42 !== s && + 33 !== s && + 124 !== s && + 61 !== s && + 62 !== s && + 39 !== s && + 34 !== s && + 37 !== s && + 64 !== s && + 96 !== s + ); + })(codePointAt(s, 0)) && + (function isPlainSafeLast(s) { + return !isWhitespace(s) && 58 !== s; + })(codePointAt(s, s.length - 1)); + if (o || x) + for (j = 0; j < s.length; L >= 65536 ? (j += 2) : j++) { + if (!isPrintable((L = codePointAt(s, j)))) return 5; + ((Y = Y && isPlainSafe(L, B, C)), (B = L)); + } + else { + for (j = 0; j < s.length; L >= 65536 ? (j += 2) : j++) { + if (10 === (L = codePointAt(s, j))) + (($ = !0), U && ((V = V || (j - z - 1 > u && ' ' !== s[z + 1])), (z = j))); + else if (!isPrintable(L)) return 5; + ((Y = Y && isPlainSafe(L, B, C)), (B = L)); + } + V = V || (U && j - z - 1 > u && ' ' !== s[z + 1]); + } + return $ || V + ? i > 9 && needIndentIndicator(s) + ? 5 + : x + ? 2 === w + ? 5 + : 2 + : V + ? 4 + : 3 + : !Y || x || _(s) + ? 2 === w + ? 5 + : 2 + : 1; + } + function writeScalar(s, o, i, u, _) { + s.dump = (function () { + if (0 === o.length) return 2 === s.quotingType ? '""' : "''"; + if (!s.noCompatMode && (-1 !== Xr.indexOf(o) || Zr.test(o))) + return 2 === s.quotingType ? '"' + o + '"' : "'" + o + "'"; + var w = s.indent * Math.max(1, i), + x = -1 === s.lineWidth ? -1 : Math.max(Math.min(s.lineWidth, 40), s.lineWidth - w), + C = u || (s.flowLevel > -1 && i >= s.flowLevel); + switch ( + chooseScalarStyle( + o, + C, + s.indent, + x, + function testAmbiguity(o) { + return (function testImplicitResolving(s, o) { + var i, u; + for (i = 0, u = s.implicitTypes.length; i < u; i += 1) + if (s.implicitTypes[i].resolve(o)) return !0; + return !1; + })(s, o); + }, + s.quotingType, + s.forceQuotes && !u, + _ + ) + ) { + case 1: + return o; + case 2: + return "'" + o.replace(/'/g, "''") + "'"; + case 3: + return '|' + blockHeader(o, s.indent) + dropEndingNewline(indentString(o, w)); + case 4: + return ( + '>' + + blockHeader(o, s.indent) + + dropEndingNewline( + indentString( + (function foldString(s, o) { + var i, + u, + _ = /(\n+)([^\n]*)/g, + w = + ((C = s.indexOf('\n')), + (C = -1 !== C ? C : s.length), + (_.lastIndex = C), + foldLine(s.slice(0, C), o)), + x = '\n' === s[0] || ' ' === s[0]; + var C; + for (; (u = _.exec(s)); ) { + var j = u[1], + L = u[2]; + ((i = ' ' === L[0]), + (w += j + (x || i || '' === L ? '' : '\n') + foldLine(L, o)), + (x = i)); + } + return w; + })(o, x), + w + ) + ) + ); + case 5: + return ( + '"' + + (function escapeString(s) { + for (var o, i = '', u = 0, _ = 0; _ < s.length; u >= 65536 ? (_ += 2) : _++) + ((u = codePointAt(s, _)), + !(o = Yr[u]) && isPrintable(u) + ? ((i += s[_]), u >= 65536 && (i += s[_ + 1])) + : (i += o || encodeHex(u))); + return i; + })(o) + + '"' + ); + default: + throw new rr('impossible error: invalid scalar style'); + } + })(); + } + function blockHeader(s, o) { + var i = needIndentIndicator(s) ? String(o) : '', + u = '\n' === s[s.length - 1]; + return i + (u && ('\n' === s[s.length - 2] || '\n' === s) ? '+' : u ? '' : '-') + '\n'; + } + function dropEndingNewline(s) { + return '\n' === s[s.length - 1] ? s.slice(0, -1) : s; + } + function foldLine(s, o) { + if ('' === s || ' ' === s[0]) return s; + for (var i, u, _ = / [^ ]/g, w = 0, x = 0, C = 0, j = ''; (i = _.exec(s)); ) + ((C = i.index) - w > o && + ((u = x > w ? x : C), (j += '\n' + s.slice(w, u)), (w = u + 1)), + (x = C)); + return ( + (j += '\n'), + s.length - w > o && x > w + ? (j += s.slice(w, x) + '\n' + s.slice(x + 1)) + : (j += s.slice(w)), + j.slice(1) + ); + } + function writeBlockSequence(s, o, i, u) { + var _, + w, + x, + C = '', + j = s.tag; + for (_ = 0, w = i.length; _ < w; _ += 1) + ((x = i[_]), + s.replacer && (x = s.replacer.call(i, String(_), x)), + (writeNode(s, o + 1, x, !0, !0, !1, !0) || + (void 0 === x && writeNode(s, o + 1, null, !0, !0, !1, !0))) && + ((u && '' === C) || (C += generateNextLine(s, o)), + s.dump && 10 === s.dump.charCodeAt(0) ? (C += '-') : (C += '- '), + (C += s.dump))); + ((s.tag = j), (s.dump = C || '[]')); + } + function detectType(s, o, i) { + var u, _, w, x, C, j; + for (w = 0, x = (_ = i ? s.explicitTypes : s.implicitTypes).length; w < x; w += 1) + if ( + ((C = _[w]).instanceOf || C.predicate) && + (!C.instanceOf || ('object' == typeof o && o instanceof C.instanceOf)) && + (!C.predicate || C.predicate(o)) + ) { + if ( + (i + ? C.multi && C.representName + ? (s.tag = C.representName(o)) + : (s.tag = C.tag) + : (s.tag = '?'), + C.represent) + ) { + if ( + ((j = s.styleMap[C.tag] || C.defaultStyle), + '[object Function]' === Hr.call(C.represent)) + ) + u = C.represent(o, j); + else { + if (!Jr.call(C.represent, j)) + throw new rr('!<' + C.tag + '> tag resolver accepts not "' + j + '" style'); + u = C.represent[j](o, j); + } + s.dump = u; + } + return !0; + } + return !1; + } + function writeNode(s, o, i, u, _, w, x) { + ((s.tag = null), (s.dump = i), detectType(s, i, !1) || detectType(s, i, !0)); + var C, + j = Hr.call(s.dump), + L = u; + u && (u = s.flowLevel < 0 || s.flowLevel > o); + var B, + $, + V = '[object Object]' === j || '[object Array]' === j; + if ( + (V && ($ = -1 !== (B = s.duplicates.indexOf(i))), + ((null !== s.tag && '?' !== s.tag) || $ || (2 !== s.indent && o > 0)) && (_ = !1), + $ && s.usedDuplicates[B]) + ) + s.dump = '*ref_' + B; + else { + if ( + (V && $ && !s.usedDuplicates[B] && (s.usedDuplicates[B] = !0), + '[object Object]' === j) + ) + u && 0 !== Object.keys(s.dump).length + ? (!(function writeBlockMapping(s, o, i, u) { + var _, + w, + x, + C, + j, + L, + B = '', + $ = s.tag, + V = Object.keys(i); + if (!0 === s.sortKeys) V.sort(); + else if ('function' == typeof s.sortKeys) V.sort(s.sortKeys); + else if (s.sortKeys) throw new rr('sortKeys must be a boolean or a function'); + for (_ = 0, w = V.length; _ < w; _ += 1) + ((L = ''), + (u && '' === B) || (L += generateNextLine(s, o)), + (C = i[(x = V[_])]), + s.replacer && (C = s.replacer.call(i, x, C)), + writeNode(s, o + 1, x, !0, !0, !0) && + ((j = + (null !== s.tag && '?' !== s.tag) || + (s.dump && s.dump.length > 1024)) && + (s.dump && 10 === s.dump.charCodeAt(0) ? (L += '?') : (L += '? ')), + (L += s.dump), + j && (L += generateNextLine(s, o)), + writeNode(s, o + 1, C, !0, j) && + (s.dump && 10 === s.dump.charCodeAt(0) ? (L += ':') : (L += ': '), + (B += L += s.dump)))); + ((s.tag = $), (s.dump = B || '{}')); + })(s, o, s.dump, _), + $ && (s.dump = '&ref_' + B + s.dump)) + : (!(function writeFlowMapping(s, o, i) { + var u, + _, + w, + x, + C, + j = '', + L = s.tag, + B = Object.keys(i); + for (u = 0, _ = B.length; u < _; u += 1) + ((C = ''), + '' !== j && (C += ', '), + s.condenseFlow && (C += '"'), + (x = i[(w = B[u])]), + s.replacer && (x = s.replacer.call(i, w, x)), + writeNode(s, o, w, !1, !1) && + (s.dump.length > 1024 && (C += '? '), + (C += + s.dump + + (s.condenseFlow ? '"' : '') + + ':' + + (s.condenseFlow ? '' : ' ')), + writeNode(s, o, x, !1, !1) && (j += C += s.dump))); + ((s.tag = L), (s.dump = '{' + j + '}')); + })(s, o, s.dump), + $ && (s.dump = '&ref_' + B + ' ' + s.dump)); + else if ('[object Array]' === j) + u && 0 !== s.dump.length + ? (s.noArrayIndent && !x && o > 0 + ? writeBlockSequence(s, o - 1, s.dump, _) + : writeBlockSequence(s, o, s.dump, _), + $ && (s.dump = '&ref_' + B + s.dump)) + : (!(function writeFlowSequence(s, o, i) { + var u, + _, + w, + x = '', + C = s.tag; + for (u = 0, _ = i.length; u < _; u += 1) + ((w = i[u]), + s.replacer && (w = s.replacer.call(i, String(u), w)), + (writeNode(s, o, w, !1, !1) || + (void 0 === w && writeNode(s, o, null, !1, !1))) && + ('' !== x && (x += ',' + (s.condenseFlow ? '' : ' ')), (x += s.dump))); + ((s.tag = C), (s.dump = '[' + x + ']')); + })(s, o, s.dump), + $ && (s.dump = '&ref_' + B + ' ' + s.dump)); + else { + if ('[object String]' !== j) { + if ('[object Undefined]' === j) return !1; + if (s.skipInvalid) return !1; + throw new rr('unacceptable kind of an object to dump ' + j); + } + '?' !== s.tag && writeScalar(s, s.dump, o, w, L); + } + null !== s.tag && + '?' !== s.tag && + ((C = encodeURI('!' === s.tag[0] ? s.tag.slice(1) : s.tag).replace(/!/g, '%21')), + (C = + '!' === s.tag[0] + ? '!' + C + : 'tag:yaml.org,2002:' === C.slice(0, 18) + ? '!!' + C.slice(18) + : '!<' + C + '>'), + (s.dump = C + ' ' + s.dump)); + } + return !0; + } + function getDuplicateReferences(s, o) { + var i, + u, + _ = [], + w = []; + for (inspectNode(s, _, w), i = 0, u = w.length; i < u; i += 1) o.duplicates.push(_[w[i]]); + o.usedDuplicates = new Array(u); + } + function inspectNode(s, o, i) { + var u, _, w; + if (null !== s && 'object' == typeof s) + if (-1 !== (_ = o.indexOf(s))) -1 === i.indexOf(_) && i.push(_); + else if ((o.push(s), Array.isArray(s))) + for (_ = 0, w = s.length; _ < w; _ += 1) inspectNode(s[_], o, i); + else + for (_ = 0, w = (u = Object.keys(s)).length; _ < w; _ += 1) + inspectNode(s[u[_]], o, i); + } + var Qr = function dump$1(s, o) { + var i = new State((o = o || {})); + i.noRefs || getDuplicateReferences(s, i); + var u = s; + return ( + i.replacer && (u = i.replacer.call({ '': u }, '', u)), + writeNode(i, 0, u, !0, !0) ? i.dump + '\n' : '' + ); + }; + function renamed(s, o) { + return function () { + throw new Error( + 'Function yaml.' + + s + + ' is removed in js-yaml 4. Use yaml.' + + o + + ' instead, which is now safe by default.' + ); + }; + } + var en = ar, + tn = lr, + rn = dr, + nn = _r, + sn = Er, + on = Rr, + an = Kr.load, + ln = Kr.loadAll, + cn = { dump: Qr }.dump, + un = rr, + pn = { + binary: Or, + float: br, + map: pr, + null: fr, + pairs: Mr, + set: Nr, + timestamp: xr, + bool: mr, + int: gr, + merge: kr, + omap: Ir, + seq: ur, + str: cr + }, + hn = renamed('safeLoad', 'load'), + dn = renamed('safeLoadAll', 'loadAll'), + fn = renamed('safeDump', 'dump'); + const mn = { + Type: en, + Schema: tn, + FAILSAFE_SCHEMA: rn, + JSON_SCHEMA: nn, + CORE_SCHEMA: sn, + DEFAULT_SCHEMA: on, + load: an, + loadAll: ln, + dump: cn, + YAMLException: un, + types: pn, + safeLoad: hn, + safeLoadAll: dn, + safeDump: fn + }, + gn = 'configs_update', + yn = 'configs_toggle'; + function update(s, o) { + return { type: gn, payload: { [s]: o } }; + } + function toggle(s) { + return { type: yn, payload: s }; + } + const actions_loaded = () => () => {}, + downloadConfig = (s) => (o) => { + const { + fn: { fetch: i } + } = o; + return i(s); + }, + getConfigByUrl = (s, o) => (i) => { + const { specActions: u, configsActions: _ } = i; + if (s) return _.downloadConfig(s).then(next, next); + function next(_) { + _ instanceof Error || _.status >= 400 + ? (u.updateLoadingStatus('failedConfig'), + u.updateLoadingStatus('failedConfig'), + u.updateUrl(''), + console.error(_.statusText + ' ' + s.url), + o(null)) + : o( + ((s, o) => { + try { + return mn.load(s); + } catch (s) { + return (o && o.errActions.newThrownErr(new Error(s)), {}); + } + })(_.text, i) + ); + } + }, + get = (s, o) => s.getIn(Array.isArray(o) ? o : [o]), + vn = { + [gn]: (s, o) => s.merge((0, qe.fromJS)(o.payload)), + [yn]: (s, o) => { + const i = o.payload, + u = s.get(i); + return s.set(i, !u); + } + }; + function configsPlugin() { + return { statePlugins: { configs: { reducers: vn, actions: u, selectors: w } } }; + } + const setHash = (s) => + s ? history.pushState(null, null, `#${s}`) : (window.location.hash = ''); + var bn = __webpack_require__(86215), + _n = __webpack_require__.n(bn); + const En = 'layout_scroll_to', + wn = 'layout_clear_scroll'; + const Sn = { + fn: { + getScrollParent: function getScrollParent(s, o) { + const i = document.documentElement; + let u = getComputedStyle(s); + const _ = 'absolute' === u.position, + w = o ? /(auto|scroll|hidden)/ : /(auto|scroll)/; + if ('fixed' === u.position) return i; + for (let o = s; (o = o.parentElement); ) + if ( + ((u = getComputedStyle(o)), + (!_ || 'static' !== u.position) && w.test(u.overflow + u.overflowY + u.overflowX)) + ) + return o; + return i; + } + }, + statePlugins: { + layout: { + actions: { + scrollToElement: (s, o) => (i) => { + try { + ((o = o || i.fn.getScrollParent(s)), _n().createScroller(o).to(s)); + } catch (s) { + console.error(s); + } + }, + scrollTo: (s) => ({ type: En, payload: Array.isArray(s) ? s : [s] }), + clearScrollTo: () => ({ type: wn }), + readyToScroll: (s, o) => (i) => { + const u = i.layoutSelectors.getScrollToKey(); + $e().is(u, (0, qe.fromJS)(s)) && + (i.layoutActions.scrollToElement(o), i.layoutActions.clearScrollTo()); + }, + parseDeepLinkHash: + (s) => + ({ layoutActions: o, layoutSelectors: i, getConfigs: u }) => { + if (u().deepLinking && s) { + let u = s.slice(1); + ('!' === u[0] && (u = u.slice(1)), '/' === u[0] && (u = u.slice(1))); + const _ = u.split('/').map((s) => s || ''), + w = i.isShownKeyFromUrlHashArray(_), + [x, C = '', j = ''] = w; + if ('operations' === x) { + const s = i.isShownKeyFromUrlHashArray([C]); + (C.indexOf('_') > -1 && + (console.warn( + 'Warning: escaping deep link whitespace with `_` will be unsupported in v4.0, use `%20` instead.' + ), + o.show( + s.map((s) => s.replace(/_/g, ' ')), + !0 + )), + o.show(s, !0)); + } + ((C.indexOf('_') > -1 || j.indexOf('_') > -1) && + (console.warn( + 'Warning: escaping deep link whitespace with `_` will be unsupported in v4.0, use `%20` instead.' + ), + o.show( + w.map((s) => s.replace(/_/g, ' ')), + !0 + )), + o.show(w, !0), + o.scrollTo(w)); + } + } + }, + selectors: { + getScrollToKey: (s) => s.get('scrollToKey'), + isShownKeyFromUrlHashArray(s, o) { + const [i, u] = o; + return u ? ['operations', i, u] : i ? ['operations-tag', i] : []; + }, + urlHashArrayFromIsShownKey(s, o) { + let [i, u, _] = o; + return 'operations' == i ? [u, _] : 'operations-tag' == i ? [u] : []; + } + }, + reducers: { + [En]: (s, o) => s.set('scrollToKey', $e().fromJS(o.payload)), + [wn]: (s) => s.delete('scrollToKey') + }, + wrapActions: { + show: + (s, { getConfigs: o, layoutSelectors: i }) => + (...u) => { + if ((s(...u), o().deepLinking)) + try { + let [s, o] = u; + s = Array.isArray(s) ? s : [s]; + const _ = i.urlHashArrayFromIsShownKey(s); + if (!_.length) return; + const [w, x] = _; + if (!o) return setHash('/'); + 2 === _.length + ? setHash( + createDeepLinkPath( + `/${encodeURIComponent(w)}/${encodeURIComponent(x)}` + ) + ) + : 1 === _.length && + setHash(createDeepLinkPath(`/${encodeURIComponent(w)}`)); + } catch (s) { + console.error(s); + } + } + } + } + } + }; + var xn = __webpack_require__(2209), + kn = __webpack_require__.n(xn); + const operation_wrapper = (s, o) => + class OperationWrapper extends Pe.Component { + onLoad = (s) => { + const { operation: i } = this.props, + { tag: u, operationId: _ } = i.toObject(); + let { isShownKey: w } = i.toObject(); + ((w = w || ['operations', u, _]), o.layoutActions.readyToScroll(w, s)); + }; + render() { + return Pe.createElement( + 'span', + { ref: this.onLoad }, + Pe.createElement(s, this.props) + ); + } + }, + operation_tag_wrapper = (s, o) => + class OperationTagWrapper extends Pe.Component { + onLoad = (s) => { + const { tag: i } = this.props, + u = ['operations-tag', i]; + o.layoutActions.readyToScroll(u, s); + }; + render() { + return Pe.createElement( + 'span', + { ref: this.onLoad }, + Pe.createElement(s, this.props) + ); + } + }; + function deep_linking() { + return [ + Sn, + { + statePlugins: { + configs: { + wrapActions: { + loaded: + (s, o) => + (...i) => { + s(...i); + const u = decodeURIComponent(window.location.hash); + o.layoutActions.parseDeepLinkHash(u); + } + } + } + }, + wrapComponents: { operation: operation_wrapper, OperationTag: operation_tag_wrapper } + } + ]; + } + var Cn = __webpack_require__(40860), + On = __webpack_require__.n(Cn); + function transform(s) { + return s.map((s) => { + let o = 'is not of a type(s)', + i = s.get('message').indexOf(o); + if (i > -1) { + let o = s + .get('message') + .slice(i + 19) + .split(','); + return s.set( + 'message', + s.get('message').slice(0, i) + + (function makeNewMessage(s) { + return s.reduce( + (s, o, i, u) => + i === u.length - 1 && u.length > 1 + ? s + 'or ' + o + : u[i + 1] && u.length > 2 + ? s + o + ', ' + : u[i + 1] + ? s + o + ' ' + : s + o, + 'should be a' + ); + })(o) + ); + } + return s; + }); + } + var An = __webpack_require__(58156), + jn = __webpack_require__.n(An); + function parameter_oneof_transform(s, { jsSpec: o }) { + return s; + } + const In = [x, C]; + function transformErrors(s) { + let o = { jsSpec: {} }, + i = On()( + In, + (s, i) => { + try { + return i.transform(s, o).filter((s) => !!s); + } catch (o) { + return (console.error('Transformer error:', o), s); + } + }, + s + ); + return i.filter((s) => !!s).map((s) => (!s.get('line') && s.get('path'), s)); + } + let Pn = { line: 0, level: 'error', message: 'Unknown error' }; + const Mn = Ut( + (s) => s, + (s) => s.get('errors', (0, qe.List)()) + ), + Tn = Ut(Mn, (s) => s.last()); + function err(o) { + return { + statePlugins: { + err: { + reducers: { + [et]: (s, { payload: o }) => { + let i = Object.assign(Pn, o, { type: 'thrown' }); + return s + .update('errors', (s) => (s || (0, qe.List)()).push((0, qe.fromJS)(i))) + .update('errors', (s) => transformErrors(s)); + }, + [tt]: (s, { payload: o }) => ( + (o = o.map((s) => (0, qe.fromJS)(Object.assign(Pn, s, { type: 'thrown' })))), + s + .update('errors', (s) => (s || (0, qe.List)()).concat((0, qe.fromJS)(o))) + .update('errors', (s) => transformErrors(s)) + ), + [rt]: (s, { payload: o }) => { + let i = (0, qe.fromJS)(o); + return ( + (i = i.set('type', 'spec')), + s + .update('errors', (s) => + (s || (0, qe.List)()).push((0, qe.fromJS)(i)).sortBy((s) => s.get('line')) + ) + .update('errors', (s) => transformErrors(s)) + ); + }, + [nt]: (s, { payload: o }) => ( + (o = o.map((s) => (0, qe.fromJS)(Object.assign(Pn, s, { type: 'spec' })))), + s + .update('errors', (s) => (s || (0, qe.List)()).concat((0, qe.fromJS)(o))) + .update('errors', (s) => transformErrors(s)) + ), + [st]: (s, { payload: o }) => { + let i = (0, qe.fromJS)(Object.assign({}, o)); + return ( + (i = i.set('type', 'auth')), + s + .update('errors', (s) => (s || (0, qe.List)()).push((0, qe.fromJS)(i))) + .update('errors', (s) => transformErrors(s)) + ); + }, + [ot]: (s, { payload: o }) => { + if (!o || !s.get('errors')) return s; + let i = s.get('errors').filter((s) => + s.keySeq().every((i) => { + const u = s.get(i), + _ = o[i]; + return !_ || u !== _; + }) + ); + return s.merge({ errors: i }); + }, + [it]: (s, { payload: o }) => { + if (!o || 'function' != typeof o) return s; + let i = s.get('errors').filter((s) => o(s)); + return s.merge({ errors: i }); + } + }, + actions: s, + selectors: j + } + } + }; + } + function opsFilter(s, o) { + return s.filter((s, i) => -1 !== i.indexOf(o)); + } + function filter() { + return { fn: { opsFilter } }; + } + var Nn = __webpack_require__(7666), + Rn = __webpack_require__.n(Nn); + const arrow_up = ({ className: s = null, width: o = 20, height: i = 20, ...u }) => + Pe.createElement( + 'svg', + Rn()( + { + xmlns: 'http://www.w3.org/2000/svg', + viewBox: '0 0 20 20', + className: s, + width: o, + height: i, + 'aria-hidden': 'true', + focusable: 'false' + }, + u + ), + Pe.createElement('path', { + d: 'M 17.418 14.908 C 17.69 15.176 18.127 15.176 18.397 14.908 C 18.667 14.64 18.668 14.207 18.397 13.939 L 10.489 6.109 C 10.219 5.841 9.782 5.841 9.51 6.109 L 1.602 13.939 C 1.332 14.207 1.332 14.64 1.602 14.908 C 1.873 15.176 2.311 15.176 2.581 14.908 L 10 7.767 L 17.418 14.908 Z' + }) + ), + arrow_down = ({ className: s = null, width: o = 20, height: i = 20, ...u }) => + Pe.createElement( + 'svg', + Rn()( + { + xmlns: 'http://www.w3.org/2000/svg', + viewBox: '0 0 20 20', + className: s, + width: o, + height: i, + 'aria-hidden': 'true', + focusable: 'false' + }, + u + ), + Pe.createElement('path', { + d: 'M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z' + }) + ), + arrow = ({ className: s = null, width: o = 20, height: i = 20, ...u }) => + Pe.createElement( + 'svg', + Rn()( + { + xmlns: 'http://www.w3.org/2000/svg', + viewBox: '0 0 20 20', + className: s, + width: o, + height: i, + 'aria-hidden': 'true', + focusable: 'false' + }, + u + ), + Pe.createElement('path', { + d: 'M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z' + }) + ), + components_close = ({ className: s = null, width: o = 20, height: i = 20, ...u }) => + Pe.createElement( + 'svg', + Rn()( + { + xmlns: 'http://www.w3.org/2000/svg', + viewBox: '0 0 20 20', + className: s, + width: o, + height: i, + 'aria-hidden': 'true', + focusable: 'false' + }, + u + ), + Pe.createElement('path', { + d: 'M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z' + }) + ), + copy = ({ className: s = null, width: o = 15, height: i = 16, ...u }) => + Pe.createElement( + 'svg', + Rn()( + { + xmlns: 'http://www.w3.org/2000/svg', + viewBox: '0 0 15 16', + className: s, + width: o, + height: i, + 'aria-hidden': 'true', + focusable: 'false' + }, + u + ), + Pe.createElement( + 'g', + { transform: 'translate(2, -1)' }, + Pe.createElement('path', { + fill: '#ffffff', + fillRule: 'evenodd', + d: 'M2 13h4v1H2v-1zm5-6H2v1h5V7zm2 3V8l-3 3 3 3v-2h5v-2H9zM4.5 9H2v1h2.5V9zM2 12h2.5v-1H2v1zm9 1h1v2c-.02.28-.11.52-.3.7-.19.18-.42.28-.7.3H1c-.55 0-1-.45-1-1V4c0-.55.45-1 1-1h3c0-1.11.89-2 2-2 1.11 0 2 .89 2 2h3c.55 0 1 .45 1 1v5h-1V6H1v9h10v-2zM2 5h8c0-.55-.45-1-1-1H8c-.55 0-1-.45-1-1s-.45-1-1-1-1 .45-1 1-.45 1-1 1H3c-.55 0-1 .45-1 1z' + }) + ) + ), + lock = ({ className: s = null, width: o = 20, height: i = 20, ...u }) => + Pe.createElement( + 'svg', + Rn()( + { + xmlns: 'http://www.w3.org/2000/svg', + viewBox: '0 0 20 20', + className: s, + width: o, + height: i, + 'aria-hidden': 'true', + focusable: 'false' + }, + u + ), + Pe.createElement('path', { + d: 'M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z' + }) + ), + unlock = ({ className: s = null, width: o = 20, height: i = 20, ...u }) => + Pe.createElement( + 'svg', + Rn()( + { + xmlns: 'http://www.w3.org/2000/svg', + viewBox: '0 0 20 20', + className: s, + width: o, + height: i, + 'aria-hidden': 'true', + focusable: 'false' + }, + u + ), + Pe.createElement('path', { + d: 'M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z' + }) + ), + icons = () => ({ + components: { + ArrowUpIcon: arrow_up, + ArrowDownIcon: arrow_down, + ArrowIcon: arrow, + CloseIcon: components_close, + CopyIcon: copy, + LockIcon: lock, + UnlockIcon: unlock + } + }), + Dn = 'layout_update_layout', + Ln = 'layout_update_filter', + Bn = 'layout_update_mode', + Fn = 'layout_show'; + function updateLayout(s) { + return { type: Dn, payload: s }; + } + function updateFilter(s) { + return { type: Ln, payload: s }; + } + function actions_show(s, o = !0) { + return ((s = normalizeArray(s)), { type: Fn, payload: { thing: s, shown: o } }); + } + function changeMode(s, o = '') { + return ((s = normalizeArray(s)), { type: Bn, payload: { thing: s, mode: o } }); + } + const qn = { + [Dn]: (s, o) => s.set('layout', o.payload), + [Ln]: (s, o) => s.set('filter', o.payload), + [Fn]: (s, o) => { + const i = o.payload.shown, + u = (0, qe.fromJS)(o.payload.thing); + return s.update('shown', (0, qe.fromJS)({}), (s) => s.set(u, i)); + }, + [Bn]: (s, o) => { + let i = o.payload.thing, + u = o.payload.mode; + return s.setIn(['modes'].concat(i), (u || '') + ''); + } + }, + current = (s) => s.get('layout'), + currentFilter = (s) => s.get('filter'), + isShown = (s, o, i) => ( + (o = normalizeArray(o)), + s.get('shown', (0, qe.fromJS)({})).get((0, qe.fromJS)(o), i) + ), + whatMode = (s, o, i = '') => ((o = normalizeArray(o)), s.getIn(['modes', ...o], i)), + $n = Ut( + (s) => s, + (s) => !isShown(s, 'editor') + ), + taggedOperations = + (s, o) => + (i, ...u) => { + let _ = s(i, ...u); + const { fn: w, layoutSelectors: x, getConfigs: C } = o.getSystem(), + j = C(), + { maxDisplayedTags: L } = j; + let B = x.currentFilter(); + return (B && !0 !== B && (_ = w.opsFilter(_, B)), L >= 0 && (_ = _.slice(0, L)), _); + }; + function plugins_layout() { + return { + statePlugins: { + layout: { reducers: qn, actions: L, selectors: B }, + spec: { wrapSelectors: $ } + } + }; + } + function logs({ configs: s }) { + const o = { debug: 0, info: 1, log: 2, warn: 3, error: 4 }, + getLevel = (s) => o[s] || -1; + let { logLevel: i } = s, + u = getLevel(i); + function log(s, ...o) { + getLevel(s) >= u && console[s](...o); + } + return ( + (log.warn = log.bind(null, 'warn')), + (log.error = log.bind(null, 'error')), + (log.info = log.bind(null, 'info')), + (log.debug = log.bind(null, 'debug')), + { rootInjects: { log } } + ); + } + let Vn = !1; + function on_complete() { + return { + statePlugins: { + spec: { + wrapActions: { + updateSpec: + (s) => + (...o) => ((Vn = !0), s(...o)), + updateJsonSpec: + (s, o) => + (...i) => { + const u = o.getConfigs().onComplete; + return ( + Vn && 'function' == typeof u && (setTimeout(u, 0), (Vn = !1)), + s(...i) + ); + } + } + } + } + }; + } + const extractKey = (s) => { + const o = '_**[]'; + return s.indexOf(o) < 0 ? s : s.split(o)[0].trim(); + }, + escapeShell = (s) => + '-d ' === s || /^[_\/-]/g.test(s) ? s : "'" + s.replace(/'/g, "'\\''") + "'", + escapeCMD = (s) => + '-d ' === + (s = s + .replace(/\^/g, '^^') + .replace(/\\"/g, '\\\\"') + .replace(/"/g, '""') + .replace(/\n/g, '^\n')) + ? s.replace(/-d /g, '-d ^\n') + : /^[_\/-]/g.test(s) + ? s + : '"' + s + '"', + escapePowershell = (s) => { + if ('-d ' === s) return s; + if (/\n/.test(s)) { + return `@"\n${s.replace(/`/g, '``').replace(/\$/g, '`$')}\n"@`; + } + if (!/^[_\/-]/.test(s)) { + return `'${s.replace(/'/g, "''")}'`; + } + return s; + }; + const curlify = (s, o, i, u = '') => { + let _ = !1, + w = ''; + const addWords = (...s) => (w += ' ' + s.map(o).join(' ')), + addWordsWithoutLeadingSpace = (...s) => (w += s.map(o).join(' ')), + addNewLine = () => (w += ` ${i}`), + addIndent = (s = 1) => (w += ' '.repeat(s)); + let x = s.get('headers'); + w += 'curl' + u; + const C = s.get('curlOptions'); + if ( + (qe.List.isList(C) && !C.isEmpty() && addWords(...s.get('curlOptions')), + addWords('-X', s.get('method')), + addNewLine(), + addIndent(), + addWordsWithoutLeadingSpace(`${s.get('url')}`), + x && x.size) + ) + for (let o of s.get('headers').entries()) { + (addNewLine(), addIndent()); + let [s, i] = o; + (addWordsWithoutLeadingSpace('-H', `${s}: ${i}`), + (_ = _ || (/^content-type$/i.test(s) && /^multipart\/form-data$/i.test(i)))); + } + const j = s.get('body'); + if (j) + if (_ && ['POST', 'PUT', 'PATCH'].includes(s.get('method'))) + for (let [s, o] of j.entrySeq()) { + let i = extractKey(s); + (addNewLine(), + addIndent(), + addWordsWithoutLeadingSpace('-F'), + o instanceof at.File && 'string' == typeof o.valueOf() + ? addWords(`${i}=${o.data}${o.type ? `;type=${o.type}` : ''}`) + : o instanceof at.File + ? addWords(`${i}=@${o.name}${o.type ? `;type=${o.type}` : ''}`) + : addWords(`${i}=${o}`)); + } + else if (j instanceof at.File) + (addNewLine(), + addIndent(), + addWordsWithoutLeadingSpace(`--data-binary '@${j.name}'`)); + else { + (addNewLine(), addIndent(), addWordsWithoutLeadingSpace('-d ')); + let o = j; + qe.Map.isMap(o) + ? addWordsWithoutLeadingSpace( + (function getStringBodyOfMap(s) { + let o = []; + for (let [i, u] of s.get('body').entrySeq()) { + let s = extractKey(i); + u instanceof at.File + ? o.push( + ` "${s}": {\n "name": "${u.name}"${u.type ? `,\n "type": "${u.type}"` : ''}\n }` + ) + : o.push( + ` "${s}": ${JSON.stringify(u, null, 2).replace(/(\r\n|\r|\n)/g, '\n ')}` + ); + } + return `{\n${o.join(',\n')}\n}`; + })(s) + ) + : ('string' != typeof o && (o = JSON.stringify(o)), + addWordsWithoutLeadingSpace(o)); + } + else + j || + 'POST' !== s.get('method') || + (addNewLine(), addIndent(), addWordsWithoutLeadingSpace("-d ''")); + return w; + }, + requestSnippetGenerator_curl_powershell = (s) => + curlify(s, escapePowershell, '`\n', '.exe'), + requestSnippetGenerator_curl_bash = (s) => curlify(s, escapeShell, '\\\n'), + requestSnippetGenerator_curl_cmd = (s) => curlify(s, escapeCMD, '^\n'), + request_snippets_selectors_state = (s) => s || (0, qe.Map)(), + Un = Ut(request_snippets_selectors_state, (s) => { + const o = s.get('languages'), + i = s.get('generators', (0, qe.Map)()); + return !o || o.isEmpty() ? i : i.filter((s, i) => o.includes(i)); + }), + getSnippetGenerators = + (s) => + ({ fn: o }) => + Un(s) + .map((s, i) => { + const u = ((s) => o[`requestSnippetGenerator_${s}`])(i); + return 'function' != typeof u ? null : s.set('fn', u); + }) + .filter((s) => s), + zn = Ut(request_snippets_selectors_state, (s) => s.get('activeLanguage')), + Wn = Ut(request_snippets_selectors_state, (s) => s.get('defaultExpanded')); + var Kn = __webpack_require__(46942), + Hn = __webpack_require__.n(Kn), + Jn = __webpack_require__(59399); + const Gn = { + cursor: 'pointer', + lineHeight: 1, + display: 'inline-flex', + backgroundColor: 'rgb(250, 250, 250)', + paddingBottom: '0', + paddingTop: '0', + border: '1px solid rgb(51, 51, 51)', + borderRadius: '4px 4px 0 0', + boxShadow: 'none', + borderBottom: 'none' + }, + Yn = { + cursor: 'pointer', + lineHeight: 1, + display: 'inline-flex', + backgroundColor: 'rgb(51, 51, 51)', + boxShadow: 'none', + border: '1px solid rgb(51, 51, 51)', + paddingBottom: '0', + paddingTop: '0', + borderRadius: '4px 4px 0 0', + marginTop: '-5px', + marginRight: '-5px', + marginLeft: '-5px', + zIndex: '9999', + borderBottom: 'none' + }, + request_snippets = ({ request: s, requestSnippetsSelectors: o, getComponent: i }) => { + const u = (0, Pe.useRef)(null), + _ = i('ArrowUpIcon'), + w = i('ArrowDownIcon'), + x = i('SyntaxHighlighter', !0), + [C, j] = (0, Pe.useState)(o.getSnippetGenerators()?.keySeq().first()), + [L, B] = (0, Pe.useState)(o?.getDefaultExpanded()), + $ = o.getSnippetGenerators(), + V = $.get(C), + U = V.get('fn')(s), + handleSetIsExpanded = () => { + B(!L); + }, + handleGetBtnStyle = (s) => (s === C ? Yn : Gn), + handlePreventYScrollingBeyondElement = (s) => { + const { target: o, deltaY: i } = s, + { scrollHeight: u, offsetHeight: _, scrollTop: w } = o; + u > _ && ((0 === w && i < 0) || (_ + w >= u && i > 0)) && s.preventDefault(); + }; + return ( + (0, Pe.useEffect)(() => {}, []), + (0, Pe.useEffect)(() => { + const s = Array.from(u.current.childNodes).filter( + (s) => !!s.nodeType && s.classList?.contains('curl-command') + ); + return ( + s.forEach((s) => + s.addEventListener('mousewheel', handlePreventYScrollingBeyondElement, { + passive: !1 + }) + ), + () => { + s.forEach((s) => + s.removeEventListener('mousewheel', handlePreventYScrollingBeyondElement) + ); + } + ); + }, [s]), + Pe.createElement( + 'div', + { className: 'request-snippets', ref: u }, + Pe.createElement( + 'div', + { + style: { + width: '100%', + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + marginBottom: '15px' + } + }, + Pe.createElement( + 'h4', + { onClick: () => handleSetIsExpanded(), style: { cursor: 'pointer' } }, + 'Snippets' + ), + Pe.createElement( + 'button', + { + onClick: () => handleSetIsExpanded(), + style: { border: 'none', background: 'none' }, + title: L ? 'Collapse operation' : 'Expand operation' + }, + L + ? Pe.createElement(w, { className: 'arrow', width: '10', height: '10' }) + : Pe.createElement(_, { className: 'arrow', width: '10', height: '10' }) + ) + ), + L && + Pe.createElement( + 'div', + { className: 'curl-command' }, + Pe.createElement( + 'div', + { + style: { + paddingLeft: '15px', + paddingRight: '10px', + width: '100%', + display: 'flex' + } + }, + $.entrySeq().map(([s, o]) => + Pe.createElement( + 'div', + { + className: Hn()('btn', { active: s === C }), + style: handleGetBtnStyle(s), + key: s, + onClick: () => + ((s) => { + C !== s && j(s); + })(s) + }, + Pe.createElement( + 'h4', + { style: s === C ? { color: 'white' } : {} }, + o.get('title') + ) + ) + ) + ), + Pe.createElement( + 'div', + { className: 'copy-to-clipboard' }, + Pe.createElement( + Jn.CopyToClipboard, + { text: U }, + Pe.createElement('button', null) + ) + ), + Pe.createElement( + 'div', + null, + Pe.createElement( + x, + { + language: V.get('syntax'), + className: 'curl microlight', + renderPlainText: ({ children: s, PlainTextViewer: o }) => + Pe.createElement(o, { className: 'curl' }, s) + }, + U + ) + ) + ) + ) + ); + }, + plugins_request_snippets = () => ({ + components: { RequestSnippets: request_snippets }, + fn: V, + statePlugins: { requestSnippets: { selectors: U } } + }); + class ModelCollapse extends Pe.Component { + static defaultProps = { + collapsedContent: '{...}', + expanded: !1, + title: null, + onToggle: () => {}, + hideSelfOnExpand: !1, + specPath: $e().List([]) + }; + constructor(s, o) { + super(s, o); + let { expanded: i, collapsedContent: u } = this.props; + this.state = { + expanded: i, + collapsedContent: u || ModelCollapse.defaultProps.collapsedContent + }; + } + componentDidMount() { + const { hideSelfOnExpand: s, expanded: o, modelName: i } = this.props; + s && o && this.props.onToggle(i, o); + } + UNSAFE_componentWillReceiveProps(s) { + this.props.expanded !== s.expanded && this.setState({ expanded: s.expanded }); + } + toggleCollapsed = () => { + (this.props.onToggle && this.props.onToggle(this.props.modelName, !this.state.expanded), + this.setState({ expanded: !this.state.expanded })); + }; + onLoad = (s) => { + if (s && this.props.layoutSelectors) { + const o = this.props.layoutSelectors.getScrollToKey(); + ($e().is(o, this.props.specPath) && this.toggleCollapsed(), + this.props.layoutActions.readyToScroll(this.props.specPath, s.parentElement)); + } + }; + render() { + const { title: s, classes: o } = this.props; + return this.state.expanded && this.props.hideSelfOnExpand + ? Pe.createElement('span', { className: o || '' }, this.props.children) + : Pe.createElement( + 'span', + { className: o || '', ref: this.onLoad }, + Pe.createElement( + 'button', + { + 'aria-expanded': this.state.expanded, + className: 'model-box-control', + onClick: this.toggleCollapsed + }, + s && Pe.createElement('span', { className: 'pointer' }, s), + Pe.createElement('span', { + className: 'model-toggle' + (this.state.expanded ? '' : ' collapsed') + }), + !this.state.expanded && + Pe.createElement('span', null, this.state.collapsedContent) + ), + this.state.expanded && this.props.children + ); + } + } + const useTabs = ({ initialTab: s, isExecute: o, schema: i, example: u }) => { + const _ = (0, Pe.useMemo)(() => ({ example: 'example', model: 'model' }), []), + w = (0, Pe.useMemo)(() => Object.keys(_), [_]).includes(s) && i && !o ? s : _.example, + x = ((s) => { + const o = (0, Pe.useRef)(); + return ( + (0, Pe.useEffect)(() => { + o.current = s; + }), + o.current + ); + })(o), + [C, j] = (0, Pe.useState)(w), + L = (0, Pe.useCallback)((s) => { + j(s.target.dataset.name); + }, []); + return ( + (0, Pe.useEffect)(() => { + x && !o && u && j(_.example); + }, [x, o, u]), + { activeTab: C, onTabChange: L, tabs: _ } + ); + }, + model_example = ({ + schema: s, + example: o, + isExecute: i = !1, + specPath: u, + includeWriteOnly: _ = !1, + includeReadOnly: w = !1, + getComponent: x, + getConfigs: C, + specSelectors: j + }) => { + const { defaultModelRendering: L, defaultModelExpandDepth: B } = C(), + $ = x('ModelWrapper'), + V = x('HighlightCode', !0), + U = St()(5).toString('base64'), + z = St()(5).toString('base64'), + Y = St()(5).toString('base64'), + Z = St()(5).toString('base64'), + ee = j.isOAS3(), + { + activeTab: ie, + tabs: ae, + onTabChange: le + } = useTabs({ initialTab: L, isExecute: i, schema: s, example: o }); + return Pe.createElement( + 'div', + { className: 'model-example' }, + Pe.createElement( + 'ul', + { className: 'tab', role: 'tablist' }, + Pe.createElement( + 'li', + { + className: Hn()('tabitem', { active: ie === ae.example }), + role: 'presentation' + }, + Pe.createElement( + 'button', + { + 'aria-controls': z, + 'aria-selected': ie === ae.example, + className: 'tablinks', + 'data-name': 'example', + id: U, + onClick: le, + role: 'tab' + }, + i ? 'Edit Value' : 'Example Value' + ) + ), + s && + Pe.createElement( + 'li', + { + className: Hn()('tabitem', { active: ie === ae.model }), + role: 'presentation' + }, + Pe.createElement( + 'button', + { + 'aria-controls': Z, + 'aria-selected': ie === ae.model, + className: Hn()('tablinks', { inactive: i }), + 'data-name': 'model', + id: Y, + onClick: le, + role: 'tab' + }, + ee ? 'Schema' : 'Model' + ) + ) + ), + ie === ae.example && + Pe.createElement( + 'div', + { + 'aria-hidden': ie !== ae.example, + 'aria-labelledby': U, + 'data-name': 'examplePanel', + id: z, + role: 'tabpanel', + tabIndex: '0' + }, + o || Pe.createElement(V, null, '(no example available') + ), + ie === ae.model && + Pe.createElement( + 'div', + { + 'aria-hidden': ie === ae.example, + 'aria-labelledby': Y, + 'data-name': 'modelPanel', + id: Z, + role: 'tabpanel', + tabIndex: '0' + }, + Pe.createElement($, { + schema: s, + getComponent: x, + getConfigs: C, + specSelectors: j, + expandDepth: B, + specPath: u, + includeReadOnly: w, + includeWriteOnly: _ + }) + ) + ); + }; + class ModelWrapper extends Pe.Component { + onToggle = (s, o) => { + this.props.layoutActions && this.props.layoutActions.show(this.props.fullPath, o); + }; + render() { + let { getComponent: s, getConfigs: o } = this.props; + const i = s('Model'); + let u; + return ( + this.props.layoutSelectors && + (u = this.props.layoutSelectors.isShown(this.props.fullPath)), + Pe.createElement( + 'div', + { className: 'model-box' }, + Pe.createElement( + i, + Rn()({}, this.props, { + getConfigs: o, + expanded: u, + depth: 1, + onToggle: this.onToggle, + expandDepth: this.props.expandDepth || 0 + }) + ) + ) + ); + } + } + function _typeof(s) { + return ( + (_typeof = + 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator + ? function (s) { + return typeof s; + } + : function (s) { + return s && + 'function' == typeof Symbol && + s.constructor === Symbol && + s !== Symbol.prototype + ? 'symbol' + : typeof s; + }), + _typeof(s) + ); + } + function _defineProperties(s, o) { + for (var i = 0; i < o.length; i++) { + var u = o[i]; + ((u.enumerable = u.enumerable || !1), + (u.configurable = !0), + 'value' in u && (u.writable = !0), + Object.defineProperty(s, u.key, u)); + } + } + function _defineProperty(s, o, i) { + return ( + o in s + ? Object.defineProperty(s, o, { + value: i, + enumerable: !0, + configurable: !0, + writable: !0 + }) + : (s[o] = i), + s + ); + } + function ownKeys(s, o) { + var i = Object.keys(s); + if (Object.getOwnPropertySymbols) { + var u = Object.getOwnPropertySymbols(s); + (o && + (u = u.filter(function (o) { + return Object.getOwnPropertyDescriptor(s, o).enumerable; + })), + i.push.apply(i, u)); + } + return i; + } + function _getPrototypeOf(s) { + return ( + (_getPrototypeOf = Object.setPrototypeOf + ? Object.getPrototypeOf + : function _getPrototypeOf(s) { + return s.__proto__ || Object.getPrototypeOf(s); + }), + _getPrototypeOf(s) + ); + } + function _setPrototypeOf(s, o) { + return ( + (_setPrototypeOf = + Object.setPrototypeOf || + function _setPrototypeOf(s, o) { + return ((s.__proto__ = o), s); + }), + _setPrototypeOf(s, o) + ); + } + function _possibleConstructorReturn(s, o) { + return !o || ('object' != typeof o && 'function' != typeof o) + ? (function _assertThisInitialized(s) { + if (void 0 === s) + throw new ReferenceError( + "this hasn't been initialised - super() hasn't been called" + ); + return s; + })(s) + : o; + } + var Xn = {}; + function react_immutable_pure_component_es_get(s, o, i) { + return (function isInvalid(s) { + return null == s; + })(s) + ? i + : (function isMapLike(s) { + return ( + null !== s && + 'object' === _typeof(s) && + 'function' == typeof s.get && + 'function' == typeof s.has + ); + })(s) + ? s.has(o) + ? s.get(o) + : i + : hasOwnProperty.call(s, o) + ? s[o] + : i; + } + function getIn(s, o, i) { + for (var u = 0; u !== o.length; ) + if ((s = react_immutable_pure_component_es_get(s, o[u++], Xn)) === Xn) return i; + return s; + } + function check(s) { + var o = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}, + i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {}, + u = (function createChecker(s, o) { + return function (i) { + if ('string' == typeof i) return (0, qe.is)(o[i], s[i]); + if (Array.isArray(i)) return (0, qe.is)(getIn(o, i), getIn(s, i)); + throw new TypeError('Invalid key: expected Array or string: ' + i); + }; + })(o, i), + _ = + s || + Object.keys( + (function _objectSpread2(s) { + for (var o = 1; o < arguments.length; o++) { + var i = null != arguments[o] ? arguments[o] : {}; + o % 2 + ? ownKeys(i, !0).forEach(function (o) { + _defineProperty(s, o, i[o]); + }) + : Object.getOwnPropertyDescriptors + ? Object.defineProperties(s, Object.getOwnPropertyDescriptors(i)) + : ownKeys(i).forEach(function (o) { + Object.defineProperty(s, o, Object.getOwnPropertyDescriptor(i, o)); + }); + } + return s; + })({}, i, {}, o) + ); + return _.every(u); + } + const Zn = (function (s) { + function ImmutablePureComponent() { + return ( + (function _classCallCheck(s, o) { + if (!(s instanceof o)) throw new TypeError('Cannot call a class as a function'); + })(this, ImmutablePureComponent), + _possibleConstructorReturn( + this, + _getPrototypeOf(ImmutablePureComponent).apply(this, arguments) + ) + ); + } + return ( + (function _inherits(s, o) { + if ('function' != typeof o && null !== o) + throw new TypeError('Super expression must either be null or a function'); + ((s.prototype = Object.create(o && o.prototype, { + constructor: { value: s, writable: !0, configurable: !0 } + })), + o && _setPrototypeOf(s, o)); + })(ImmutablePureComponent, s), + (function _createClass(s, o, i) { + return (o && _defineProperties(s.prototype, o), i && _defineProperties(s, i), s); + })(ImmutablePureComponent, [ + { + key: 'shouldComponentUpdate', + value: function shouldComponentUpdate(s) { + var o = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}; + return ( + !check(this.updateOnProps, this.props, s, 'updateOnProps') || + !check(this.updateOnStates, this.state, o, 'updateOnStates') + ); + } + } + ]), + ImmutablePureComponent + ); + })(Pe.Component); + var Qn, + es = __webpack_require__(5556), + ts = __webpack_require__.n(es); + function _extends() { + return ( + (_extends = Object.assign + ? Object.assign.bind() + : function (s) { + for (var o = 1; o < arguments.length; o++) { + var i = arguments[o]; + for (var u in i) ({}).hasOwnProperty.call(i, u) && (s[u] = i[u]); + } + return s; + }), + _extends.apply(null, arguments) + ); + } + const rolling_load = (s) => + Pe.createElement( + 'svg', + _extends( + { + xmlns: 'http://www.w3.org/2000/svg', + width: 200, + height: 200, + className: 'rolling-load_svg__lds-rolling', + preserveAspectRatio: 'xMidYMid', + style: { + backgroundImage: 'none', + backgroundPosition: 'initial initial', + backgroundRepeat: 'initial initial' + }, + viewBox: '0 0 100 100' + }, + s + ), + Qn || + (Qn = Pe.createElement( + 'circle', + { + cx: 50, + cy: 50, + r: 35, + fill: 'none', + stroke: '#555', + strokeDasharray: '164.93361431346415 56.97787143782138', + strokeWidth: 10 + }, + Pe.createElement('animateTransform', { + attributeName: 'transform', + begin: '0s', + calcMode: 'linear', + dur: '1s', + keyTimes: '0;1', + repeatCount: 'indefinite', + type: 'rotate', + values: '0 50 50;360 50 50' + }) + )) + ), + decodeRefName = (s) => { + const o = s.replace(/~1/g, '/').replace(/~0/g, '~'); + try { + return decodeURIComponent(o); + } catch { + return o; + } + }; + class Model extends Zn { + static propTypes = { + schema: kn().map.isRequired, + getComponent: ts().func.isRequired, + getConfigs: ts().func.isRequired, + specSelectors: ts().object.isRequired, + name: ts().string, + displayName: ts().string, + isRef: ts().bool, + required: ts().bool, + expandDepth: ts().number, + depth: ts().number, + specPath: kn().list.isRequired, + includeReadOnly: ts().bool, + includeWriteOnly: ts().bool + }; + getModelName = (s) => + -1 !== s.indexOf('#/definitions/') + ? decodeRefName(s.replace(/^.*#\/definitions\//, '')) + : -1 !== s.indexOf('#/components/schemas/') + ? decodeRefName(s.replace(/^.*#\/components\/schemas\//, '')) + : void 0; + getRefSchema = (s) => { + let { specSelectors: o } = this.props; + return o.findDefinition(s); + }; + render() { + let { + getComponent: s, + getConfigs: o, + specSelectors: i, + schema: u, + required: _, + name: w, + isRef: x, + specPath: C, + displayName: j, + includeReadOnly: L, + includeWriteOnly: B + } = this.props; + const $ = s('ObjectModel'), + V = s('ArrayModel'), + U = s('PrimitiveModel'); + let z = 'object', + Y = u && u.get('$$ref'), + Z = u && u.get('$ref'); + if ((!w && Y && (w = this.getModelName(Y)), Z)) { + const s = this.getModelName(Z), + o = this.getRefSchema(s); + qe.Map.isMap(o) + ? ((u = o.mergeDeep(u)), Y || ((u = u.set('$$ref', Z)), (Y = Z))) + : qe.Map.isMap(u) && 1 === u.size && ((u = null), (w = Z)); + } + if (!u) + return Pe.createElement( + 'span', + { className: 'model model-title' }, + Pe.createElement('span', { className: 'model-title__text' }, j || w), + !Z && Pe.createElement(rolling_load, { height: '20px', width: '20px' }) + ); + const ee = i.isOAS3() && u.get('deprecated'); + switch (((x = void 0 !== x ? x : !!Y), (z = (u && u.get('type')) || z), z)) { + case 'object': + return Pe.createElement( + $, + Rn()({ className: 'object' }, this.props, { + specPath: C, + getConfigs: o, + schema: u, + name: w, + deprecated: ee, + isRef: x, + includeReadOnly: L, + includeWriteOnly: B + }) + ); + case 'array': + return Pe.createElement( + V, + Rn()({ className: 'array' }, this.props, { + getConfigs: o, + schema: u, + name: w, + deprecated: ee, + required: _, + includeReadOnly: L, + includeWriteOnly: B + }) + ); + default: + return Pe.createElement( + U, + Rn()({}, this.props, { + getComponent: s, + getConfigs: o, + schema: u, + name: w, + deprecated: ee, + required: _ + }) + ); + } + } + } + class Models extends Pe.Component { + getSchemaBasePath = () => + this.props.specSelectors.isOAS3() ? ['components', 'schemas'] : ['definitions']; + getCollapsedContent = () => ' '; + handleToggle = (s, o) => { + const { layoutActions: i } = this.props; + (i.show([...this.getSchemaBasePath(), s], o), + o && this.props.specActions.requestResolvedSubtree([...this.getSchemaBasePath(), s])); + }; + onLoadModels = (s) => { + s && this.props.layoutActions.readyToScroll(this.getSchemaBasePath(), s); + }; + onLoadModel = (s) => { + if (s) { + const o = s.getAttribute('data-name'); + this.props.layoutActions.readyToScroll([...this.getSchemaBasePath(), o], s); + } + }; + render() { + let { + specSelectors: s, + getComponent: o, + layoutSelectors: i, + layoutActions: u, + getConfigs: _ + } = this.props, + w = s.definitions(), + { docExpansion: x, defaultModelsExpandDepth: C } = _(); + if (!w.size || C < 0) return null; + const j = this.getSchemaBasePath(); + let L = i.isShown(j, C > 0 && 'none' !== x); + const B = s.isOAS3(), + $ = o('ModelWrapper'), + V = o('Collapse'), + U = o('ModelCollapse'), + z = o('JumpToPath', !0), + Y = o('ArrowUpIcon'), + Z = o('ArrowDownIcon'); + return Pe.createElement( + 'section', + { className: L ? 'models is-open' : 'models', ref: this.onLoadModels }, + Pe.createElement( + 'h4', + null, + Pe.createElement( + 'button', + { 'aria-expanded': L, className: 'models-control', onClick: () => u.show(j, !L) }, + Pe.createElement('span', null, B ? 'Schemas' : 'Models'), + L ? Pe.createElement(Y, null) : Pe.createElement(Z, null) + ) + ), + Pe.createElement( + V, + { isOpened: L }, + w + .entrySeq() + .map(([w]) => { + const x = [...j, w], + L = $e().List(x), + B = s.specResolvedSubtree(x), + V = s.specJson().getIn(x), + Y = qe.Map.isMap(B) ? B : $e().Map(), + Z = qe.Map.isMap(V) ? V : $e().Map(), + ee = Y.get('title') || Z.get('title') || w, + ie = i.isShown(x, !1); + ie && + 0 === Y.size && + Z.size > 0 && + this.props.specActions.requestResolvedSubtree(x); + const ae = Pe.createElement($, { + name: w, + expandDepth: C, + schema: Y || $e().Map(), + displayName: ee, + fullPath: x, + specPath: L, + getComponent: o, + specSelectors: s, + getConfigs: _, + layoutSelectors: i, + layoutActions: u, + includeReadOnly: !0, + includeWriteOnly: !0 + }), + le = Pe.createElement( + 'span', + { className: 'model-box' }, + Pe.createElement('span', { className: 'model model-title' }, ee) + ); + return Pe.createElement( + 'div', + { + id: `model-${w}`, + className: 'model-container', + key: `models-section-${w}`, + 'data-name': w, + ref: this.onLoadModel + }, + Pe.createElement( + 'span', + { className: 'models-jump-to-path' }, + Pe.createElement(z, { specPath: L }) + ), + Pe.createElement( + U, + { + classes: 'model-box', + collapsedContent: this.getCollapsedContent(w), + onToggle: this.handleToggle, + title: le, + displayName: ee, + modelName: w, + specPath: L, + layoutSelectors: i, + layoutActions: u, + hideSelfOnExpand: !0, + expanded: C > 0 && ie + }, + ae + ) + ); + }) + .toArray() + ) + ); + } + } + const enum_model = ({ value: s, getComponent: o }) => { + let i = o('ModelCollapse'), + u = Pe.createElement('span', null, 'Array [ ', s.count(), ' ]'); + return Pe.createElement( + 'span', + { className: 'prop-enum' }, + 'Enum:', + Pe.createElement('br', null), + Pe.createElement(i, { collapsedContent: u }, '[ ', s.map(String).join(', '), ' ]') + ); + }; + class ObjectModel extends Pe.Component { + render() { + let { + schema: s, + name: o, + displayName: i, + isRef: u, + getComponent: _, + getConfigs: w, + depth: x, + onToggle: C, + expanded: j, + specPath: L, + ...B + } = this.props, + { specSelectors: $, expandDepth: V, includeReadOnly: U, includeWriteOnly: z } = B; + const { isOAS3: Y } = $; + if (!s) return null; + const { showExtensions: Z } = w(); + let ee = s.get('description'), + ie = s.get('properties'), + ae = s.get('additionalProperties'), + le = s.get('title') || i || o, + ce = s.get('required'), + pe = s.filter( + (s, o) => + -1 !== ['maxProperties', 'minProperties', 'nullable', 'example'].indexOf(o) + ), + de = s.get('deprecated'), + fe = s.getIn(['externalDocs', 'url']), + ye = s.getIn(['externalDocs', 'description']); + const be = _('JumpToPath', !0), + _e = _('Markdown', !0), + we = _('Model'), + Se = _('ModelCollapse'), + xe = _('Property'), + Te = _('Link'), + JumpToPathSection = () => + Pe.createElement( + 'span', + { className: 'model-jump-to-path' }, + Pe.createElement(be, { specPath: L }) + ), + Re = Pe.createElement( + 'span', + null, + Pe.createElement('span', null, '{'), + '...', + Pe.createElement('span', null, '}'), + u ? Pe.createElement(JumpToPathSection, null) : '' + ), + $e = $.isOAS3() ? s.get('allOf') : null, + ze = $.isOAS3() ? s.get('anyOf') : null, + We = $.isOAS3() ? s.get('oneOf') : null, + He = $.isOAS3() ? s.get('not') : null, + Ye = + le && + Pe.createElement( + 'span', + { className: 'model-title' }, + u && + s.get('$$ref') && + Pe.createElement('span', { className: 'model-hint' }, s.get('$$ref')), + Pe.createElement('span', { className: 'model-title__text' }, le) + ); + return Pe.createElement( + 'span', + { className: 'model' }, + Pe.createElement( + Se, + { + modelName: o, + title: Ye, + onToggle: C, + expanded: !!j || x <= V, + collapsedContent: Re + }, + Pe.createElement('span', { className: 'brace-open object' }, '{'), + u ? Pe.createElement(JumpToPathSection, null) : null, + Pe.createElement( + 'span', + { className: 'inner-object' }, + Pe.createElement( + 'table', + { className: 'model' }, + Pe.createElement( + 'tbody', + null, + ee + ? Pe.createElement( + 'tr', + { className: 'description' }, + Pe.createElement('td', null, 'description:'), + Pe.createElement('td', null, Pe.createElement(_e, { source: ee })) + ) + : null, + fe && + Pe.createElement( + 'tr', + { className: 'external-docs' }, + Pe.createElement('td', null, 'externalDocs:'), + Pe.createElement( + 'td', + null, + Pe.createElement( + Te, + { target: '_blank', href: sanitizeUrl(fe) }, + ye || fe + ) + ) + ), + de + ? Pe.createElement( + 'tr', + { className: 'property' }, + Pe.createElement('td', null, 'deprecated:'), + Pe.createElement('td', null, 'true') + ) + : null, + ie && ie.size + ? ie + .entrySeq() + .filter( + ([, s]) => (!s.get('readOnly') || U) && (!s.get('writeOnly') || z) + ) + .map(([s, i]) => { + let u = Y() && i.get('deprecated'), + C = qe.List.isList(ce) && ce.contains(s), + j = ['property-row']; + return ( + u && j.push('deprecated'), + C && j.push('required'), + Pe.createElement( + 'tr', + { key: s, className: j.join(' ') }, + Pe.createElement( + 'td', + null, + s, + C && Pe.createElement('span', { className: 'star' }, '*') + ), + Pe.createElement( + 'td', + null, + Pe.createElement( + we, + Rn()({ key: `object-${o}-${s}_${i}` }, B, { + required: C, + getComponent: _, + specPath: L.push('properties', s), + getConfigs: w, + schema: i, + depth: x + 1 + }) + ) + ) + ) + ); + }) + .toArray() + : null, + Z ? Pe.createElement('tr', null, Pe.createElement('td', null, ' ')) : null, + Z + ? s + .entrySeq() + .map(([s, o]) => { + if ('x-' !== s.slice(0, 2)) return; + const i = o ? (o.toJS ? o.toJS() : o) : null; + return Pe.createElement( + 'tr', + { key: s, className: 'extension' }, + Pe.createElement('td', null, s), + Pe.createElement('td', null, JSON.stringify(i)) + ); + }) + .toArray() + : null, + ae && ae.size + ? Pe.createElement( + 'tr', + null, + Pe.createElement('td', null, '< * >:'), + Pe.createElement( + 'td', + null, + Pe.createElement( + we, + Rn()({}, B, { + required: !1, + getComponent: _, + specPath: L.push('additionalProperties'), + getConfigs: w, + schema: ae, + depth: x + 1 + }) + ) + ) + ) + : null, + $e + ? Pe.createElement( + 'tr', + null, + Pe.createElement('td', null, 'allOf ->'), + Pe.createElement( + 'td', + null, + $e.map((s, o) => + Pe.createElement( + 'div', + { key: o }, + Pe.createElement( + we, + Rn()({}, B, { + required: !1, + getComponent: _, + specPath: L.push('allOf', o), + getConfigs: w, + schema: s, + depth: x + 1 + }) + ) + ) + ) + ) + ) + : null, + ze + ? Pe.createElement( + 'tr', + null, + Pe.createElement('td', null, 'anyOf ->'), + Pe.createElement( + 'td', + null, + ze.map((s, o) => + Pe.createElement( + 'div', + { key: o }, + Pe.createElement( + we, + Rn()({}, B, { + required: !1, + getComponent: _, + specPath: L.push('anyOf', o), + getConfigs: w, + schema: s, + depth: x + 1 + }) + ) + ) + ) + ) + ) + : null, + We + ? Pe.createElement( + 'tr', + null, + Pe.createElement('td', null, 'oneOf ->'), + Pe.createElement( + 'td', + null, + We.map((s, o) => + Pe.createElement( + 'div', + { key: o }, + Pe.createElement( + we, + Rn()({}, B, { + required: !1, + getComponent: _, + specPath: L.push('oneOf', o), + getConfigs: w, + schema: s, + depth: x + 1 + }) + ) + ) + ) + ) + ) + : null, + He + ? Pe.createElement( + 'tr', + null, + Pe.createElement('td', null, 'not ->'), + Pe.createElement( + 'td', + null, + Pe.createElement( + 'div', + null, + Pe.createElement( + we, + Rn()({}, B, { + required: !1, + getComponent: _, + specPath: L.push('not'), + getConfigs: w, + schema: He, + depth: x + 1 + }) + ) + ) + ) + ) + : null + ) + ) + ), + Pe.createElement('span', { className: 'brace-close' }, '}') + ), + pe.size + ? pe.entrySeq().map(([s, o]) => + Pe.createElement(xe, { + key: `${s}-${o}`, + propKey: s, + propVal: o, + propClass: 'property' + }) + ) + : null + ); + } + } + class ArrayModel extends Pe.Component { + render() { + let { + getComponent: s, + getConfigs: o, + schema: i, + depth: u, + expandDepth: _, + name: w, + displayName: x, + specPath: C + } = this.props, + j = i.get('description'), + L = i.get('items'), + B = i.get('title') || x || w, + $ = i.filter( + (s, o) => + -1 === ['type', 'items', 'description', '$$ref', 'externalDocs'].indexOf(o) + ), + V = i.getIn(['externalDocs', 'url']), + U = i.getIn(['externalDocs', 'description']); + const z = s('Markdown', !0), + Y = s('ModelCollapse'), + Z = s('Model'), + ee = s('Property'), + ie = s('Link'), + ae = + B && + Pe.createElement( + 'span', + { className: 'model-title' }, + Pe.createElement('span', { className: 'model-title__text' }, B) + ); + return Pe.createElement( + 'span', + { className: 'model' }, + Pe.createElement( + Y, + { title: ae, expanded: u <= _, collapsedContent: '[...]' }, + '[', + $.size + ? $.entrySeq().map(([s, o]) => + Pe.createElement(ee, { + key: `${s}-${o}`, + propKey: s, + propVal: o, + propClass: 'property' + }) + ) + : null, + j + ? Pe.createElement(z, { source: j }) + : $.size + ? Pe.createElement('div', { className: 'markdown' }) + : null, + V && + Pe.createElement( + 'div', + { className: 'external-docs' }, + Pe.createElement(ie, { target: '_blank', href: sanitizeUrl(V) }, U || V) + ), + Pe.createElement( + 'span', + null, + Pe.createElement( + Z, + Rn()({}, this.props, { + getConfigs: o, + specPath: C.push('items'), + name: null, + schema: L, + required: !1, + depth: u + 1 + }) + ) + ), + ']' + ) + ); + } + } + const rs = 'property primitive'; + class Primitive extends Pe.Component { + render() { + let { + schema: s, + getComponent: o, + getConfigs: i, + name: u, + displayName: _, + depth: w, + expandDepth: x + } = this.props; + const { showExtensions: C } = i(); + if (!s || !s.get) return Pe.createElement('div', null); + let j = s.get('type'), + L = s.get('format'), + B = s.get('xml'), + $ = s.get('enum'), + V = s.get('title') || _ || u, + U = s.get('description'), + z = getExtensions(s), + Y = s + .filter( + (s, o) => + -1 === + ['enum', 'type', 'format', 'description', '$$ref', 'externalDocs'].indexOf(o) + ) + .filterNot((s, o) => z.has(o)), + Z = s.getIn(['externalDocs', 'url']), + ee = s.getIn(['externalDocs', 'description']); + const ie = o('Markdown', !0), + ae = o('EnumModel'), + le = o('Property'), + ce = o('ModelCollapse'), + pe = o('Link'), + de = + V && + Pe.createElement( + 'span', + { className: 'model-title' }, + Pe.createElement('span', { className: 'model-title__text' }, V) + ); + return Pe.createElement( + 'span', + { className: 'model' }, + Pe.createElement( + ce, + { title: de, expanded: w <= x, collapsedContent: '[...]' }, + Pe.createElement( + 'span', + { className: 'prop' }, + u && w > 1 && Pe.createElement('span', { className: 'prop-name' }, V), + Pe.createElement('span', { className: 'prop-type' }, j), + L && Pe.createElement('span', { className: 'prop-format' }, '($', L, ')'), + Y.size + ? Y.entrySeq().map(([s, o]) => + Pe.createElement(le, { + key: `${s}-${o}`, + propKey: s, + propVal: o, + propClass: rs + }) + ) + : null, + C && z.size + ? z.entrySeq().map(([s, o]) => + Pe.createElement(le, { + key: `${s}-${o}`, + propKey: s, + propVal: o, + propClass: rs + }) + ) + : null, + U ? Pe.createElement(ie, { source: U }) : null, + Z && + Pe.createElement( + 'div', + { className: 'external-docs' }, + Pe.createElement(pe, { target: '_blank', href: sanitizeUrl(Z) }, ee || Z) + ), + B && B.size + ? Pe.createElement( + 'span', + null, + Pe.createElement('br', null), + Pe.createElement('span', { className: rs }, 'xml:'), + B.entrySeq() + .map(([s, o]) => + Pe.createElement( + 'span', + { key: `${s}-${o}`, className: rs }, + Pe.createElement('br', null), + '   ', + s, + ': ', + String(o) + ) + ) + .toArray() + ) + : null, + $ && Pe.createElement(ae, { value: $, getComponent: o }) + ) + ) + ); + } + } + class Schemes extends Pe.Component { + UNSAFE_componentWillMount() { + let { schemes: s } = this.props; + this.setScheme(s.first()); + } + UNSAFE_componentWillReceiveProps(s) { + (this.props.currentScheme && s.schemes.includes(this.props.currentScheme)) || + this.setScheme(s.schemes.first()); + } + onChange = (s) => { + this.setScheme(s.target.value); + }; + setScheme = (s) => { + let { path: o, method: i, specActions: u } = this.props; + u.setScheme(s, o, i); + }; + render() { + let { schemes: s, currentScheme: o } = this.props; + return Pe.createElement( + 'label', + { htmlFor: 'schemes' }, + Pe.createElement('span', { className: 'schemes-title' }, 'Schemes'), + Pe.createElement( + 'select', + { onChange: this.onChange, value: o, id: 'schemes' }, + s + .valueSeq() + .map((s) => Pe.createElement('option', { value: s, key: s }, s)) + .toArray() + ) + ); + } + } + class SchemesContainer extends Pe.Component { + render() { + const { specActions: s, specSelectors: o, getComponent: i } = this.props, + u = o.operationScheme(), + _ = o.schemes(), + w = i('schemes'); + return _ && _.size + ? Pe.createElement(w, { currentScheme: u, schemes: _, specActions: s }) + : null; + } + } + var ns = __webpack_require__(24677), + ss = __webpack_require__.n(ns); + const os = { + value: '', + onChange: () => {}, + schema: {}, + keyName: '', + required: !1, + errors: (0, qe.List)() + }; + class JsonSchemaForm extends Pe.Component { + static defaultProps = os; + componentDidMount() { + const { dispatchInitialValue: s, value: o, onChange: i } = this.props; + s ? i(o) : !1 === s && i(''); + } + render() { + let { + schema: s, + errors: o, + value: i, + onChange: u, + getComponent: _, + fn: w, + disabled: x + } = this.props; + const C = s && s.get ? s.get('format') : null, + j = s && s.get ? s.get('type') : null; + let getComponentSilently = (s) => _(s, !1, { failSilently: !0 }), + L = j + ? getComponentSilently(C ? `JsonSchema_${j}_${C}` : `JsonSchema_${j}`) + : _('JsonSchema_string'); + return ( + L || (L = _('JsonSchema_string')), + Pe.createElement( + L, + Rn()({}, this.props, { + errors: o, + fn: w, + getComponent: _, + value: i, + onChange: u, + schema: s, + disabled: x + }) + ) + ); + } + } + class JsonSchema_string extends Pe.Component { + static defaultProps = os; + onChange = (s) => { + const o = + this.props.schema && 'file' === this.props.schema.get('type') + ? s.target.files[0] + : s.target.value; + this.props.onChange(o, this.props.keyName); + }; + onEnumChange = (s) => this.props.onChange(s); + render() { + let { + getComponent: s, + value: o, + schema: i, + errors: u, + required: _, + description: w, + disabled: x + } = this.props; + const C = i && i.get ? i.get('enum') : null, + j = i && i.get ? i.get('format') : null, + L = i && i.get ? i.get('type') : null, + B = i && i.get ? i.get('in') : null; + if ((o || (o = ''), (u = u.toJS ? u.toJS() : []), C)) { + const i = s('Select'); + return Pe.createElement(i, { + className: u.length ? 'invalid' : '', + title: u.length ? u : '', + allowedValues: [...C], + value: o, + allowEmptyValue: !_, + disabled: x, + onChange: this.onEnumChange + }); + } + const $ = x || (B && 'formData' === B && !('FormData' in window)), + V = s('Input'); + return L && 'file' === L + ? Pe.createElement(V, { + type: 'file', + className: u.length ? 'invalid' : '', + title: u.length ? u : '', + onChange: this.onChange, + disabled: $ + }) + : Pe.createElement(ss(), { + type: j && 'password' === j ? 'password' : 'text', + className: u.length ? 'invalid' : '', + title: u.length ? u : '', + value: o, + minLength: 0, + debounceTimeout: 350, + placeholder: w, + onChange: this.onChange, + disabled: $ + }); + } + } + class JsonSchema_array extends Pe.PureComponent { + static defaultProps = os; + constructor(s, o) { + (super(s, o), (this.state = { value: valueOrEmptyList(s.value), schema: s.schema })); + } + UNSAFE_componentWillReceiveProps(s) { + const o = valueOrEmptyList(s.value); + (o !== this.state.value && this.setState({ value: o }), + s.schema !== this.state.schema && this.setState({ schema: s.schema })); + } + onChange = () => { + this.props.onChange(this.state.value); + }; + onItemChange = (s, o) => { + this.setState(({ value: i }) => ({ value: i.set(o, s) }), this.onChange); + }; + removeItem = (s) => { + this.setState(({ value: o }) => ({ value: o.delete(s) }), this.onChange); + }; + addItem = () => { + const { fn: s } = this.props; + let o = valueOrEmptyList(this.state.value); + this.setState( + () => ({ + value: o.push( + s.getSampleSchema(this.state.schema.get('items'), !1, { includeWriteOnly: !0 }) + ) + }), + this.onChange + ); + }; + onEnumChange = (s) => { + this.setState(() => ({ value: s }), this.onChange); + }; + render() { + let { + getComponent: s, + required: o, + schema: i, + errors: u, + fn: _, + disabled: w + } = this.props; + u = u.toJS ? u.toJS() : Array.isArray(u) ? u : []; + const x = u.filter((s) => 'string' == typeof s), + C = u.filter((s) => void 0 !== s.needRemove).map((s) => s.error), + j = this.state.value, + L = !!(j && j.count && j.count() > 0), + B = i.getIn(['items', 'enum']), + $ = i.getIn(['items', 'type']), + V = i.getIn(['items', 'format']), + U = i.get('items'); + let z, + Y = !1, + Z = 'file' === $ || ('string' === $ && 'binary' === V); + if ( + ($ && V + ? (z = s(`JsonSchema_${$}_${V}`)) + : ('boolean' !== $ && 'array' !== $ && 'object' !== $) || + (z = s(`JsonSchema_${$}`)), + z || Z || (Y = !0), + B) + ) { + const i = s('Select'); + return Pe.createElement(i, { + className: u.length ? 'invalid' : '', + title: u.length ? u : '', + multiple: !0, + value: j, + disabled: w, + allowedValues: B, + allowEmptyValue: !o, + onChange: this.onEnumChange + }); + } + const ee = s('Button'); + return Pe.createElement( + 'div', + { className: 'json-schema-array' }, + L + ? j.map((o, i) => { + const x = (0, qe.fromJS)([ + ...u.filter((s) => s.index === i).map((s) => s.error) + ]); + return Pe.createElement( + 'div', + { key: i, className: 'json-schema-form-item' }, + Z + ? Pe.createElement(JsonSchemaArrayItemFile, { + value: o, + onChange: (s) => this.onItemChange(s, i), + disabled: w, + errors: x, + getComponent: s + }) + : Y + ? Pe.createElement(JsonSchemaArrayItemText, { + value: o, + onChange: (s) => this.onItemChange(s, i), + disabled: w, + errors: x + }) + : Pe.createElement( + z, + Rn()({}, this.props, { + value: o, + onChange: (s) => this.onItemChange(s, i), + disabled: w, + errors: x, + schema: U, + getComponent: s, + fn: _ + }) + ), + w + ? null + : Pe.createElement( + ee, + { + className: `btn btn-sm json-schema-form-item-remove ${C.length ? 'invalid' : null}`, + title: C.length ? C : '', + onClick: () => this.removeItem(i) + }, + ' - ' + ) + ); + }) + : null, + w + ? null + : Pe.createElement( + ee, + { + className: `btn btn-sm json-schema-form-item-add ${x.length ? 'invalid' : null}`, + title: x.length ? x : '', + onClick: this.addItem + }, + 'Add ', + $ ? `${$} ` : '', + 'item' + ) + ); + } + } + class JsonSchemaArrayItemText extends Pe.Component { + static defaultProps = os; + onChange = (s) => { + const o = s.target.value; + this.props.onChange(o, this.props.keyName); + }; + render() { + let { value: s, errors: o, description: i, disabled: u } = this.props; + return ( + s || (s = ''), + (o = o.toJS ? o.toJS() : []), + Pe.createElement(ss(), { + type: 'text', + className: o.length ? 'invalid' : '', + title: o.length ? o : '', + value: s, + minLength: 0, + debounceTimeout: 350, + placeholder: i, + onChange: this.onChange, + disabled: u + }) + ); + } + } + class JsonSchemaArrayItemFile extends Pe.Component { + static defaultProps = os; + onFileChange = (s) => { + const o = s.target.files[0]; + this.props.onChange(o, this.props.keyName); + }; + render() { + let { getComponent: s, errors: o, disabled: i } = this.props; + const u = s('Input'), + _ = i || !('FormData' in window); + return Pe.createElement(u, { + type: 'file', + className: o.length ? 'invalid' : '', + title: o.length ? o : '', + onChange: this.onFileChange, + disabled: _ + }); + } + } + class JsonSchema_boolean extends Pe.Component { + static defaultProps = os; + onEnumChange = (s) => this.props.onChange(s); + render() { + let { + getComponent: s, + value: o, + errors: i, + schema: u, + required: _, + disabled: w + } = this.props; + i = i.toJS ? i.toJS() : []; + let x = u && u.get ? u.get('enum') : null, + C = !x || !_, + j = !x && ['true', 'false']; + const L = s('Select'); + return Pe.createElement(L, { + className: i.length ? 'invalid' : '', + title: i.length ? i : '', + value: String(o), + disabled: w, + allowedValues: x ? [...x] : j, + allowEmptyValue: C, + onChange: this.onEnumChange + }); + } + } + const stringifyObjectErrors = (s) => + s.map((s) => { + const o = void 0 !== s.propKey ? s.propKey : s.index; + let i = 'string' == typeof s ? s : 'string' == typeof s.error ? s.error : null; + if (!o && i) return i; + let u = s.error, + _ = `/${s.propKey}`; + for (; 'object' == typeof u; ) { + const s = void 0 !== u.propKey ? u.propKey : u.index; + if (void 0 === s) break; + if (((_ += `/${s}`), !u.error)) break; + u = u.error; + } + return `${_}: ${u}`; + }); + class JsonSchema_object extends Pe.PureComponent { + constructor() { + super(); + } + static defaultProps = os; + onChange = (s) => { + this.props.onChange(s); + }; + handleOnChange = (s) => { + const o = s.target.value; + this.onChange(o); + }; + render() { + let { getComponent: s, value: o, errors: i, disabled: u } = this.props; + const _ = s('TextArea'); + return ( + (i = i.toJS ? i.toJS() : Array.isArray(i) ? i : []), + Pe.createElement( + 'div', + null, + Pe.createElement(_, { + className: Hn()({ invalid: i.length }), + title: i.length ? stringifyObjectErrors(i).join(', ') : '', + value: stringify(o), + disabled: u, + onChange: this.handleOnChange + }) + ) + ); + } + } + function valueOrEmptyList(s) { + return qe.List.isList(s) ? s : Array.isArray(s) ? (0, qe.fromJS)(s) : (0, qe.List)(); + } + const json_schema_5 = () => ({ + components: { + modelExample: model_example, + ModelWrapper, + ModelCollapse, + Model, + Models, + EnumModel: enum_model, + ObjectModel, + ArrayModel, + PrimitiveModel: Primitive, + schemes: Schemes, + SchemesContainer, + ...z + } + }); + var as = __webpack_require__(19123), + ls = __webpack_require__.n(as), + cs = __webpack_require__(41859), + us = __webpack_require__.n(cs), + ps = __webpack_require__(62193), + hs = __webpack_require__.n(ps); + const shallowArrayEquals = (s) => (o) => + Array.isArray(s) && + Array.isArray(o) && + s.length === o.length && + s.every((s, i) => s === o[i]), + list = (...s) => s; + class Cache extends Map { + delete(s) { + const o = Array.from(this.keys()).find(shallowArrayEquals(s)); + return super.delete(o); + } + get(s) { + const o = Array.from(this.keys()).find(shallowArrayEquals(s)); + return super.get(o); + } + has(s) { + return -1 !== Array.from(this.keys()).findIndex(shallowArrayEquals(s)); + } + } + const utils_memoizeN = (s, o = list) => { + const { Cache: i } = ut(); + ut().Cache = Cache; + const u = ut()(s, o); + return ((ut().Cache = i), u); + }, + ds = { + string: (s) => + s.pattern + ? ((s) => { + try { + return new (us())(s).gen(); + } catch (s) { + return 'string'; + } + })(s.pattern) + : 'string', + string_email: () => 'user@example.com', + 'string_date-time': () => new Date().toISOString(), + string_date: () => new Date().toISOString().substring(0, 10), + string_uuid: () => '3fa85f64-5717-4562-b3fc-2c963f66afa6', + string_hostname: () => 'example.com', + string_ipv4: () => '198.51.100.42', + string_ipv6: () => '2001:0db8:5b96:0000:0000:426f:8e17:642a', + number: () => 0, + number_float: () => 0, + integer: () => 0, + boolean: (s) => 'boolean' != typeof s.default || s.default + }, + primitive = (s) => { + s = objectify(s); + let { type: o, format: i } = s, + u = ds[`${o}_${i}`] || ds[o]; + return isFunc(u) ? u(s) : 'Unknown Type: ' + s.type; + }, + sanitizeRef = (s) => + deeplyStripKey(s, '$$ref', (s) => 'string' == typeof s && s.indexOf('#') > -1), + fs = ['maxProperties', 'minProperties'], + ms = ['minItems', 'maxItems'], + gs = ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum'], + ys = ['minLength', 'maxLength'], + mergeJsonSchema = (s, o, i = {}) => { + const u = { ...s }; + if ( + (['example', 'default', 'enum', 'xml', 'type', ...fs, ...ms, ...gs, ...ys].forEach( + (s) => + ((s) => { + void 0 === u[s] && void 0 !== o[s] && (u[s] = o[s]); + })(s) + ), + void 0 !== o.required && + Array.isArray(o.required) && + ((void 0 !== u.required && u.required.length) || (u.required = []), + o.required.forEach((s) => { + u.required.includes(s) || u.required.push(s); + })), + o.properties) + ) { + u.properties || (u.properties = {}); + let s = objectify(o.properties); + for (let _ in s) + Object.prototype.hasOwnProperty.call(s, _) && + ((s[_] && s[_].deprecated) || + (s[_] && s[_].readOnly && !i.includeReadOnly) || + (s[_] && s[_].writeOnly && !i.includeWriteOnly) || + u.properties[_] || + ((u.properties[_] = s[_]), + !o.required && + Array.isArray(o.required) && + -1 !== o.required.indexOf(_) && + (u.required ? u.required.push(_) : (u.required = [_])))); + } + return ( + o.items && + (u.items || (u.items = {}), (u.items = mergeJsonSchema(u.items, o.items, i))), + u + ); + }, + sampleFromSchemaGeneric = (s, o = {}, i = void 0, u = !1) => { + s && isFunc(s.toJS) && (s = s.toJS()); + let _ = void 0 !== i || (s && void 0 !== s.example) || (s && void 0 !== s.default); + const w = !_ && s && s.oneOf && s.oneOf.length > 0, + x = !_ && s && s.anyOf && s.anyOf.length > 0; + if (!_ && (w || x)) { + const i = objectify(w ? s.oneOf[0] : s.anyOf[0]); + if ( + (!(s = mergeJsonSchema(s, i, o)).xml && i.xml && (s.xml = i.xml), + void 0 !== s.example && void 0 !== i.example) + ) + _ = !0; + else if (i.properties) { + s.properties || (s.properties = {}); + let u = objectify(i.properties); + for (let _ in u) + Object.prototype.hasOwnProperty.call(u, _) && + ((u[_] && u[_].deprecated) || + (u[_] && u[_].readOnly && !o.includeReadOnly) || + (u[_] && u[_].writeOnly && !o.includeWriteOnly) || + s.properties[_] || + ((s.properties[_] = u[_]), + !i.required && + Array.isArray(i.required) && + -1 !== i.required.indexOf(_) && + (s.required ? s.required.push(_) : (s.required = [_])))); + } + } + const C = {}; + let { + xml: j, + type: L, + example: B, + properties: $, + additionalProperties: V, + items: U + } = s || {}, + { includeReadOnly: z, includeWriteOnly: Y } = o; + j = j || {}; + let Z, + { name: ee, prefix: ie, namespace: ae } = j, + le = {}; + if (u && ((ee = ee || 'notagname'), (Z = (ie ? ie + ':' : '') + ee), ae)) { + C[ie ? 'xmlns:' + ie : 'xmlns'] = ae; + } + u && (le[Z] = []); + const schemaHasAny = (o) => o.some((o) => Object.prototype.hasOwnProperty.call(s, o)); + s && + !L && + ($ || V || schemaHasAny(fs) + ? (L = 'object') + : U || schemaHasAny(ms) + ? (L = 'array') + : schemaHasAny(gs) + ? ((L = 'number'), (s.type = 'number')) + : _ || s.enum || ((L = 'string'), (s.type = 'string'))); + const handleMinMaxItems = (o) => { + if ((null != s?.maxItems && (o = o.slice(0, s?.maxItems)), null != s?.minItems)) { + let i = 0; + for (; o.length < s?.minItems; ) o.push(o[i++ % o.length]); + } + return o; + }, + ce = objectify($); + let pe, + de = 0; + const hasExceededMaxProperties = () => + s && + null !== s.maxProperties && + void 0 !== s.maxProperties && + de >= s.maxProperties, + canAddProperty = (o) => + !s || + null === s.maxProperties || + void 0 === s.maxProperties || + (!hasExceededMaxProperties() && + (!((o) => !(s && s.required && s.required.length && s.required.includes(o)))(o) || + s.maxProperties - + de - + (() => { + if (!s || !s.required) return 0; + let o = 0; + return ( + u + ? s.required.forEach((s) => (o += void 0 === le[s] ? 0 : 1)) + : s.required.forEach( + (s) => (o += void 0 === le[Z]?.find((o) => void 0 !== o[s]) ? 0 : 1) + ), + s.required.length - o + ); + })() > + 0)); + if ( + ((pe = u + ? (i, _ = void 0) => { + if (s && ce[i]) { + if (((ce[i].xml = ce[i].xml || {}), ce[i].xml.attribute)) { + const s = Array.isArray(ce[i].enum) ? ce[i].enum[0] : void 0, + o = ce[i].example, + u = ce[i].default; + return void (C[ce[i].xml.name || i] = + void 0 !== o + ? o + : void 0 !== u + ? u + : void 0 !== s + ? s + : primitive(ce[i])); + } + ce[i].xml.name = ce[i].xml.name || i; + } else ce[i] || !1 === V || (ce[i] = { xml: { name: i } }); + let w = sampleFromSchemaGeneric((s && ce[i]) || void 0, o, _, u); + canAddProperty(i) && + (de++, Array.isArray(w) ? (le[Z] = le[Z].concat(w)) : le[Z].push(w)); + } + : (i, _) => { + if (canAddProperty(i)) { + if ( + Object.prototype.hasOwnProperty.call(s, 'discriminator') && + s.discriminator && + Object.prototype.hasOwnProperty.call(s.discriminator, 'mapping') && + s.discriminator.mapping && + Object.prototype.hasOwnProperty.call(s, '$$ref') && + s.$$ref && + s.discriminator.propertyName === i + ) { + for (let o in s.discriminator.mapping) + if (-1 !== s.$$ref.search(s.discriminator.mapping[o])) { + le[i] = o; + break; + } + } else le[i] = sampleFromSchemaGeneric(ce[i], o, _, u); + de++; + } + }), + _) + ) { + let _; + if (((_ = sanitizeRef(void 0 !== i ? i : void 0 !== B ? B : s.default)), !u)) { + if ('number' == typeof _ && 'string' === L) return `${_}`; + if ('string' != typeof _ || 'string' === L) return _; + try { + return JSON.parse(_); + } catch (s) { + return _; + } + } + if ((s || (L = Array.isArray(_) ? 'array' : typeof _), 'array' === L)) { + if (!Array.isArray(_)) { + if ('string' == typeof _) return _; + _ = [_]; + } + const i = s ? s.items : void 0; + i && ((i.xml = i.xml || j || {}), (i.xml.name = i.xml.name || j.name)); + let w = _.map((s) => sampleFromSchemaGeneric(i, o, s, u)); + return ( + (w = handleMinMaxItems(w)), + j.wrapped ? ((le[Z] = w), hs()(C) || le[Z].push({ _attr: C })) : (le = w), + le + ); + } + if ('object' === L) { + if ('string' == typeof _) return _; + for (let o in _) + Object.prototype.hasOwnProperty.call(_, o) && + ((s && ce[o] && ce[o].readOnly && !z) || + (s && ce[o] && ce[o].writeOnly && !Y) || + (s && ce[o] && ce[o].xml && ce[o].xml.attribute + ? (C[ce[o].xml.name || o] = _[o]) + : pe(o, _[o]))); + return (hs()(C) || le[Z].push({ _attr: C }), le); + } + return ((le[Z] = hs()(C) ? _ : [{ _attr: C }, _]), le); + } + if ('object' === L) { + for (let s in ce) + Object.prototype.hasOwnProperty.call(ce, s) && + ((ce[s] && ce[s].deprecated) || + (ce[s] && ce[s].readOnly && !z) || + (ce[s] && ce[s].writeOnly && !Y) || + pe(s)); + if ((u && C && le[Z].push({ _attr: C }), hasExceededMaxProperties())) return le; + if (!0 === V) + (u + ? le[Z].push({ additionalProp: 'Anything can be here' }) + : (le.additionalProp1 = {}), + de++); + else if (V) { + const i = objectify(V), + _ = sampleFromSchemaGeneric(i, o, void 0, u); + if (u && i.xml && i.xml.name && 'notagname' !== i.xml.name) le[Z].push(_); + else { + const o = + null !== s.minProperties && void 0 !== s.minProperties && de < s.minProperties + ? s.minProperties - de + : 3; + for (let s = 1; s <= o; s++) { + if (hasExceededMaxProperties()) return le; + if (u) { + const o = {}; + ((o['additionalProp' + s] = _.notagname), le[Z].push(o)); + } else le['additionalProp' + s] = _; + de++; + } + } + } + return le; + } + if ('array' === L) { + if (!U) return; + let i; + if ( + (u && ((U.xml = U.xml || s?.xml || {}), (U.xml.name = U.xml.name || j.name)), + Array.isArray(U.anyOf)) + ) + i = U.anyOf.map((s) => + sampleFromSchemaGeneric(mergeJsonSchema(s, U, o), o, void 0, u) + ); + else if (Array.isArray(U.oneOf)) + i = U.oneOf.map((s) => + sampleFromSchemaGeneric(mergeJsonSchema(s, U, o), o, void 0, u) + ); + else { + if (!(!u || (u && j.wrapped))) return sampleFromSchemaGeneric(U, o, void 0, u); + i = [sampleFromSchemaGeneric(U, o, void 0, u)]; + } + return ( + (i = handleMinMaxItems(i)), + u && j.wrapped ? ((le[Z] = i), hs()(C) || le[Z].push({ _attr: C }), le) : i + ); + } + let fe; + if (s && Array.isArray(s.enum)) fe = normalizeArray(s.enum)[0]; + else { + if (!s) return; + if (((fe = primitive(s)), 'number' == typeof fe)) { + let o = s.minimum; + null != o && (s.exclusiveMinimum && o++, (fe = o)); + let i = s.maximum; + null != i && (s.exclusiveMaximum && i--, (fe = i)); + } + if ( + 'string' == typeof fe && + (null !== s.maxLength && void 0 !== s.maxLength && (fe = fe.slice(0, s.maxLength)), + null !== s.minLength && void 0 !== s.minLength) + ) { + let o = 0; + for (; fe.length < s.minLength; ) fe += fe[o++ % fe.length]; + } + } + if ('file' !== L) return u ? ((le[Z] = hs()(C) ? fe : [{ _attr: C }, fe]), le) : fe; + }, + inferSchema = (s) => (s.schema && (s = s.schema), s.properties && (s.type = 'object'), s), + createXMLExample = (s, o, i) => { + const u = sampleFromSchemaGeneric(s, o, i, !0); + if (u) return 'string' == typeof u ? u : ls()(u, { declaration: !0, indent: '\t' }); + }, + sampleFromSchema = (s, o, i) => sampleFromSchemaGeneric(s, o, i, !1), + resolver = (s, o, i) => [s, JSON.stringify(o), JSON.stringify(i)], + vs = utils_memoizeN(createXMLExample, resolver), + bs = utils_memoizeN(sampleFromSchema, resolver), + _s = [{ when: /json/, shouldStringifyTypes: ['string'] }], + Es = ['object'], + get_json_sample_schema = (s) => (o, i, u, _) => { + const { fn: w } = s(), + x = w.memoizedSampleFromSchema(o, i, _), + C = typeof x, + j = _s.reduce((s, o) => (o.when.test(u) ? [...s, ...o.shouldStringifyTypes] : s), Es); + return mt()(j, (s) => s === C) ? JSON.stringify(x, null, 2) : x; + }, + get_yaml_sample_schema = (s) => (o, i, u, _) => { + const { fn: w } = s(), + x = w.getJsonSampleSchema(o, i, u, _); + let C; + try { + ((C = mn.dump(mn.load(x), { lineWidth: -1 }, { schema: nn })), + '\n' === C[C.length - 1] && (C = C.slice(0, C.length - 1))); + } catch (s) { + return (console.error(s), 'error: could not generate yaml example'); + } + return C.replace(/\t/g, ' '); + }, + get_xml_sample_schema = (s) => (o, i, u) => { + const { fn: _ } = s(); + if ((o && !o.xml && (o.xml = {}), o && !o.xml.name)) { + if (!o.$$ref && (o.type || o.items || o.properties || o.additionalProperties)) + return '\n\x3c!-- XML example cannot be generated; root element name is undefined --\x3e'; + if (o.$$ref) { + let s = o.$$ref.match(/\S*\/(\S+)$/); + o.xml.name = s[1]; + } + } + return _.memoizedCreateXMLExample(o, i, u); + }, + get_sample_schema = + (s) => + (o, i = '', u = {}, _ = void 0) => { + const { fn: w } = s(); + return ( + 'function' == typeof o?.toJS && (o = o.toJS()), + 'function' == typeof _?.toJS && (_ = _.toJS()), + /xml/.test(i) + ? w.getXmlSampleSchema(o, u, _) + : /(yaml|yml)/.test(i) + ? w.getYamlSampleSchema(o, u, i, _) + : w.getJsonSampleSchema(o, u, i, _) + ); + }, + json_schema_5_samples = ({ getSystem: s }) => { + const o = get_json_sample_schema(s), + i = get_yaml_sample_schema(s), + u = get_xml_sample_schema(s), + _ = get_sample_schema(s); + return { + fn: { + jsonSchema5: { + inferSchema, + sampleFromSchema, + sampleFromSchemaGeneric, + createXMLExample, + memoizedSampleFromSchema: bs, + memoizedCreateXMLExample: vs, + getJsonSampleSchema: o, + getYamlSampleSchema: i, + getXmlSampleSchema: u, + getSampleSchema: _, + mergeJsonSchema + }, + inferSchema, + sampleFromSchema, + sampleFromSchemaGeneric, + createXMLExample, + memoizedSampleFromSchema: bs, + memoizedCreateXMLExample: vs, + getJsonSampleSchema: o, + getYamlSampleSchema: i, + getXmlSampleSchema: u, + getSampleSchema: _, + mergeJsonSchema + } + }; + }; + var ws = __webpack_require__(37334), + Ss = __webpack_require__.n(ws); + const xs = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'], + spec_selectors_state = (s) => s || (0, qe.Map)(), + ks = Ut(spec_selectors_state, (s) => s.get('lastError')), + Cs = Ut(spec_selectors_state, (s) => s.get('url')), + Os = Ut(spec_selectors_state, (s) => s.get('spec') || ''), + As = Ut(spec_selectors_state, (s) => s.get('specSource') || 'not-editor'), + js = Ut(spec_selectors_state, (s) => s.get('json', (0, qe.Map)())), + Is = Ut(js, (s) => s.toJS()), + Ps = Ut(spec_selectors_state, (s) => s.get('resolved', (0, qe.Map)())), + specResolvedSubtree = (s, o) => s.getIn(['resolvedSubtrees', ...o], void 0), + mergerFn = (s, o) => + qe.Map.isMap(s) && qe.Map.isMap(o) + ? o.get('$$ref') + ? o + : (0, qe.OrderedMap)().mergeWith(mergerFn, s, o) + : o, + Ms = Ut(spec_selectors_state, (s) => + (0, qe.OrderedMap)().mergeWith(mergerFn, s.get('json'), s.get('resolvedSubtrees')) + ), + spec = (s) => js(s), + Ts = Ut(spec, () => !1), + Ns = Ut(spec, (s) => returnSelfOrNewMap(s && s.get('info'))), + Rs = Ut(spec, (s) => returnSelfOrNewMap(s && s.get('externalDocs'))), + Ds = Ut(Ns, (s) => s && s.get('version')), + Ls = Ut(Ds, (s) => /v?([0-9]*)\.([0-9]*)\.([0-9]*)/i.exec(s).slice(1)), + Bs = Ut(Ms, (s) => s.get('paths')), + Fs = Ss()(['get', 'put', 'post', 'delete', 'options', 'head', 'patch']), + qs = Ut(Bs, (s) => { + if (!s || s.size < 1) return (0, qe.List)(); + let o = (0, qe.List)(); + return s && s.forEach + ? (s.forEach((s, i) => { + if (!s || !s.forEach) return {}; + s.forEach((s, u) => { + xs.indexOf(u) < 0 || + (o = o.push( + (0, qe.fromJS)({ path: i, method: u, operation: s, id: `${u}-${i}` }) + )); + }); + }), + o) + : (0, qe.List)(); + }), + $s = Ut(spec, (s) => (0, qe.Set)(s.get('consumes'))), + Vs = Ut(spec, (s) => (0, qe.Set)(s.get('produces'))), + Us = Ut(spec, (s) => s.get('security', (0, qe.List)())), + zs = Ut(spec, (s) => s.get('securityDefinitions')), + findDefinition = (s, o) => { + const i = s.getIn(['resolvedSubtrees', 'definitions', o], null), + u = s.getIn(['json', 'definitions', o], null); + return i || u || null; + }, + Ws = Ut(spec, (s) => { + const o = s.get('definitions'); + return qe.Map.isMap(o) ? o : (0, qe.Map)(); + }), + Ks = Ut(spec, (s) => s.get('basePath')), + Hs = Ut(spec, (s) => s.get('host')), + Js = Ut(spec, (s) => s.get('schemes', (0, qe.Map)())), + Gs = Ut([qs, $s, Vs], (s, o, i) => + s.map((s) => + s.update('operation', (s) => { + if (s) { + if (!qe.Map.isMap(s)) return; + return s.withMutations( + (s) => ( + s.get('consumes') || s.update('consumes', (s) => (0, qe.Set)(s).merge(o)), + s.get('produces') || s.update('produces', (s) => (0, qe.Set)(s).merge(i)), + s + ) + ); + } + return (0, qe.Map)(); + }) + ) + ), + Ys = Ut(spec, (s) => { + const o = s.get('tags', (0, qe.List)()); + return qe.List.isList(o) ? o.filter((s) => qe.Map.isMap(s)) : (0, qe.List)(); + }), + tagDetails = (s, o) => + (Ys(s) || (0, qe.List)()) + .filter(qe.Map.isMap) + .find((s) => s.get('name') === o, (0, qe.Map)()), + Xs = Ut(Gs, Ys, (s, o) => + s.reduce( + (s, o) => { + let i = (0, qe.Set)(o.getIn(['operation', 'tags'])); + return i.count() < 1 + ? s.update('default', (0, qe.List)(), (s) => s.push(o)) + : i.reduce((s, i) => s.update(i, (0, qe.List)(), (s) => s.push(o)), s); + }, + o.reduce((s, o) => s.set(o.get('name'), (0, qe.List)()), (0, qe.OrderedMap)()) + ) + ), + selectors_taggedOperations = + (s) => + ({ getConfigs: o }) => { + let { tagsSorter: i, operationsSorter: u } = o(); + return Xs(s) + .sortBy( + (s, o) => o, + (s, o) => { + let u = 'function' == typeof i ? i : It.tagsSorter[i]; + return u ? u(s, o) : null; + } + ) + .map((o, i) => { + let _ = 'function' == typeof u ? u : It.operationsSorter[u], + w = _ ? o.sort(_) : o; + return (0, qe.Map)({ tagDetails: tagDetails(s, i), operations: w }); + }); + }, + Zs = Ut(spec_selectors_state, (s) => s.get('responses', (0, qe.Map)())), + Qs = Ut(spec_selectors_state, (s) => s.get('requests', (0, qe.Map)())), + eo = Ut(spec_selectors_state, (s) => s.get('mutatedRequests', (0, qe.Map)())), + responseFor = (s, o, i) => Zs(s).getIn([o, i], null), + requestFor = (s, o, i) => Qs(s).getIn([o, i], null), + mutatedRequestFor = (s, o, i) => eo(s).getIn([o, i], null), + allowTryItOutFor = () => !0, + parameterWithMetaByIdentity = (s, o, i) => { + const u = Ms(s).getIn(['paths', ...o, 'parameters'], (0, qe.OrderedMap)()), + _ = s.getIn(['meta', 'paths', ...o, 'parameters'], (0, qe.OrderedMap)()); + return u + .map((s) => { + const o = _.get(`${i.get('in')}.${i.get('name')}`), + u = _.get(`${i.get('in')}.${i.get('name')}.hash-${i.hashCode()}`); + return (0, qe.OrderedMap)().merge(s, o, u); + }) + .find( + (s) => s.get('in') === i.get('in') && s.get('name') === i.get('name'), + (0, qe.OrderedMap)() + ); + }, + parameterInclusionSettingFor = (s, o, i, u) => { + const _ = `${u}.${i}`; + return s.getIn(['meta', 'paths', ...o, 'parameter_inclusions', _], !1); + }, + parameterWithMeta = (s, o, i, u) => { + const _ = Ms(s) + .getIn(['paths', ...o, 'parameters'], (0, qe.OrderedMap)()) + .find((s) => s.get('in') === u && s.get('name') === i, (0, qe.OrderedMap)()); + return parameterWithMetaByIdentity(s, o, _); + }, + operationWithMeta = (s, o, i) => { + const u = Ms(s).getIn(['paths', o, i], (0, qe.OrderedMap)()), + _ = s.getIn(['meta', 'paths', o, i], (0, qe.OrderedMap)()), + w = u + .get('parameters', (0, qe.List)()) + .map((u) => parameterWithMetaByIdentity(s, [o, i], u)); + return (0, qe.OrderedMap)().merge(u, _).set('parameters', w); + }; + function getParameter(s, o, i, u) { + return ( + (o = o || []), + s + .getIn(['meta', 'paths', ...o, 'parameters'], (0, qe.fromJS)([])) + .find((s) => qe.Map.isMap(s) && s.get('name') === i && s.get('in') === u) || + (0, qe.Map)() + ); + } + const to = Ut(spec, (s) => { + const o = s.get('host'); + return 'string' == typeof o && o.length > 0 && '/' !== o[0]; + }); + function parameterValues(s, o, i) { + return ( + (o = o || []), + operationWithMeta(s, ...o) + .get('parameters', (0, qe.List)()) + .reduce( + (s, o) => { + let u = i && 'body' === o.get('in') ? o.get('value_xml') : o.get('value'); + return ( + qe.List.isList(u) && (u = u.filter((s) => '' !== s)), + s.set(paramToIdentifier(o, { allowHashes: !1 }), u) + ); + }, + (0, qe.fromJS)({}) + ) + ); + } + function parametersIncludeIn(s, o = '') { + if (qe.List.isList(s)) return s.some((s) => qe.Map.isMap(s) && s.get('in') === o); + } + function parametersIncludeType(s, o = '') { + if (qe.List.isList(s)) return s.some((s) => qe.Map.isMap(s) && s.get('type') === o); + } + function contentTypeValues(s, o) { + o = o || []; + let i = Ms(s).getIn(['paths', ...o], (0, qe.fromJS)({})), + u = s.getIn(['meta', 'paths', ...o], (0, qe.fromJS)({})), + _ = currentProducesFor(s, o); + const w = i.get('parameters') || new qe.List(), + x = u.get('consumes_value') + ? u.get('consumes_value') + : parametersIncludeType(w, 'file') + ? 'multipart/form-data' + : parametersIncludeType(w, 'formData') + ? 'application/x-www-form-urlencoded' + : void 0; + return (0, qe.fromJS)({ requestContentType: x, responseContentType: _ }); + } + function currentProducesFor(s, o) { + o = o || []; + const i = Ms(s).getIn(['paths', ...o], null); + if (null === i) return; + const u = s.getIn(['meta', 'paths', ...o, 'produces_value'], null), + _ = i.getIn(['produces', 0], null); + return u || _ || 'application/json'; + } + function producesOptionsFor(s, o) { + o = o || []; + const i = Ms(s), + u = i.getIn(['paths', ...o], null); + if (null === u) return; + const [_] = o, + w = u.get('produces', null), + x = i.getIn(['paths', _, 'produces'], null), + C = i.getIn(['produces'], null); + return w || x || C; + } + function consumesOptionsFor(s, o) { + o = o || []; + const i = Ms(s), + u = i.getIn(['paths', ...o], null); + if (null === u) return; + const [_] = o, + w = u.get('consumes', null), + x = i.getIn(['paths', _, 'consumes'], null), + C = i.getIn(['consumes'], null); + return w || x || C; + } + const operationScheme = (s, o, i) => { + let u = s.get('url').match(/^([a-z][a-z0-9+\-.]*):/), + _ = Array.isArray(u) ? u[1] : null; + return s.getIn(['scheme', o, i]) || s.getIn(['scheme', '_defaultScheme']) || _ || ''; + }, + canExecuteScheme = (s, o, i) => ['http', 'https'].indexOf(operationScheme(s, o, i)) > -1, + validationErrors = (s, o) => { + o = o || []; + const i = s.getIn(['meta', 'paths', ...o, 'parameters'], (0, qe.fromJS)([])), + u = []; + if (0 === i.length) return u; + const getErrorsWithPaths = (s, o = []) => { + const getNestedErrorsWithPaths = (s, o) => { + const i = [...o, s.get('propKey') || s.get('index')]; + return qe.Map.isMap(s.get('error')) + ? getErrorsWithPaths(s.get('error'), i) + : { error: s.get('error'), path: i }; + }; + return qe.List.isList(s) + ? s.map((s) => + qe.Map.isMap(s) ? getNestedErrorsWithPaths(s, o) : { error: s, path: o } + ) + : getNestedErrorsWithPaths(s, o); + }; + return ( + i.forEach((s, o) => { + const i = o.split('.').slice(1, -1).join('.'), + _ = s.get('errors'); + if (_ && _.count()) { + getErrorsWithPaths(_).forEach(({ error: s, path: o }) => { + u.push( + ((s, o, i) => + `For '${i}'${(o = o.reduce((s, o) => ('number' == typeof o ? `${s}[${o}]` : s ? `${s}.${o}` : o), '')) ? ` at path '${o}'` : ''}: ${s}.`)( + s, + o, + i + ) + ); + }); + } + }), + u + ); + }, + validateBeforeExecute = (s, o) => 0 === validationErrors(s, o).length, + getOAS3RequiredRequestBodyContentType = (s, o) => { + let i = { requestBody: !1, requestContentType: {} }, + u = s.getIn(['resolvedSubtrees', 'paths', ...o, 'requestBody'], (0, qe.fromJS)([])); + return ( + u.size < 1 || + (u.getIn(['required']) && (i.requestBody = u.getIn(['required'])), + u + .getIn(['content']) + .entrySeq() + .forEach((s) => { + const o = s[0]; + if (s[1].getIn(['schema', 'required'])) { + const u = s[1].getIn(['schema', 'required']).toJS(); + i.requestContentType[o] = u; + } + })), + i + ); + }, + isMediaTypeSchemaPropertiesEqual = (s, o, i, u) => { + if ((i || u) && i === u) return !0; + let _ = s.getIn( + ['resolvedSubtrees', 'paths', ...o, 'requestBody', 'content'], + (0, qe.fromJS)([]) + ); + if (_.size < 2 || !i || !u) return !1; + let w = _.getIn([i, 'schema', 'properties'], (0, qe.fromJS)([])), + x = _.getIn([u, 'schema', 'properties'], (0, qe.fromJS)([])); + return !!w.equals(x); + }; + function returnSelfOrNewMap(s) { + return qe.Map.isMap(s) ? s : new qe.Map(); + } + var ro = __webpack_require__(85015), + no = __webpack_require__.n(ro), + so = __webpack_require__(38221), + oo = __webpack_require__.n(so), + io = __webpack_require__(63560), + ao = __webpack_require__.n(io), + lo = __webpack_require__(56367), + co = __webpack_require__.n(lo); + const uo = 'spec_update_spec', + po = 'spec_update_url', + ho = 'spec_update_json', + fo = 'spec_update_param', + mo = 'spec_update_empty_param_inclusion', + go = 'spec_validate_param', + yo = 'spec_set_response', + vo = 'spec_set_request', + bo = 'spec_set_mutated_request', + _o = 'spec_log_request', + Eo = 'spec_clear_response', + wo = 'spec_clear_request', + So = 'spec_clear_validate_param', + xo = 'spec_update_operation_meta_value', + ko = 'spec_update_resolved', + Co = 'spec_update_resolved_subtree', + Oo = 'set_scheme', + toStr = (s) => (no()(s) ? s : ''); + function updateSpec(s) { + const o = toStr(s).replace(/\t/g, ' '); + if ('string' == typeof s) return { type: uo, payload: o }; + } + function updateResolved(s) { + return { type: ko, payload: s }; + } + function updateUrl(s) { + return { type: po, payload: s }; + } + function updateJsonSpec(s) { + return { type: ho, payload: s }; + } + const parseToJson = + (s) => + ({ specActions: o, specSelectors: i, errActions: u }) => { + let { specStr: _ } = i, + w = null; + try { + ((s = s || _()), u.clear({ source: 'parser' }), (w = mn.load(s, { schema: nn }))); + } catch (s) { + return ( + console.error(s), + u.newSpecErr({ + source: 'parser', + level: 'error', + message: s.reason, + line: s.mark && s.mark.line ? s.mark.line + 1 : void 0 + }) + ); + } + return w && 'object' == typeof w ? o.updateJsonSpec(w) : {}; + }; + let Ao = !1; + const resolveSpec = + (s, o) => + ({ + specActions: i, + specSelectors: u, + errActions: _, + fn: { fetch: w, resolve: x, AST: C = {} }, + getConfigs: j + }) => { + Ao || + (console.warn( + 'specActions.resolveSpec is deprecated since v3.10.0 and will be removed in v4.0.0; use requestResolvedSubtree instead!' + ), + (Ao = !0)); + const { + modelPropertyMacro: L, + parameterMacro: B, + requestInterceptor: $, + responseInterceptor: V + } = j(); + (void 0 === s && (s = u.specJson()), void 0 === o && (o = u.url())); + let U = C.getLineNumberForPath ? C.getLineNumberForPath : () => {}, + z = u.specStr(); + return x({ + fetch: w, + spec: s, + baseDoc: String(new URL(o, document.baseURI)), + modelPropertyMacro: L, + parameterMacro: B, + requestInterceptor: $, + responseInterceptor: V + }).then(({ spec: s, errors: o }) => { + if ((_.clear({ type: 'thrown' }), Array.isArray(o) && o.length > 0)) { + let s = o.map( + (s) => ( + console.error(s), + (s.line = s.fullPath ? U(z, s.fullPath) : null), + (s.path = s.fullPath ? s.fullPath.join('.') : null), + (s.level = 'error'), + (s.type = 'thrown'), + (s.source = 'resolver'), + Object.defineProperty(s, 'message', { enumerable: !0, value: s.message }), + s + ) + ); + _.newThrownErrBatch(s); + } + return i.updateResolved(s); + }); + }; + let jo = []; + const Io = oo()(() => { + const s = jo.reduce( + (s, { path: o, system: i }) => (s.has(i) || s.set(i, []), s.get(i).push(o), s), + new Map() + ); + ((jo = []), + s.forEach(async (s, o) => { + if (!o) + return void console.error( + "debResolveSubtrees: don't have a system to operate on, aborting." + ); + if (!o.fn.resolveSubtree) + return void console.error( + 'Error: Swagger-Client did not provide a `resolveSubtree` method, doing nothing.' + ); + const { + errActions: i, + errSelectors: u, + fn: { resolveSubtree: _, fetch: w, AST: x = {} }, + specSelectors: C, + specActions: j + } = o, + L = x.getLineNumberForPath ?? Ss()(void 0), + B = C.specStr(), + { + modelPropertyMacro: $, + parameterMacro: V, + requestInterceptor: U, + responseInterceptor: z + } = o.getConfigs(); + try { + const o = await s.reduce( + async (s, o) => { + let { resultMap: x, specWithCurrentSubtrees: j } = await s; + const { errors: Y, spec: Z } = await _(j, o, { + baseDoc: String(new URL(C.url(), document.baseURI)), + modelPropertyMacro: $, + parameterMacro: V, + requestInterceptor: U, + responseInterceptor: z + }); + if ( + (u.allErrors().size && + i.clearBy( + (s) => + 'thrown' !== s.get('type') || + 'resolver' !== s.get('source') || + !s.get('fullPath').every((s, i) => s === o[i] || void 0 === o[i]) + ), + Array.isArray(Y) && Y.length > 0) + ) { + let s = Y.map( + (s) => ( + (s.line = s.fullPath ? L(B, s.fullPath) : null), + (s.path = s.fullPath ? s.fullPath.join('.') : null), + (s.level = 'error'), + (s.type = 'thrown'), + (s.source = 'resolver'), + Object.defineProperty(s, 'message', { + enumerable: !0, + value: s.message + }), + s + ) + ); + i.newThrownErrBatch(s); + } + return ( + Z && + C.isOAS3() && + 'components' === o[0] && + 'securitySchemes' === o[1] && + (await Promise.all( + Object.values(Z) + .filter((s) => 'openIdConnect' === s.type) + .map(async (s) => { + const o = { + url: s.openIdConnectUrl, + requestInterceptor: U, + responseInterceptor: z + }; + try { + const i = await w(o); + i instanceof Error || i.status >= 400 + ? console.error(i.statusText + ' ' + o.url) + : (s.openIdConnectData = JSON.parse(i.text)); + } catch (s) { + console.error(s); + } + }) + )), + ao()(x, o, Z), + (j = co()(o, Z, j)), + { resultMap: x, specWithCurrentSubtrees: j } + ); + }, + Promise.resolve({ + resultMap: (C.specResolvedSubtree([]) || (0, qe.Map)()).toJS(), + specWithCurrentSubtrees: C.specJS() + }) + ); + j.updateResolvedSubtree([], o.resultMap); + } catch (s) { + console.error(s); + } + })); + }, 35), + requestResolvedSubtree = (s) => (o) => { + jo.find(({ path: i, system: u }) => u === o && i.toString() === s.toString()) || + (jo.push({ path: s, system: o }), Io()); + }; + function changeParam(s, o, i, u, _) { + return { type: fo, payload: { path: s, value: u, paramName: o, paramIn: i, isXml: _ } }; + } + function changeParamByIdentity(s, o, i, u) { + return { type: fo, payload: { path: s, param: o, value: i, isXml: u } }; + } + const updateResolvedSubtree = (s, o) => ({ type: Co, payload: { path: s, value: o } }), + invalidateResolvedSubtreeCache = () => ({ + type: Co, + payload: { path: [], value: (0, qe.Map)() } + }), + validateParams = (s, o) => ({ type: go, payload: { pathMethod: s, isOAS3: o } }), + updateEmptyParamInclusion = (s, o, i, u) => ({ + type: mo, + payload: { pathMethod: s, paramName: o, paramIn: i, includeEmptyValue: u } + }); + function clearValidateParams(s) { + return { type: So, payload: { pathMethod: s } }; + } + function changeConsumesValue(s, o) { + return { type: xo, payload: { path: s, value: o, key: 'consumes_value' } }; + } + function changeProducesValue(s, o) { + return { type: xo, payload: { path: s, value: o, key: 'produces_value' } }; + } + const setResponse = (s, o, i) => ({ payload: { path: s, method: o, res: i }, type: yo }), + setRequest = (s, o, i) => ({ payload: { path: s, method: o, req: i }, type: vo }), + setMutatedRequest = (s, o, i) => ({ payload: { path: s, method: o, req: i }, type: bo }), + logRequest = (s) => ({ payload: s, type: _o }), + executeRequest = + (s) => + ({ fn: o, specActions: i, specSelectors: u, getConfigs: _, oas3Selectors: w }) => { + let { pathName: x, method: C, operation: j } = s, + { requestInterceptor: L, responseInterceptor: B } = _(), + $ = j.toJS(); + if ( + (j && + j.get('parameters') && + j + .get('parameters') + .filter((s) => s && !0 === s.get('allowEmptyValue')) + .forEach((o) => { + if (u.parameterInclusionSettingFor([x, C], o.get('name'), o.get('in'))) { + s.parameters = s.parameters || {}; + const i = paramToValue(o, s.parameters); + (!i || (i && 0 === i.size)) && (s.parameters[o.get('name')] = ''); + } + }), + (s.contextUrl = Mt()(u.url()).toString()), + $ && $.operationId + ? (s.operationId = $.operationId) + : $ && x && C && (s.operationId = o.opId($, x, C)), + u.isOAS3()) + ) { + const o = `${x}:${C}`; + s.server = w.selectedServer(o) || w.selectedServer(); + const i = w.serverVariables({ server: s.server, namespace: o }).toJS(), + u = w.serverVariables({ server: s.server }).toJS(); + ((s.serverVariables = Object.keys(i).length ? i : u), + (s.requestContentType = w.requestContentType(x, C)), + (s.responseContentType = w.responseContentType(x, C) || '*/*')); + const _ = w.requestBodyValue(x, C), + j = w.requestBodyInclusionSetting(x, C); + _ && _.toJS + ? (s.requestBody = _.map((s) => (qe.Map.isMap(s) ? s.get('value') : s)) + .filter( + (s, o) => (Array.isArray(s) ? 0 !== s.length : !isEmptyValue(s)) || j.get(o) + ) + .toJS()) + : (s.requestBody = _); + } + let V = Object.assign({}, s); + ((V = o.buildRequest(V)), i.setRequest(s.pathName, s.method, V)); + ((s.requestInterceptor = async (o) => { + let u = await L.apply(void 0, [o]), + _ = Object.assign({}, u); + return (i.setMutatedRequest(s.pathName, s.method, _), u); + }), + (s.responseInterceptor = B)); + const U = Date.now(); + return o + .execute(s) + .then((o) => { + ((o.duration = Date.now() - U), i.setResponse(s.pathName, s.method, o)); + }) + .catch((o) => { + ('Failed to fetch' === o.message && + ((o.name = ''), + (o.message = + '**Failed to fetch.** \n**Possible Reasons:** \n - CORS \n - Network Failure \n - URL scheme must be "http" or "https" for CORS request.')), + i.setResponse(s.pathName, s.method, { error: !0, err: o })); + }); + }, + actions_execute = + ({ path: s, method: o, ...i } = {}) => + (u) => { + let { + fn: { fetch: _ }, + specSelectors: w, + specActions: x + } = u, + C = w.specJsonWithResolvedSubtrees().toJS(), + j = w.operationScheme(s, o), + { requestContentType: L, responseContentType: B } = w + .contentTypeValues([s, o]) + .toJS(), + $ = /xml/i.test(L), + V = w.parameterValues([s, o], $).toJS(); + return x.executeRequest({ + ...i, + fetch: _, + spec: C, + pathName: s, + method: o, + parameters: V, + requestContentType: L, + scheme: j, + responseContentType: B + }); + }; + function clearResponse(s, o) { + return { type: Eo, payload: { path: s, method: o } }; + } + function clearRequest(s, o) { + return { type: wo, payload: { path: s, method: o } }; + } + function setScheme(s, o, i) { + return { type: Oo, payload: { scheme: s, path: o, method: i } }; + } + const Po = { + [uo]: (s, o) => ('string' == typeof o.payload ? s.set('spec', o.payload) : s), + [po]: (s, o) => s.set('url', o.payload + ''), + [ho]: (s, o) => s.set('json', fromJSOrdered(o.payload)), + [ko]: (s, o) => s.setIn(['resolved'], fromJSOrdered(o.payload)), + [Co]: (s, o) => { + const { value: i, path: u } = o.payload; + return s.setIn(['resolvedSubtrees', ...u], fromJSOrdered(i)); + }, + [fo]: (s, { payload: o }) => { + let { path: i, paramName: u, paramIn: _, param: w, value: x, isXml: C } = o, + j = w ? paramToIdentifier(w) : `${_}.${u}`; + const L = C ? 'value_xml' : 'value'; + return s.setIn(['meta', 'paths', ...i, 'parameters', j, L], (0, qe.fromJS)(x)); + }, + [mo]: (s, { payload: o }) => { + let { pathMethod: i, paramName: u, paramIn: _, includeEmptyValue: w } = o; + if (!u || !_) + return ( + console.warn( + 'Warning: UPDATE_EMPTY_PARAM_INCLUSION could not generate a paramKey.' + ), + s + ); + const x = `${_}.${u}`; + return s.setIn(['meta', 'paths', ...i, 'parameter_inclusions', x], w); + }, + [go]: (s, { payload: { pathMethod: o, isOAS3: i } }) => { + const u = Ms(s).getIn(['paths', ...o]), + _ = parameterValues(s, o).toJS(); + return s.updateIn(['meta', 'paths', ...o, 'parameters'], (0, qe.fromJS)({}), (w) => + u.get('parameters', (0, qe.List)()).reduce((u, w) => { + const x = paramToValue(w, _), + C = parameterInclusionSettingFor(s, o, w.get('name'), w.get('in')), + j = ((s, o, { isOAS3: i = !1, bypassRequiredCheck: u = !1 } = {}) => { + let _ = s.get('required'), + { schema: w, parameterContentMediaType: x } = getParameterSchema(s, { + isOAS3: i + }); + return validateValueBySchema(o, w, _, u, x); + })(w, x, { bypassRequiredCheck: C, isOAS3: i }); + return u.setIn([paramToIdentifier(w), 'errors'], (0, qe.fromJS)(j)); + }, w) + ); + }, + [So]: (s, { payload: { pathMethod: o } }) => + s.updateIn(['meta', 'paths', ...o, 'parameters'], (0, qe.fromJS)([]), (s) => + s.map((s) => s.set('errors', (0, qe.fromJS)([]))) + ), + [yo]: (s, { payload: { res: o, path: i, method: u } }) => { + let _; + ((_ = o.error + ? Object.assign( + { + error: !0, + name: o.err.name, + message: o.err.message, + statusCode: o.err.statusCode + }, + o.err.response + ) + : o), + (_.headers = _.headers || {})); + let w = s.setIn(['responses', i, u], fromJSOrdered(_)); + return ( + at.Blob && + _.data instanceof at.Blob && + (w = w.setIn(['responses', i, u, 'text'], _.data)), + w + ); + }, + [vo]: (s, { payload: { req: o, path: i, method: u } }) => + s.setIn(['requests', i, u], fromJSOrdered(o)), + [bo]: (s, { payload: { req: o, path: i, method: u } }) => + s.setIn(['mutatedRequests', i, u], fromJSOrdered(o)), + [xo]: (s, { payload: { path: o, value: i, key: u } }) => { + let _ = ['paths', ...o], + w = ['meta', 'paths', ...o]; + return s.getIn(['json', ..._]) || + s.getIn(['resolved', ..._]) || + s.getIn(['resolvedSubtrees', ..._]) + ? s.setIn([...w, u], (0, qe.fromJS)(i)) + : s; + }, + [Eo]: (s, { payload: { path: o, method: i } }) => s.deleteIn(['responses', o, i]), + [wo]: (s, { payload: { path: o, method: i } }) => s.deleteIn(['requests', o, i]), + [Oo]: (s, { payload: { scheme: o, path: i, method: u } }) => + i && u + ? s.setIn(['scheme', i, u], o) + : i || u + ? void 0 + : s.setIn(['scheme', '_defaultScheme'], o) + }, + wrap_actions_updateSpec = + (s, { specActions: o }) => + (...i) => { + (s(...i), o.parseToJson(...i)); + }, + wrap_actions_updateJsonSpec = + (s, { specActions: o }) => + (...i) => { + (s(...i), o.invalidateResolvedSubtreeCache()); + const [u] = i, + _ = jn()(u, ['paths']) || {}; + (Object.keys(_).forEach((s) => { + jn()(_, [s]).$ref && o.requestResolvedSubtree(['paths', s]); + }), + o.requestResolvedSubtree(['components', 'securitySchemes'])); + }, + wrap_actions_executeRequest = + (s, { specActions: o }) => + (i) => (o.logRequest(i), s(i)), + wrap_actions_validateParams = + (s, { specSelectors: o }) => + (i) => + s(i, o.isOAS3()), + plugins_spec = () => ({ + statePlugins: { + spec: { + wrapActions: { ...ee }, + reducers: { ...Po }, + actions: { ...Z }, + selectors: { ...Y } + } + } + }); + var Mo = (function () { + var extendStatics = function (s, o) { + return ( + (extendStatics = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function (s, o) { + s.__proto__ = o; + }) || + function (s, o) { + for (var i in o) o.hasOwnProperty(i) && (s[i] = o[i]); + }), + extendStatics(s, o) + ); + }; + return function (s, o) { + function __() { + this.constructor = s; + } + (extendStatics(s, o), + (s.prototype = + null === o ? Object.create(o) : ((__.prototype = o.prototype), new __()))); + }; + })(), + To = Object.prototype.hasOwnProperty; + function module_helpers_hasOwnProperty(s, o) { + return To.call(s, o); + } + function _objectKeys(s) { + if (Array.isArray(s)) { + for (var o = new Array(s.length), i = 0; i < o.length; i++) o[i] = '' + i; + return o; + } + if (Object.keys) return Object.keys(s); + var u = []; + for (var _ in s) module_helpers_hasOwnProperty(s, _) && u.push(_); + return u; + } + function _deepClone(s) { + switch (typeof s) { + case 'object': + return JSON.parse(JSON.stringify(s)); + case 'undefined': + return null; + default: + return s; + } + } + function helpers_isInteger(s) { + for (var o, i = 0, u = s.length; i < u; ) { + if (!((o = s.charCodeAt(i)) >= 48 && o <= 57)) return !1; + i++; + } + return !0; + } + function escapePathComponent(s) { + return -1 === s.indexOf('/') && -1 === s.indexOf('~') + ? s + : s.replace(/~/g, '~0').replace(/\//g, '~1'); + } + function unescapePathComponent(s) { + return s.replace(/~1/g, '/').replace(/~0/g, '~'); + } + function hasUndefined(s) { + if (void 0 === s) return !0; + if (s) + if (Array.isArray(s)) { + for (var o = 0, i = s.length; o < i; o++) if (hasUndefined(s[o])) return !0; + } else if ('object' == typeof s) + for (var u = _objectKeys(s), _ = u.length, w = 0; w < _; w++) + if (hasUndefined(s[u[w]])) return !0; + return !1; + } + function patchErrorMessageFormatter(s, o) { + var i = [s]; + for (var u in o) { + var _ = 'object' == typeof o[u] ? JSON.stringify(o[u], null, 2) : o[u]; + void 0 !== _ && i.push(u + ': ' + _); + } + return i.join('\n'); + } + var No = (function (s) { + function PatchError(o, i, u, _, w) { + var x = this.constructor, + C = + s.call( + this, + patchErrorMessageFormatter(o, { name: i, index: u, operation: _, tree: w }) + ) || this; + return ( + (C.name = i), + (C.index = u), + (C.operation = _), + (C.tree = w), + Object.setPrototypeOf(C, x.prototype), + (C.message = patchErrorMessageFormatter(o, { + name: i, + index: u, + operation: _, + tree: w + })), + C + ); + } + return (Mo(PatchError, s), PatchError); + })(Error), + Ro = No, + Do = _deepClone, + Lo = { + add: function (s, o, i) { + return ((s[o] = this.value), { newDocument: i }); + }, + remove: function (s, o, i) { + var u = s[o]; + return (delete s[o], { newDocument: i, removed: u }); + }, + replace: function (s, o, i) { + var u = s[o]; + return ((s[o] = this.value), { newDocument: i, removed: u }); + }, + move: function (s, o, i) { + var u = getValueByPointer(i, this.path); + u && (u = _deepClone(u)); + var _ = applyOperation(i, { op: 'remove', path: this.from }).removed; + return ( + applyOperation(i, { op: 'add', path: this.path, value: _ }), + { newDocument: i, removed: u } + ); + }, + copy: function (s, o, i) { + var u = getValueByPointer(i, this.from); + return ( + applyOperation(i, { op: 'add', path: this.path, value: _deepClone(u) }), + { newDocument: i } + ); + }, + test: function (s, o, i) { + return { newDocument: i, test: _areEquals(s[o], this.value) }; + }, + _get: function (s, o, i) { + return ((this.value = s[o]), { newDocument: i }); + } + }, + Bo = { + add: function (s, o, i) { + return ( + helpers_isInteger(o) ? s.splice(o, 0, this.value) : (s[o] = this.value), + { newDocument: i, index: o } + ); + }, + remove: function (s, o, i) { + return { newDocument: i, removed: s.splice(o, 1)[0] }; + }, + replace: function (s, o, i) { + var u = s[o]; + return ((s[o] = this.value), { newDocument: i, removed: u }); + }, + move: Lo.move, + copy: Lo.copy, + test: Lo.test, + _get: Lo._get + }; + function getValueByPointer(s, o) { + if ('' == o) return s; + var i = { op: '_get', path: o }; + return (applyOperation(s, i), i.value); + } + function applyOperation(s, o, i, u, _, w) { + if ( + (void 0 === i && (i = !1), + void 0 === u && (u = !0), + void 0 === _ && (_ = !0), + void 0 === w && (w = 0), + i && ('function' == typeof i ? i(o, 0, s, o.path) : validator(o, 0)), + '' === o.path) + ) { + var x = { newDocument: s }; + if ('add' === o.op) return ((x.newDocument = o.value), x); + if ('replace' === o.op) return ((x.newDocument = o.value), (x.removed = s), x); + if ('move' === o.op || 'copy' === o.op) + return ( + (x.newDocument = getValueByPointer(s, o.from)), + 'move' === o.op && (x.removed = s), + x + ); + if ('test' === o.op) { + if (((x.test = _areEquals(s, o.value)), !1 === x.test)) + throw new Ro('Test operation failed', 'TEST_OPERATION_FAILED', w, o, s); + return ((x.newDocument = s), x); + } + if ('remove' === o.op) return ((x.removed = s), (x.newDocument = null), x); + if ('_get' === o.op) return ((o.value = s), x); + if (i) + throw new Ro( + 'Operation `op` property is not one of operations defined in RFC-6902', + 'OPERATION_OP_INVALID', + w, + o, + s + ); + return x; + } + u || (s = _deepClone(s)); + var C = (o.path || '').split('/'), + j = s, + L = 1, + B = C.length, + $ = void 0, + V = void 0, + U = void 0; + for (U = 'function' == typeof i ? i : validator; ; ) { + if ( + ((V = C[L]) && -1 != V.indexOf('~') && (V = unescapePathComponent(V)), + _ && ('__proto__' == V || ('prototype' == V && L > 0 && 'constructor' == C[L - 1]))) + ) + throw new TypeError( + 'JSON-Patch: modifying `__proto__` or `constructor/prototype` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README' + ); + if ( + (i && + void 0 === $ && + (void 0 === j[V] ? ($ = C.slice(0, L).join('/')) : L == B - 1 && ($ = o.path), + void 0 !== $ && U(o, 0, s, $)), + L++, + Array.isArray(j)) + ) { + if ('-' === V) V = j.length; + else { + if (i && !helpers_isInteger(V)) + throw new Ro( + 'Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index', + 'OPERATION_PATH_ILLEGAL_ARRAY_INDEX', + w, + o, + s + ); + helpers_isInteger(V) && (V = ~~V); + } + if (L >= B) { + if (i && 'add' === o.op && V > j.length) + throw new Ro( + 'The specified index MUST NOT be greater than the number of elements in the array', + 'OPERATION_VALUE_OUT_OF_BOUNDS', + w, + o, + s + ); + if (!1 === (x = Bo[o.op].call(o, j, V, s)).test) + throw new Ro('Test operation failed', 'TEST_OPERATION_FAILED', w, o, s); + return x; + } + } else if (L >= B) { + if (!1 === (x = Lo[o.op].call(o, j, V, s)).test) + throw new Ro('Test operation failed', 'TEST_OPERATION_FAILED', w, o, s); + return x; + } + if (((j = j[V]), i && L < B && (!j || 'object' != typeof j))) + throw new Ro( + 'Cannot perform operation at the desired path', + 'OPERATION_PATH_UNRESOLVABLE', + w, + o, + s + ); + } + } + function applyPatch(s, o, i, u, _) { + if ((void 0 === u && (u = !0), void 0 === _ && (_ = !0), i && !Array.isArray(o))) + throw new Ro('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY'); + u || (s = _deepClone(s)); + for (var w = new Array(o.length), x = 0, C = o.length; x < C; x++) + ((w[x] = applyOperation(s, o[x], i, !0, _, x)), (s = w[x].newDocument)); + return ((w.newDocument = s), w); + } + function applyReducer(s, o, i) { + var u = applyOperation(s, o); + if (!1 === u.test) + throw new Ro('Test operation failed', 'TEST_OPERATION_FAILED', i, o, s); + return u.newDocument; + } + function validator(s, o, i, u) { + if ('object' != typeof s || null === s || Array.isArray(s)) + throw new Ro('Operation is not an object', 'OPERATION_NOT_AN_OBJECT', o, s, i); + if (!Lo[s.op]) + throw new Ro( + 'Operation `op` property is not one of operations defined in RFC-6902', + 'OPERATION_OP_INVALID', + o, + s, + i + ); + if ('string' != typeof s.path) + throw new Ro( + 'Operation `path` property is not a string', + 'OPERATION_PATH_INVALID', + o, + s, + i + ); + if (0 !== s.path.indexOf('/') && s.path.length > 0) + throw new Ro( + 'Operation `path` property must start with "/"', + 'OPERATION_PATH_INVALID', + o, + s, + i + ); + if (('move' === s.op || 'copy' === s.op) && 'string' != typeof s.from) + throw new Ro( + 'Operation `from` property is not present (applicable in `move` and `copy` operations)', + 'OPERATION_FROM_REQUIRED', + o, + s, + i + ); + if (('add' === s.op || 'replace' === s.op || 'test' === s.op) && void 0 === s.value) + throw new Ro( + 'Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)', + 'OPERATION_VALUE_REQUIRED', + o, + s, + i + ); + if (('add' === s.op || 'replace' === s.op || 'test' === s.op) && hasUndefined(s.value)) + throw new Ro( + 'Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)', + 'OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED', + o, + s, + i + ); + if (i) + if ('add' == s.op) { + var _ = s.path.split('/').length, + w = u.split('/').length; + if (_ !== w + 1 && _ !== w) + throw new Ro( + 'Cannot perform an `add` operation at the desired path', + 'OPERATION_PATH_CANNOT_ADD', + o, + s, + i + ); + } else if ('replace' === s.op || 'remove' === s.op || '_get' === s.op) { + if (s.path !== u) + throw new Ro( + 'Cannot perform the operation at a path that does not exist', + 'OPERATION_PATH_UNRESOLVABLE', + o, + s, + i + ); + } else if ('move' === s.op || 'copy' === s.op) { + var x = validate([{ op: '_get', path: s.from, value: void 0 }], i); + if (x && 'OPERATION_PATH_UNRESOLVABLE' === x.name) + throw new Ro( + 'Cannot perform the operation from a path that does not exist', + 'OPERATION_FROM_UNRESOLVABLE', + o, + s, + i + ); + } + } + function validate(s, o, i) { + try { + if (!Array.isArray(s)) + throw new Ro('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY'); + if (o) applyPatch(_deepClone(o), _deepClone(s), i || !0); + else { + i = i || validator; + for (var u = 0; u < s.length; u++) i(s[u], u, o, void 0); + } + } catch (s) { + if (s instanceof Ro) return s; + throw s; + } + } + function _areEquals(s, o) { + if (s === o) return !0; + if (s && o && 'object' == typeof s && 'object' == typeof o) { + var i, + u, + _, + w = Array.isArray(s), + x = Array.isArray(o); + if (w && x) { + if ((u = s.length) != o.length) return !1; + for (i = u; 0 != i--; ) if (!_areEquals(s[i], o[i])) return !1; + return !0; + } + if (w != x) return !1; + var C = Object.keys(s); + if ((u = C.length) !== Object.keys(o).length) return !1; + for (i = u; 0 != i--; ) if (!o.hasOwnProperty(C[i])) return !1; + for (i = u; 0 != i--; ) if (!_areEquals(s[(_ = C[i])], o[_])) return !1; + return !0; + } + return s != s && o != o; + } + var Fo = new WeakMap(), + qo = function qo(s) { + ((this.observers = new Map()), (this.obj = s)); + }, + $o = function $o(s, o) { + ((this.callback = s), (this.observer = o)); + }; + function unobserve(s, o) { + o.unobserve(); + } + function observe(s, o) { + var i, + u = (function getMirror(s) { + return Fo.get(s); + })(s); + if (u) { + var _ = (function getObserverFromMirror(s, o) { + return s.observers.get(o); + })(u, o); + i = _ && _.observer; + } else ((u = new qo(s)), Fo.set(s, u)); + if (i) return i; + if (((i = {}), (u.value = _deepClone(s)), o)) { + ((i.callback = o), (i.next = null)); + var dirtyCheck = function () { + generate(i); + }, + fastCheck = function () { + (clearTimeout(i.next), (i.next = setTimeout(dirtyCheck))); + }; + 'undefined' != typeof window && + (window.addEventListener('mouseup', fastCheck), + window.addEventListener('keyup', fastCheck), + window.addEventListener('mousedown', fastCheck), + window.addEventListener('keydown', fastCheck), + window.addEventListener('change', fastCheck)); + } + return ( + (i.patches = []), + (i.object = s), + (i.unobserve = function () { + (generate(i), + clearTimeout(i.next), + (function removeObserverFromMirror(s, o) { + s.observers.delete(o.callback); + })(u, i), + 'undefined' != typeof window && + (window.removeEventListener('mouseup', fastCheck), + window.removeEventListener('keyup', fastCheck), + window.removeEventListener('mousedown', fastCheck), + window.removeEventListener('keydown', fastCheck), + window.removeEventListener('change', fastCheck))); + }), + u.observers.set(o, new $o(o, i)), + i + ); + } + function generate(s, o) { + void 0 === o && (o = !1); + var i = Fo.get(s.object); + (_generate(i.value, s.object, s.patches, '', o), + s.patches.length && applyPatch(i.value, s.patches)); + var u = s.patches; + return (u.length > 0 && ((s.patches = []), s.callback && s.callback(u)), u); + } + function _generate(s, o, i, u, _) { + if (o !== s) { + 'function' == typeof o.toJSON && (o = o.toJSON()); + for ( + var w = _objectKeys(o), x = _objectKeys(s), C = !1, j = x.length - 1; + j >= 0; + j-- + ) { + var L = s[($ = x[j])]; + if ( + !module_helpers_hasOwnProperty(o, $) || + (void 0 === o[$] && void 0 !== L && !1 === Array.isArray(o)) + ) + Array.isArray(s) === Array.isArray(o) + ? (_ && + i.push({ + op: 'test', + path: u + '/' + escapePathComponent($), + value: _deepClone(L) + }), + i.push({ op: 'remove', path: u + '/' + escapePathComponent($) }), + (C = !0)) + : (_ && i.push({ op: 'test', path: u, value: s }), + i.push({ op: 'replace', path: u, value: o }), + !0); + else { + var B = o[$]; + 'object' == typeof L && + null != L && + 'object' == typeof B && + null != B && + Array.isArray(L) === Array.isArray(B) + ? _generate(L, B, i, u + '/' + escapePathComponent($), _) + : L !== B && + (_ && + i.push({ + op: 'test', + path: u + '/' + escapePathComponent($), + value: _deepClone(L) + }), + i.push({ + op: 'replace', + path: u + '/' + escapePathComponent($), + value: _deepClone(B) + })); + } + } + if (C || w.length != x.length) + for (j = 0; j < w.length; j++) { + var $; + module_helpers_hasOwnProperty(s, ($ = w[j])) || + void 0 === o[$] || + i.push({ + op: 'add', + path: u + '/' + escapePathComponent($), + value: _deepClone(o[$]) + }); + } + } + } + function compare(s, o, i) { + void 0 === i && (i = !1); + var u = []; + return (_generate(s, o, u, '', i), u); + } + Object.assign({}, ie, ae, { + JsonPatchError: No, + deepClone: _deepClone, + escapePathComponent, + unescapePathComponent + }); + var Vo = __webpack_require__(14744), + Uo = __webpack_require__.n(Vo); + const zo = { + add: function add(s, o) { + return { op: 'add', path: s, value: o }; + }, + replace, + remove: function remove(s) { + return { op: 'remove', path: s }; + }, + merge: function lib_merge(s, o) { + return { type: 'mutation', op: 'merge', path: s, value: o }; + }, + mergeDeep: function mergeDeep(s, o) { + return { type: 'mutation', op: 'mergeDeep', path: s, value: o }; + }, + context: function context(s, o) { + return { type: 'context', path: s, value: o }; + }, + getIn: function lib_getIn(s, o) { + return o.reduce((s, o) => (void 0 !== o && s ? s[o] : s), s); + }, + applyPatch: function lib_applyPatch(s, o, i) { + if ( + ((i = i || {}), + 'merge' === (o = { ...o, path: o.path && normalizeJSONPath(o.path) }).op) + ) { + const i = getInByJsonPath(s, o.path); + (Object.assign(i, o.value), applyPatch(s, [replace(o.path, i)])); + } else if ('mergeDeep' === o.op) { + const i = getInByJsonPath(s, o.path), + u = Uo()(i, o.value); + s = applyPatch(s, [replace(o.path, u)]).newDocument; + } else if ('add' === o.op && '' === o.path && lib_isObject(o.value)) { + applyPatch( + s, + Object.keys(o.value).reduce( + (s, i) => ( + s.push({ op: 'add', path: `/${normalizeJSONPath(i)}`, value: o.value[i] }), + s + ), + [] + ) + ); + } else if ('replace' === o.op && '' === o.path) { + let { value: u } = o; + (i.allowMetaPatches && + o.meta && + isAdditiveMutation(o) && + (Array.isArray(o.value) || lib_isObject(o.value)) && + (u = { ...u, ...o.meta }), + (s = u)); + } else if ( + (applyPatch(s, [o]), + i.allowMetaPatches && + o.meta && + isAdditiveMutation(o) && + (Array.isArray(o.value) || lib_isObject(o.value))) + ) { + const i = { ...getInByJsonPath(s, o.path), ...o.meta }; + applyPatch(s, [replace(o.path, i)]); + } + return s; + }, + parentPathMatch: function parentPathMatch(s, o) { + if (!Array.isArray(o)) return !1; + for (let i = 0, u = o.length; i < u; i += 1) if (o[i] !== s[i]) return !1; + return !0; + }, + flatten, + fullyNormalizeArray: function fullyNormalizeArray(s) { + return cleanArray(flatten(lib_normalizeArray(s))); + }, + normalizeArray: lib_normalizeArray, + isPromise: function isPromise(s) { + return lib_isObject(s) && lib_isFunction(s.then); + }, + forEachNew: function forEachNew(s, o) { + try { + return forEachNewPatch(s, forEach, o); + } catch (s) { + return s; + } + }, + forEachNewPrimitive: function forEachNewPrimitive(s, o) { + try { + return forEachNewPatch(s, forEachPrimitive, o); + } catch (s) { + return s; + } + }, + isJsonPatch, + isContextPatch: function isContextPatch(s) { + return isPatch(s) && 'context' === s.type; + }, + isPatch, + isMutation, + isAdditiveMutation, + isGenerator: function isGenerator(s) { + return '[object GeneratorFunction]' === Object.prototype.toString.call(s); + }, + isFunction: lib_isFunction, + isObject: lib_isObject, + isError: function lib_isError(s) { + return s instanceof Error; + } + }; + function normalizeJSONPath(s) { + return Array.isArray(s) + ? s.length < 1 + ? '' + : `/${s.map((s) => (s + '').replace(/~/g, '~0').replace(/\//g, '~1')).join('/')}` + : s; + } + function replace(s, o, i) { + return { op: 'replace', path: s, value: o, meta: i }; + } + function forEachNewPatch(s, o, i) { + return cleanArray( + flatten(s.filter(isAdditiveMutation).map((s) => o(s.value, i, s.path)) || []) + ); + } + function forEachPrimitive(s, o, i) { + return ( + (i = i || []), + Array.isArray(s) + ? s.map((s, u) => forEachPrimitive(s, o, i.concat(u))) + : lib_isObject(s) + ? Object.keys(s).map((u) => forEachPrimitive(s[u], o, i.concat(u))) + : o(s, i[i.length - 1], i) + ); + } + function forEach(s, o, i) { + let u = []; + if ((i = i || []).length > 0) { + const _ = o(s, i[i.length - 1], i); + _ && (u = u.concat(_)); + } + if (Array.isArray(s)) { + const _ = s.map((s, u) => forEach(s, o, i.concat(u))); + _ && (u = u.concat(_)); + } else if (lib_isObject(s)) { + const _ = Object.keys(s).map((u) => forEach(s[u], o, i.concat(u))); + _ && (u = u.concat(_)); + } + return ((u = flatten(u)), u); + } + function lib_normalizeArray(s) { + return Array.isArray(s) ? s : [s]; + } + function flatten(s) { + return [].concat(...s.map((s) => (Array.isArray(s) ? flatten(s) : s))); + } + function cleanArray(s) { + return s.filter((s) => void 0 !== s); + } + function lib_isObject(s) { + return s && 'object' == typeof s; + } + function lib_isFunction(s) { + return s && 'function' == typeof s; + } + function isJsonPatch(s) { + if (isPatch(s)) { + const { op: o } = s; + return 'add' === o || 'remove' === o || 'replace' === o; + } + return !1; + } + function isMutation(s) { + return isJsonPatch(s) || (isPatch(s) && 'mutation' === s.type); + } + function isAdditiveMutation(s) { + return ( + isMutation(s) && + ('add' === s.op || 'replace' === s.op || 'merge' === s.op || 'mergeDeep' === s.op) + ); + } + function isPatch(s) { + return s && 'object' == typeof s; + } + function getInByJsonPath(s, o) { + try { + return getValueByPointer(s, o); + } catch (s) { + return (console.error(s), {}); + } + } + var Wo = __webpack_require__(48675); + const Ko = class ApiDOMAggregateError extends Wo { + constructor(s, o, i) { + if ( + (super(s, o, i), + (this.name = this.constructor.name), + 'string' == typeof o && (this.message = o), + 'function' == typeof Error.captureStackTrace + ? Error.captureStackTrace(this, this.constructor) + : (this.stack = new Error(o).stack), + null != i && 'object' == typeof i && Object.hasOwn(i, 'cause') && !('cause' in this)) + ) { + const { cause: s } = i; + ((this.cause = s), + s instanceof Error && + 'stack' in s && + (this.stack = `${this.stack}\nCAUSE: ${s.stack}`)); + } + } + }; + class ApiDOMError extends Error { + static [Symbol.hasInstance](s) { + return ( + super[Symbol.hasInstance](s) || Function.prototype[Symbol.hasInstance].call(Ko, s) + ); + } + constructor(s, o) { + if ( + (super(s, o), + (this.name = this.constructor.name), + 'string' == typeof s && (this.message = s), + 'function' == typeof Error.captureStackTrace + ? Error.captureStackTrace(this, this.constructor) + : (this.stack = new Error(s).stack), + null != o && 'object' == typeof o && Object.hasOwn(o, 'cause') && !('cause' in this)) + ) { + const { cause: s } = o; + ((this.cause = s), + s instanceof Error && + 'stack' in s && + (this.stack = `${this.stack}\nCAUSE: ${s.stack}`)); + } + } + } + const Ho = ApiDOMError; + const Jo = class ApiDOMStructuredError extends Ho { + constructor(s, o) { + if ((super(s, o), null != o && 'object' == typeof o)) { + const { cause: s, ...i } = o; + Object.assign(this, i); + } + } + }; + var Go = __webpack_require__(65606); + function _isPlaceholder(s) { + return null != s && 'object' == typeof s && !0 === s['@@functional/placeholder']; + } + function _curry1(s) { + return function f1(o) { + return 0 === arguments.length || _isPlaceholder(o) ? f1 : s.apply(this, arguments); + }; + } + function _curry2(s) { + return function f2(o, i) { + switch (arguments.length) { + case 0: + return f2; + case 1: + return _isPlaceholder(o) + ? f2 + : _curry1(function (i) { + return s(o, i); + }); + default: + return _isPlaceholder(o) && _isPlaceholder(i) + ? f2 + : _isPlaceholder(o) + ? _curry1(function (o) { + return s(o, i); + }) + : _isPlaceholder(i) + ? _curry1(function (i) { + return s(o, i); + }) + : s(o, i); + } + }; + } + function _curry3(s) { + return function f3(o, i, u) { + switch (arguments.length) { + case 0: + return f3; + case 1: + return _isPlaceholder(o) + ? f3 + : _curry2(function (i, u) { + return s(o, i, u); + }); + case 2: + return _isPlaceholder(o) && _isPlaceholder(i) + ? f3 + : _isPlaceholder(o) + ? _curry2(function (o, u) { + return s(o, i, u); + }) + : _isPlaceholder(i) + ? _curry2(function (i, u) { + return s(o, i, u); + }) + : _curry1(function (u) { + return s(o, i, u); + }); + default: + return _isPlaceholder(o) && _isPlaceholder(i) && _isPlaceholder(u) + ? f3 + : _isPlaceholder(o) && _isPlaceholder(i) + ? _curry2(function (o, i) { + return s(o, i, u); + }) + : _isPlaceholder(o) && _isPlaceholder(u) + ? _curry2(function (o, u) { + return s(o, i, u); + }) + : _isPlaceholder(i) && _isPlaceholder(u) + ? _curry2(function (i, u) { + return s(o, i, u); + }) + : _isPlaceholder(o) + ? _curry1(function (o) { + return s(o, i, u); + }) + : _isPlaceholder(i) + ? _curry1(function (i) { + return s(o, i, u); + }) + : _isPlaceholder(u) + ? _curry1(function (u) { + return s(o, i, u); + }) + : s(o, i, u); + } + }; + } + const Yo = + Number.isInteger || + function _isInteger(s) { + return (s | 0) === s; + }; + function _isString(s) { + return '[object String]' === Object.prototype.toString.call(s); + } + function _nth(s, o) { + var i = s < 0 ? o.length + s : s; + return _isString(o) ? o.charAt(i) : o[i]; + } + function _path(s, o) { + for (var i = o, u = 0; u < s.length; u += 1) { + if (null == i) return; + var _ = s[u]; + i = Yo(_) ? _nth(_, i) : i[_]; + } + return i; + } + const Xo = _curry3(function pathSatisfies(s, o, i) { + return s(_path(o, i)); + }); + function _cloneRegExp(s) { + return new RegExp( + s.source, + s.flags + ? s.flags + : (s.global ? 'g' : '') + + (s.ignoreCase ? 'i' : '') + + (s.multiline ? 'm' : '') + + (s.sticky ? 'y' : '') + + (s.unicode ? 'u' : '') + + (s.dotAll ? 's' : '') + ); + } + function _arrayFromIterator(s) { + for (var o, i = []; !(o = s.next()).done; ) i.push(o.value); + return i; + } + function _includesWith(s, o, i) { + for (var u = 0, _ = i.length; u < _; ) { + if (s(o, i[u])) return !0; + u += 1; + } + return !1; + } + function _has(s, o) { + return Object.prototype.hasOwnProperty.call(o, s); + } + const Zo = + 'function' == typeof Object.is + ? Object.is + : function _objectIs(s, o) { + return s === o ? 0 !== s || 1 / s == 1 / o : s != s && o != o; + }; + var Qo = Object.prototype.toString; + const _i = (function () { + return '[object Arguments]' === Qo.call(arguments) + ? function _isArguments(s) { + return '[object Arguments]' === Qo.call(s); + } + : function _isArguments(s) { + return _has('callee', s); + }; + })(); + var Ei = !{ toString: null }.propertyIsEnumerable('toString'), + Oi = [ + 'constructor', + 'valueOf', + 'isPrototypeOf', + 'toString', + 'propertyIsEnumerable', + 'hasOwnProperty', + 'toLocaleString' + ], + Pi = (function () { + return arguments.propertyIsEnumerable('length'); + })(), + Mi = function contains(s, o) { + for (var i = 0; i < s.length; ) { + if (s[i] === o) return !0; + i += 1; + } + return !1; + }, + Ri = + 'function' != typeof Object.keys || Pi + ? _curry1(function keys(s) { + if (Object(s) !== s) return []; + var o, + i, + u = [], + _ = Pi && _i(s); + for (o in s) !_has(o, s) || (_ && 'length' === o) || (u[u.length] = o); + if (Ei) + for (i = Oi.length - 1; i >= 0; ) + (_has((o = Oi[i]), s) && !Mi(u, o) && (u[u.length] = o), (i -= 1)); + return u; + }) + : _curry1(function keys(s) { + return Object(s) !== s ? [] : Object.keys(s); + }); + const Wi = Ri; + const ea = _curry1(function type(s) { + return null === s + ? 'Null' + : void 0 === s + ? 'Undefined' + : Object.prototype.toString.call(s).slice(8, -1); + }); + function _uniqContentEquals(s, o, i, u) { + var _ = _arrayFromIterator(s); + function eq(s, o) { + return _equals(s, o, i.slice(), u.slice()); + } + return !_includesWith( + function (s, o) { + return !_includesWith(eq, o, s); + }, + _arrayFromIterator(o), + _ + ); + } + function _equals(s, o, i, u) { + if (Zo(s, o)) return !0; + var _ = ea(s); + if (_ !== ea(o)) return !1; + if ( + 'function' == typeof s['fantasy-land/equals'] || + 'function' == typeof o['fantasy-land/equals'] + ) + return ( + 'function' == typeof s['fantasy-land/equals'] && + s['fantasy-land/equals'](o) && + 'function' == typeof o['fantasy-land/equals'] && + o['fantasy-land/equals'](s) + ); + if ('function' == typeof s.equals || 'function' == typeof o.equals) + return ( + 'function' == typeof s.equals && + s.equals(o) && + 'function' == typeof o.equals && + o.equals(s) + ); + switch (_) { + case 'Arguments': + case 'Array': + case 'Object': + if ( + 'function' == typeof s.constructor && + 'Promise' === + (function _functionName(s) { + var o = String(s).match(/^function (\w*)/); + return null == o ? '' : o[1]; + })(s.constructor) + ) + return s === o; + break; + case 'Boolean': + case 'Number': + case 'String': + if (typeof s != typeof o || !Zo(s.valueOf(), o.valueOf())) return !1; + break; + case 'Date': + if (!Zo(s.valueOf(), o.valueOf())) return !1; + break; + case 'Error': + return s.name === o.name && s.message === o.message; + case 'RegExp': + if ( + s.source !== o.source || + s.global !== o.global || + s.ignoreCase !== o.ignoreCase || + s.multiline !== o.multiline || + s.sticky !== o.sticky || + s.unicode !== o.unicode + ) + return !1; + } + for (var w = i.length - 1; w >= 0; ) { + if (i[w] === s) return u[w] === o; + w -= 1; + } + switch (_) { + case 'Map': + return ( + s.size === o.size && + _uniqContentEquals(s.entries(), o.entries(), i.concat([s]), u.concat([o])) + ); + case 'Set': + return ( + s.size === o.size && + _uniqContentEquals(s.values(), o.values(), i.concat([s]), u.concat([o])) + ); + case 'Arguments': + case 'Array': + case 'Object': + case 'Boolean': + case 'Number': + case 'String': + case 'Date': + case 'Error': + case 'RegExp': + case 'Int8Array': + case 'Uint8Array': + case 'Uint8ClampedArray': + case 'Int16Array': + case 'Uint16Array': + case 'Int32Array': + case 'Uint32Array': + case 'Float32Array': + case 'Float64Array': + case 'ArrayBuffer': + break; + default: + return !1; + } + var x = Wi(s); + if (x.length !== Wi(o).length) return !1; + var C = i.concat([s]), + j = u.concat([o]); + for (w = x.length - 1; w >= 0; ) { + var L = x[w]; + if (!_has(L, o) || !_equals(o[L], s[L], C, j)) return !1; + w -= 1; + } + return !0; + } + const ra = _curry2(function equals(s, o) { + return _equals(s, o, [], []); + }); + function _includes(s, o) { + return ( + (function _indexOf(s, o, i) { + var u, _; + if ('function' == typeof s.indexOf) + switch (typeof o) { + case 'number': + if (0 === o) { + for (u = 1 / o; i < s.length; ) { + if (0 === (_ = s[i]) && 1 / _ === u) return i; + i += 1; + } + return -1; + } + if (o != o) { + for (; i < s.length; ) { + if ('number' == typeof (_ = s[i]) && _ != _) return i; + i += 1; + } + return -1; + } + return s.indexOf(o, i); + case 'string': + case 'boolean': + case 'function': + case 'undefined': + return s.indexOf(o, i); + case 'object': + if (null === o) return s.indexOf(o, i); + } + for (; i < s.length; ) { + if (ra(s[i], o)) return i; + i += 1; + } + return -1; + })(o, s, 0) >= 0 + ); + } + function _map(s, o) { + for (var i = 0, u = o.length, _ = Array(u); i < u; ) ((_[i] = s(o[i])), (i += 1)); + return _; + } + function _quote(s) { + return ( + '"' + + s + .replace(/\\/g, '\\\\') + .replace(/[\b]/g, '\\b') + .replace(/\f/g, '\\f') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\t/g, '\\t') + .replace(/\v/g, '\\v') + .replace(/\0/g, '\\0') + .replace(/"/g, '\\"') + + '"' + ); + } + var na = function pad(s) { + return (s < 10 ? '0' : '') + s; + }; + const ia = + 'function' == typeof Date.prototype.toISOString + ? function _toISOString(s) { + return s.toISOString(); + } + : function _toISOString(s) { + return ( + s.getUTCFullYear() + + '-' + + na(s.getUTCMonth() + 1) + + '-' + + na(s.getUTCDate()) + + 'T' + + na(s.getUTCHours()) + + ':' + + na(s.getUTCMinutes()) + + ':' + + na(s.getUTCSeconds()) + + '.' + + (s.getUTCMilliseconds() / 1e3).toFixed(3).slice(2, 5) + + 'Z' + ); + }; + function _complement(s) { + return function () { + return !s.apply(this, arguments); + }; + } + function _arrayReduce(s, o, i) { + for (var u = 0, _ = i.length; u < _; ) ((o = s(o, i[u])), (u += 1)); + return o; + } + const aa = + Array.isArray || + function _isArray(s) { + return ( + null != s && s.length >= 0 && '[object Array]' === Object.prototype.toString.call(s) + ); + }; + function _dispatchable(s, o, i) { + return function () { + if (0 === arguments.length) return i(); + var u = arguments[arguments.length - 1]; + if (!aa(u)) { + for (var _ = 0; _ < s.length; ) { + if ('function' == typeof u[s[_]]) + return u[s[_]].apply(u, Array.prototype.slice.call(arguments, 0, -1)); + _ += 1; + } + if ( + (function _isTransformer(s) { + return null != s && 'function' == typeof s['@@transducer/step']; + })(u) + ) + return o.apply(null, Array.prototype.slice.call(arguments, 0, -1))(u); + } + return i.apply(this, arguments); + }; + } + function _isObject(s) { + return '[object Object]' === Object.prototype.toString.call(s); + } + const _xfBase_init = function () { + return this.xf['@@transducer/init'](); + }, + _xfBase_result = function (s) { + return this.xf['@@transducer/result'](s); + }; + var la = (function () { + function XFilter(s, o) { + ((this.xf = o), (this.f = s)); + } + return ( + (XFilter.prototype['@@transducer/init'] = _xfBase_init), + (XFilter.prototype['@@transducer/result'] = _xfBase_result), + (XFilter.prototype['@@transducer/step'] = function (s, o) { + return this.f(o) ? this.xf['@@transducer/step'](s, o) : s; + }), + XFilter + ); + })(); + function _xfilter(s) { + return function (o) { + return new la(s, o); + }; + } + var ca = _curry2( + _dispatchable(['fantasy-land/filter', 'filter'], _xfilter, function (s, o) { + return _isObject(o) + ? _arrayReduce( + function (i, u) { + return (s(o[u]) && (i[u] = o[u]), i); + }, + {}, + Wi(o) + ) + : (function _filter(s, o) { + for (var i = 0, u = o.length, _ = []; i < u; ) + (s(o[i]) && (_[_.length] = o[i]), (i += 1)); + return _; + })(s, o); + }) + ); + const ua = ca; + const da = _curry2(function reject(s, o) { + return ua(_complement(s), o); + }); + function _toString_toString(s, o) { + var i = function recur(i) { + var u = o.concat([s]); + return _includes(i, u) ? '' : _toString_toString(i, u); + }, + mapPairs = function (s, o) { + return _map(function (o) { + return _quote(o) + ': ' + i(s[o]); + }, o.slice().sort()); + }; + switch (Object.prototype.toString.call(s)) { + case '[object Arguments]': + return '(function() { return arguments; }(' + _map(i, s).join(', ') + '))'; + case '[object Array]': + return ( + '[' + + _map(i, s) + .concat( + mapPairs( + s, + da(function (s) { + return /^\d+$/.test(s); + }, Wi(s)) + ) + ) + .join(', ') + + ']' + ); + case '[object Boolean]': + return 'object' == typeof s ? 'new Boolean(' + i(s.valueOf()) + ')' : s.toString(); + case '[object Date]': + return 'new Date(' + (isNaN(s.valueOf()) ? i(NaN) : _quote(ia(s))) + ')'; + case '[object Map]': + return 'new Map(' + i(Array.from(s)) + ')'; + case '[object Null]': + return 'null'; + case '[object Number]': + return 'object' == typeof s + ? 'new Number(' + i(s.valueOf()) + ')' + : 1 / s == -1 / 0 + ? '-0' + : s.toString(10); + case '[object Set]': + return 'new Set(' + i(Array.from(s).sort()) + ')'; + case '[object String]': + return 'object' == typeof s ? 'new String(' + i(s.valueOf()) + ')' : _quote(s); + case '[object Undefined]': + return 'undefined'; + default: + if ('function' == typeof s.toString) { + var u = s.toString(); + if ('[object Object]' !== u) return u; + } + return '{' + mapPairs(s, Wi(s)).join(', ') + '}'; + } + } + const ma = _curry1(function toString(s) { + return _toString_toString(s, []); + }); + var ga = _curry2(function test(s, o) { + if ( + !(function _isRegExp(s) { + return '[object RegExp]' === Object.prototype.toString.call(s); + })(s) + ) + throw new TypeError( + '‘test’ requires a value of type RegExp as its first argument; received ' + ma(s) + ); + return _cloneRegExp(s).test(o); + }); + const ya = ga; + function _arity(s, o) { + switch (s) { + case 0: + return function () { + return o.apply(this, arguments); + }; + case 1: + return function (s) { + return o.apply(this, arguments); + }; + case 2: + return function (s, i) { + return o.apply(this, arguments); + }; + case 3: + return function (s, i, u) { + return o.apply(this, arguments); + }; + case 4: + return function (s, i, u, _) { + return o.apply(this, arguments); + }; + case 5: + return function (s, i, u, _, w) { + return o.apply(this, arguments); + }; + case 6: + return function (s, i, u, _, w, x) { + return o.apply(this, arguments); + }; + case 7: + return function (s, i, u, _, w, x, C) { + return o.apply(this, arguments); + }; + case 8: + return function (s, i, u, _, w, x, C, j) { + return o.apply(this, arguments); + }; + case 9: + return function (s, i, u, _, w, x, C, j, L) { + return o.apply(this, arguments); + }; + case 10: + return function (s, i, u, _, w, x, C, j, L, B) { + return o.apply(this, arguments); + }; + default: + throw new Error( + 'First argument to _arity must be a non-negative integer no greater than ten' + ); + } + } + function _pipe(s, o) { + return function () { + return o.call(this, s.apply(this, arguments)); + }; + } + const va = _curry1(function isArrayLike(s) { + return ( + !!aa(s) || + (!!s && + 'object' == typeof s && + !_isString(s) && + (0 === s.length || + (s.length > 0 && s.hasOwnProperty(0) && s.hasOwnProperty(s.length - 1)))) + ); + }); + var ba = 'undefined' != typeof Symbol ? Symbol.iterator : '@@iterator'; + function _createReduce(s, o, i) { + return function _reduce(u, _, w) { + if (va(w)) return s(u, _, w); + if (null == w) return _; + if ('function' == typeof w['fantasy-land/reduce']) + return o(u, _, w, 'fantasy-land/reduce'); + if (null != w[ba]) return i(u, _, w[ba]()); + if ('function' == typeof w.next) return i(u, _, w); + if ('function' == typeof w.reduce) return o(u, _, w, 'reduce'); + throw new TypeError('reduce: list must be array or iterable'); + }; + } + function _xArrayReduce(s, o, i) { + for (var u = 0, _ = i.length; u < _; ) { + if ((o = s['@@transducer/step'](o, i[u])) && o['@@transducer/reduced']) { + o = o['@@transducer/value']; + break; + } + u += 1; + } + return s['@@transducer/result'](o); + } + var _a = _curry2(function bind(s, o) { + return _arity(s.length, function () { + return s.apply(o, arguments); + }); + }); + const Ea = _a; + function _xIterableReduce(s, o, i) { + for (var u = i.next(); !u.done; ) { + if ((o = s['@@transducer/step'](o, u.value)) && o['@@transducer/reduced']) { + o = o['@@transducer/value']; + break; + } + u = i.next(); + } + return s['@@transducer/result'](o); + } + function _xMethodReduce(s, o, i, u) { + return s['@@transducer/result'](i[u](Ea(s['@@transducer/step'], s), o)); + } + const wa = _createReduce(_xArrayReduce, _xMethodReduce, _xIterableReduce); + var xa = (function () { + function XWrap(s) { + this.f = s; + } + return ( + (XWrap.prototype['@@transducer/init'] = function () { + throw new Error('init not implemented on XWrap'); + }), + (XWrap.prototype['@@transducer/result'] = function (s) { + return s; + }), + (XWrap.prototype['@@transducer/step'] = function (s, o) { + return this.f(s, o); + }), + XWrap + ); + })(); + function _xwrap(s) { + return new xa(s); + } + var ka = _curry3(function (s, o, i) { + return wa('function' == typeof s ? _xwrap(s) : s, o, i); + }); + const Ca = ka; + function _checkForMethod(s, o) { + return function () { + var i = arguments.length; + if (0 === i) return o(); + var u = arguments[i - 1]; + return aa(u) || 'function' != typeof u[s] + ? o.apply(this, arguments) + : u[s].apply(u, Array.prototype.slice.call(arguments, 0, i - 1)); + }; + } + var Aa = _curry3( + _checkForMethod('slice', function slice(s, o, i) { + return Array.prototype.slice.call(i, s, o); + }) + ); + const ja = Aa; + const Ia = _curry1(_checkForMethod('tail', ja(1, 1 / 0))); + function pipe() { + if (0 === arguments.length) throw new Error('pipe requires at least one argument'); + return _arity(arguments[0].length, Ca(_pipe, arguments[0], Ia(arguments))); + } + const Na = _curry2(function defaultTo(s, o) { + return null == o || o != o ? s : o; + }); + const Da = _curry2(function prop(s, o) { + if (null != o) return Yo(s) ? _nth(s, o) : o[s]; + }); + const La = _curry3(function propOr(s, o, i) { + return Na(s, Da(o, i)); + }); + var Ba = _curry1(function (s) { + return _nth(-1, s); + }); + const Fa = Ba; + function _curryN(s, o, i) { + return function () { + for (var u = [], _ = 0, w = s, x = 0, C = !1; x < o.length || _ < arguments.length; ) { + var j; + (x < o.length && (!_isPlaceholder(o[x]) || _ >= arguments.length) + ? (j = o[x]) + : ((j = arguments[_]), (_ += 1)), + (u[x] = j), + _isPlaceholder(j) ? (C = !0) : (w -= 1), + (x += 1)); + } + return !C && w <= 0 ? i.apply(this, u) : _arity(Math.max(0, w), _curryN(s, u, i)); + }; + } + var $a = _curry2(function curryN(s, o) { + return 1 === s ? _curry1(o) : _arity(s, _curryN(s, [], o)); + }); + const za = $a; + var Ha = _curry1(function curry(s) { + return za(s.length, s); + }); + const Ja = Ha; + function _isFunction(s) { + var o = Object.prototype.toString.call(s); + return ( + '[object Function]' === o || + '[object AsyncFunction]' === o || + '[object GeneratorFunction]' === o || + '[object AsyncGeneratorFunction]' === o + ); + } + const Ga = _curry2(function invoker(s, o) { + return za(s + 1, function () { + var i = arguments[s]; + if (null != i && _isFunction(i[o])) + return i[o].apply(i, Array.prototype.slice.call(arguments, 0, s)); + throw new TypeError(ma(i) + ' does not have a method named "' + o + '"'); + }); + }); + const tl = Ga(1, 'split'); + function dropLastWhile(s, o) { + for (var i = o.length - 1; i >= 0 && s(o[i]); ) i -= 1; + return ja(0, i + 1, o); + } + var sl = (function () { + function XDropLastWhile(s, o) { + ((this.f = s), (this.retained = []), (this.xf = o)); + } + return ( + (XDropLastWhile.prototype['@@transducer/init'] = _xfBase_init), + (XDropLastWhile.prototype['@@transducer/result'] = function (s) { + return ((this.retained = null), this.xf['@@transducer/result'](s)); + }), + (XDropLastWhile.prototype['@@transducer/step'] = function (s, o) { + return this.f(o) ? this.retain(s, o) : this.flush(s, o); + }), + (XDropLastWhile.prototype.flush = function (s, o) { + return ( + (s = wa(this.xf, s, this.retained)), + (this.retained = []), + this.xf['@@transducer/step'](s, o) + ); + }), + (XDropLastWhile.prototype.retain = function (s, o) { + return (this.retained.push(o), s); + }), + XDropLastWhile + ); + })(); + function _xdropLastWhile(s) { + return function (o) { + return new sl(s, o); + }; + } + const ul = _curry2(_dispatchable([], _xdropLastWhile, dropLastWhile)); + const yl = Ga(1, 'join'); + var vl = _curry1(function flip(s) { + return za(s.length, function (o, i) { + var u = Array.prototype.slice.call(arguments, 0); + return ((u[0] = i), (u[1] = o), s.apply(this, u)); + }); + }); + const _l = vl(_curry2(_includes)); + const El = Ja(function (s, o) { + return pipe(tl(''), ul(_l(s)), yl(''))(o); + }); + function _iterableReduce(s, o, i) { + for (var u = i.next(); !u.done; ) ((o = s(o, u.value)), (u = i.next())); + return o; + } + function _methodReduce(s, o, i, u) { + return i[u](s, o); + } + const wl = _createReduce(_arrayReduce, _methodReduce, _iterableReduce); + var Sl = (function () { + function XMap(s, o) { + ((this.xf = o), (this.f = s)); + } + return ( + (XMap.prototype['@@transducer/init'] = _xfBase_init), + (XMap.prototype['@@transducer/result'] = _xfBase_result), + (XMap.prototype['@@transducer/step'] = function (s, o) { + return this.xf['@@transducer/step'](s, this.f(o)); + }), + XMap + ); + })(); + var xl = _curry2( + _dispatchable( + ['fantasy-land/map', 'map'], + function _xmap(s) { + return function (o) { + return new Sl(s, o); + }; + }, + function map(s, o) { + switch (Object.prototype.toString.call(o)) { + case '[object Function]': + return za(o.length, function () { + return s.call(this, o.apply(this, arguments)); + }); + case '[object Object]': + return _arrayReduce( + function (i, u) { + return ((i[u] = s(o[u])), i); + }, + {}, + Wi(o) + ); + default: + return _map(s, o); + } + } + ) + ); + const kl = xl; + const Cl = _curry2(function ap(s, o) { + return 'function' == typeof o['fantasy-land/ap'] + ? o['fantasy-land/ap'](s) + : 'function' == typeof s.ap + ? s.ap(o) + : 'function' == typeof s + ? function (i) { + return s(i)(o(i)); + } + : wl( + function (s, i) { + return (function _concat(s, o) { + var i; + o = o || []; + var u = (s = s || []).length, + _ = o.length, + w = []; + for (i = 0; i < u; ) ((w[w.length] = s[i]), (i += 1)); + for (i = 0; i < _; ) ((w[w.length] = o[i]), (i += 1)); + return w; + })(s, kl(i, o)); + }, + [], + s + ); + }); + var Ol = _curry2(function liftN(s, o) { + var i = za(s, o); + return za(s, function () { + return _arrayReduce(Cl, kl(i, arguments[0]), Array.prototype.slice.call(arguments, 1)); + }); + }); + const Al = Ol; + var Il = _curry1(function lift(s) { + return Al(s.length, s); + }); + const Pl = Il; + const Ml = Pl( + _curry1(function not(s) { + return !s; + }) + ); + const Tl = _curry1(function always(s) { + return function () { + return s; + }; + }); + const Nl = Tl(void 0); + const Rl = ra(Nl()); + const Dl = Ml(Rl); + const Ll = _curry2(function max(s, o) { + if (s === o) return o; + function safeMax(s, o) { + if (s > o != o > s) return o > s ? o : s; + } + var i = safeMax(s, o); + if (void 0 !== i) return i; + var u = safeMax(typeof s, typeof o); + if (void 0 !== u) return u === typeof s ? s : o; + var _ = ma(s), + w = safeMax(_, ma(o)); + return void 0 !== w && w === _ ? s : o; + }); + var Bl = _curry2(function pluck(s, o) { + return kl(Da(s), o); + }); + const Fl = Bl; + const $l = _curry1(function anyPass(s) { + return za(Ca(Ll, 0, Fl('length', s)), function () { + for (var o = 0, i = s.length; o < i; ) { + if (s[o].apply(this, arguments)) return !0; + o += 1; + } + return !1; + }); + }); + var identical = function (s, o) { + switch (arguments.length) { + case 0: + return identical; + case 1: + return function unaryIdentical(o) { + return 0 === arguments.length ? unaryIdentical : Zo(s, o); + }; + default: + return Zo(s, o); + } + }; + const Vl = identical; + const Ul = za(1, pipe(ea, Vl('GeneratorFunction'))); + const zl = za(1, pipe(ea, Vl('AsyncFunction'))); + const Wl = $l([pipe(ea, Vl('Function')), Ul, zl]); + var Kl = _curry3(function replace(s, o, i) { + return i.replace(s, o); + }); + const Hl = Kl; + const Jl = za(1, pipe(ea, Vl('RegExp'))); + const Gl = _curry3(function when(s, o, i) { + return s(i) ? o(i) : i; + }); + const Yl = za(1, pipe(ea, Vl('String'))); + const Xl = Gl(Yl, Hl(/[.*+?^${}()|[\]\\-]/g, '\\$&')); + var Zl = function checkValue(s, o) { + if ('string' != typeof s && !(s instanceof String)) + throw TypeError('`'.concat(o, '` must be a string')); + }; + const Ql = function replaceAll(s, o, i) { + (!(function checkArguments(s, o, i) { + if (null == i || null == s || null == o) + throw TypeError('Input values must not be `null` or `undefined`'); + })(s, o, i), + Zl(i, 'str'), + Zl(o, 'replaceValue'), + (function checkSearchValue(s) { + if (!('string' == typeof s || s instanceof String || s instanceof RegExp)) + throw TypeError('`searchValue` must be a string or an regexp'); + })(s)); + var u = new RegExp(Jl(s) ? s : Xl(s), 'g'); + return Hl(u, o, i); + }; + var ec = za(3, Ql), + rc = Ga(2, 'replaceAll'); + const sc = Wl(String.prototype.replaceAll) ? rc : ec, + isWindows = () => Xo(ya(/^win/), ['platform'], Go), + getProtocol = (s) => { + try { + const o = new URL(s); + return El(':', o.protocol); + } catch { + return; + } + }, + oc = + (pipe(getProtocol, Dl), + (s) => { + if (Go.browser) return !1; + const o = getProtocol(s); + return Rl(o) || 'file' === o || /^[a-zA-Z]$/.test(o); + }), + isHttpUrl = (s) => { + const o = getProtocol(s); + return 'http' === o || 'https' === o; + }, + toFileSystemPath = (s, o) => { + const i = [/%23/g, '#', /%24/g, '$', /%26/g, '&', /%2C/g, ',', /%40/g, '@'], + u = La(!1, 'keepFileProtocol', o), + _ = La(isWindows, 'isWindows', o); + let w = decodeURI(s); + for (let s = 0; s < i.length; s += 2) w = w.replace(i[s], i[s + 1]); + let x = 'file://' === w.substring(0, 7).toLowerCase(); + return ( + x && + ((w = '/' === w[7] ? w.substring(8) : w.substring(7)), + _() && '/' === w[1] && (w = `${w[0]}:${w.substring(1)}`), + u ? (w = `file:///${w}`) : ((x = !1), (w = _() ? w : `/${w}`))), + _() && + !x && + ((w = sc('/', '\\', w)), + ':\\' === w.substring(1, 3) && (w = w[0].toUpperCase() + w.substring(1))), + w + ); + }, + getHash = (s) => { + const o = s.indexOf('#'); + return -1 !== o ? s.substring(o) : '#'; + }, + stripHash = (s) => { + const o = s.indexOf('#'); + let i = s; + return (o >= 0 && (i = s.substring(0, o)), i); + }, + url_cwd = () => { + if (Go.browser) return stripHash(globalThis.location.href); + const s = Go.cwd(), + o = Fa(s); + return ['/', '\\'].includes(o) ? s : s + (isWindows() ? '\\' : '/'); + }, + resolve = (s, o) => { + const i = new URL(o, new URL(s, 'resolve://')); + if ('resolve:' === i.protocol) { + const { pathname: s, search: o, hash: u } = i; + return s + o + u; + } + return i.toString(); + }, + sanitize = (s) => { + if (oc(s)) + return ((s) => { + const o = [/\?/g, '%3F', /#/g, '%23']; + let i = s; + (isWindows() && (i = i.replace(/\\/g, '/')), (i = encodeURI(i))); + for (let s = 0; s < o.length; s += 2) i = i.replace(o[s], o[s + 1]); + return i; + })(toFileSystemPath(s)); + try { + return new URL(s).toString(); + } catch { + return encodeURI(decodeURI(s)).replace(/%5B/g, '[').replace(/%5D/g, ']'); + } + }, + unsanitize = (s) => (oc(s) ? toFileSystemPath(s) : decodeURI(s)), + { + fetch: ic, + Response: ac, + Headers: lc, + Request: cc, + FormData: pc, + File: hc, + Blob: dc + } = globalThis; + function _array_like_to_array(s, o) { + (null == o || o > s.length) && (o = s.length); + for (var i = 0, u = new Array(o); i < o; i++) u[i] = s[i]; + return u; + } + function legacy_defineProperties(s, o) { + for (var i = 0; i < o.length; i++) { + var u = o[i]; + ((u.enumerable = u.enumerable || !1), + (u.configurable = !0), + 'value' in u && (u.writable = !0), + Object.defineProperty(s, u.key, u)); + } + } + function _instanceof(s, o) { + return null != o && 'undefined' != typeof Symbol && o[Symbol.hasInstance] + ? !!o[Symbol.hasInstance](s) + : s instanceof o; + } + function _sliced_to_array(s, o) { + return ( + (function _array_with_holes(s) { + if (Array.isArray(s)) return s; + })(s) || + (function _iterable_to_array_limit(s, o) { + var i = + null == s + ? null + : ('undefined' != typeof Symbol && s[Symbol.iterator]) || s['@@iterator']; + if (null != i) { + var u, + _, + w = [], + x = !0, + C = !1; + try { + for ( + i = i.call(s); + !(x = (u = i.next()).done) && (w.push(u.value), !o || w.length !== o); + x = !0 + ); + } catch (s) { + ((C = !0), (_ = s)); + } finally { + try { + x || null == i.return || i.return(); + } finally { + if (C) throw _; + } + } + return w; + } + })(s, o) || + (function _unsupported_iterable_to_array(s, o) { + if (!s) return; + if ('string' == typeof s) return _array_like_to_array(s, o); + var i = Object.prototype.toString.call(s).slice(8, -1); + 'Object' === i && s.constructor && (i = s.constructor.name); + if ('Map' === i || 'Set' === i) return Array.from(i); + if ('Arguments' === i || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i)) + return _array_like_to_array(s, o); + })(s, o) || + (function _non_iterable_rest() { + throw new TypeError( + 'Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' + ); + })() + ); + } + function _type_of(s) { + return s && 'undefined' != typeof Symbol && s.constructor === Symbol + ? 'symbol' + : typeof s; + } + (void 0 === globalThis.fetch && (globalThis.fetch = ic), + void 0 === globalThis.Headers && (globalThis.Headers = lc), + void 0 === globalThis.Request && (globalThis.Request = cc), + void 0 === globalThis.Response && (globalThis.Response = ac), + void 0 === globalThis.FormData && (globalThis.FormData = pc), + void 0 === globalThis.File && (globalThis.File = hc), + void 0 === globalThis.Blob && (globalThis.Blob = dc)); + var __typeError = function (s) { + throw TypeError(s); + }, + __accessCheck = function (s, o, i) { + return o.has(s) || __typeError('Cannot ' + i); + }, + __privateGet = function (s, o, i) { + return (__accessCheck(s, o, 'read from private field'), i ? i.call(s) : o.get(s)); + }, + __privateAdd = function (s, o, i) { + return o.has(s) + ? __typeError('Cannot add the same private member more than once') + : _instanceof(o, WeakSet) + ? o.add(s) + : o.set(s, i); + }, + __privateSet = function (s, o, i, u) { + return ( + __accessCheck(s, o, 'write to private field'), + u ? u.call(s, i) : o.set(s, i), + i + ); + }, + to_string = function (s) { + return Object.prototype.toString.call(s); + }, + is_typed_array = function (s) { + return ArrayBuffer.isView(s) && !_instanceof(s, DataView); + }, + fc = Array.isArray, + gc = Object.getOwnPropertyDescriptor, + bc = Object.prototype.propertyIsEnumerable, + _c = Object.getOwnPropertySymbols, + Ec = Object.prototype.hasOwnProperty; + function own_enumerable_keys(s) { + for (var o = Object.keys(s), i = _c(s), u = 0; u < i.length; u++) + bc.call(s, i[u]) && o.push(i[u]); + return o; + } + function is_writable(s, o) { + var i; + return !(null === (i = gc(s, o)) || void 0 === i ? void 0 : i.writable); + } + function legacy_copy(s, o) { + if ('object' === (void 0 === s ? 'undefined' : _type_of(s)) && null !== s) { + var i; + if (fc(s)) i = []; + else if ('[object Date]' === to_string(s)) i = new Date(s.getTime ? s.getTime() : s); + else if ( + (function (s) { + return '[object RegExp]' === to_string(s); + })(s) + ) + i = new RegExp(s); + else if ( + (function (s) { + return '[object Error]' === to_string(s); + })(s) + ) + i = { message: s.message }; + else if ( + (function (s) { + return '[object Boolean]' === to_string(s); + })(s) || + (function (s) { + return '[object Number]' === to_string(s); + })(s) || + (function (s) { + return '[object String]' === to_string(s); + })(s) + ) + i = Object(s); + else { + if (is_typed_array(s)) return s.slice(); + i = Object.create(Object.getPrototypeOf(s)); + } + var u = o.includeSymbols ? own_enumerable_keys : Object.keys, + _ = !0, + w = !1, + x = void 0; + try { + for (var C, j = u(s)[Symbol.iterator](); !(_ = (C = j.next()).done); _ = !0) { + var L = C.value; + i[L] = s[L]; + } + } catch (s) { + ((w = !0), (x = s)); + } finally { + try { + _ || null == j.return || j.return(); + } finally { + if (w) throw x; + } + } + return i; + } + return s; + } + var kc, + Oc, + jc = { includeSymbols: !1, immutable: !1 }; + function walk(s, o) { + var i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : jc, + u = [], + _ = [], + w = !0, + x = i.includeSymbols ? own_enumerable_keys : Object.keys, + C = !!i.immutable; + return (function walker(s) { + var j = C ? legacy_copy(s, i) : s, + L = {}, + B = !0, + $ = { + node: j, + node_: s, + path: [].concat(u), + parent: _[_.length - 1], + parents: _, + key: u[u.length - 1], + isRoot: 0 === u.length, + level: u.length, + circular: void 0, + isLeaf: !1, + notLeaf: !0, + notRoot: !0, + isFirst: !1, + isLast: !1, + update: function update(s) { + var o = arguments.length > 1 && void 0 !== arguments[1] && arguments[1]; + ($.isRoot || ($.parent.node[$.key] = s), ($.node = s), o && (B = !1)); + }, + delete: function _delete(s) { + (delete $.parent.node[$.key], s && (B = !1)); + }, + remove: function remove(s) { + (fc($.parent.node) ? $.parent.node.splice($.key, 1) : delete $.parent.node[$.key], + s && (B = !1)); + }, + keys: null, + before: function before(s) { + L.before = s; + }, + after: function after(s) { + L.after = s; + }, + pre: function pre(s) { + L.pre = s; + }, + post: function post(s) { + L.post = s; + }, + stop: function stop() { + w = !1; + }, + block: function block() { + B = !1; + } + }; + if (!w) return $; + function update_state() { + if ('object' === _type_of($.node) && null !== $.node) { + (($.keys && $.node_ === $.node) || ($.keys = x($.node)), + ($.isLeaf = 0 === $.keys.length)); + for (var o = 0; o < _.length; o++) + if (_[o].node_ === s) { + $.circular = _[o]; + break; + } + } else (($.isLeaf = !0), ($.keys = null)); + (($.notLeaf = !$.isLeaf), ($.notRoot = !$.isRoot)); + } + update_state(); + var V = o.call($, $.node); + if ((void 0 !== V && $.update && $.update(V), L.before && L.before.call($, $.node), !B)) + return $; + if ('object' === _type_of($.node) && null !== $.node && !$.circular) { + var U; + (_.push($), update_state()); + var z = !0, + Y = !1, + Z = void 0; + try { + for ( + var ee, + ie = Object.entries(null !== (U = $.keys) && void 0 !== U ? U : [])[ + Symbol.iterator + ](); + !(z = (ee = ie.next()).done); + z = !0 + ) { + var ae, + le = _sliced_to_array(ee.value, 2), + ce = le[0], + pe = le[1]; + (u.push(pe), L.pre && L.pre.call($, $.node[pe], pe)); + var de = walker($.node[pe]); + (C && Ec.call($.node, pe) && !is_writable($.node, pe) && ($.node[pe] = de.node), + (de.isLast = + !!(null === (ae = $.keys) || void 0 === ae ? void 0 : ae.length) && + +ce == $.keys.length - 1), + (de.isFirst = 0 == +ce), + L.post && L.post.call($, de), + u.pop()); + } + } catch (s) { + ((Y = !0), (Z = s)); + } finally { + try { + z || null == ie.return || ie.return(); + } finally { + if (Y) throw Z; + } + } + _.pop(); + } + return (L.after && L.after.call($, $.node), $); + })(s).node; + } + var Ic = (function () { + function Traverse(s) { + var o = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : jc; + (!(function _class_call_check(s, o) { + if (!(s instanceof o)) throw new TypeError('Cannot call a class as a function'); + })(this, Traverse), + __privateAdd(this, kc), + __privateAdd(this, Oc), + __privateSet(this, kc, s), + __privateSet(this, Oc, o)); + } + return ( + (function _create_class(s, o, i) { + return ( + o && legacy_defineProperties(s.prototype, o), + i && legacy_defineProperties(s, i), + s + ); + })(Traverse, [ + { + key: 'get', + value: function get(s) { + for (var o = __privateGet(this, kc), i = 0; o && i < s.length; i++) { + var u = s[i]; + if ( + !Ec.call(o, u) || + (!__privateGet(this, Oc).includeSymbols && + 'symbol' === (void 0 === u ? 'undefined' : _type_of(u))) + ) + return; + o = o[u]; + } + return o; + } + }, + { + key: 'has', + value: function has(s) { + for (var o = __privateGet(this, kc), i = 0; o && i < s.length; i++) { + var u = s[i]; + if ( + !Ec.call(o, u) || + (!__privateGet(this, Oc).includeSymbols && + 'symbol' === (void 0 === u ? 'undefined' : _type_of(u))) + ) + return !1; + o = o[u]; + } + return !0; + } + }, + { + key: 'set', + value: function set(s, o) { + var i = __privateGet(this, kc), + u = 0; + for (u = 0; u < s.length - 1; u++) { + var _ = s[u]; + (Ec.call(i, _) || (i[_] = {}), (i = i[_])); + } + return ((i[s[u]] = o), o); + } + }, + { + key: 'map', + value: function map(s) { + return walk(__privateGet(this, kc), s, { + immutable: !0, + includeSymbols: !!__privateGet(this, Oc).includeSymbols + }); + } + }, + { + key: 'forEach', + value: function forEach(s) { + return ( + __privateSet(this, kc, walk(__privateGet(this, kc), s, __privateGet(this, Oc))), + __privateGet(this, kc) + ); + } + }, + { + key: 'reduce', + value: function reduce(s, o) { + var i = 1 === arguments.length, + u = i ? __privateGet(this, kc) : o; + return ( + this.forEach(function (o) { + (this.isRoot && i) || (u = s.call(this, u, o)); + }), + u + ); + } + }, + { + key: 'paths', + value: function paths() { + var s = []; + return ( + this.forEach(function () { + s.push(this.path); + }), + s + ); + } + }, + { + key: 'nodes', + value: function nodes() { + var s = []; + return ( + this.forEach(function () { + s.push(this.node); + }), + s + ); + } + }, + { + key: 'clone', + value: function clone() { + var s = [], + o = [], + i = __privateGet(this, Oc); + return is_typed_array(__privateGet(this, kc)) + ? __privateGet(this, kc).slice() + : (function clone(u) { + for (var _ = 0; _ < s.length; _++) if (s[_] === u) return o[_]; + if ('object' === (void 0 === u ? 'undefined' : _type_of(u)) && null !== u) { + var w = legacy_copy(u, i); + (s.push(u), o.push(w)); + var x = i.includeSymbols ? own_enumerable_keys : Object.keys, + C = !0, + j = !1, + L = void 0; + try { + for ( + var B, $ = x(u)[Symbol.iterator](); + !(C = (B = $.next()).done); + C = !0 + ) { + var V = B.value; + w[V] = clone(u[V]); + } + } catch (s) { + ((j = !0), (L = s)); + } finally { + try { + C || null == $.return || $.return(); + } finally { + if (j) throw L; + } + } + return (s.pop(), o.pop(), w); + } + return u; + })(__privateGet(this, kc)); + } + } + ]), + Traverse + ); + })(); + ((kc = new WeakMap()), (Oc = new WeakMap())); + var traverse = function (s, o) { + return new Ic(s, o); + }; + ((traverse.get = function (s, o, i) { + return new Ic(s, i).get(o); + }), + (traverse.set = function (s, o, i, u) { + return new Ic(s, u).set(o, i); + }), + (traverse.has = function (s, o, i) { + return new Ic(s, i).has(o); + }), + (traverse.map = function (s, o, i) { + return new Ic(s, i).map(o); + }), + (traverse.forEach = function (s, o, i) { + return new Ic(s, i).forEach(o); + }), + (traverse.reduce = function (s, o, i, u) { + return new Ic(s, u).reduce(o, i); + }), + (traverse.paths = function (s, o) { + return new Ic(s, o).paths(); + }), + (traverse.nodes = function (s, o) { + return new Ic(s, o).nodes(); + }), + (traverse.clone = function (s, o) { + return new Ic(s, o).clone(); + })); + var Pc = traverse; + const Mc = 'application/json, application/yaml', + Nc = 'https://swagger.io', + Rc = Object.freeze({ url: '/' }), + Lc = ['properties'], + Fc = ['properties'], + qc = [ + 'definitions', + 'parameters', + 'responses', + 'securityDefinitions', + 'components/schemas', + 'components/responses', + 'components/parameters', + 'components/securitySchemes' + ], + Kc = ['schema/example', 'items/example']; + function isFreelyNamed(s) { + const o = s[s.length - 1], + i = s[s.length - 2], + u = s.join('/'); + return ( + (Lc.indexOf(o) > -1 && -1 === Fc.indexOf(i)) || + qc.indexOf(u) > -1 || + Kc.some((s) => u.indexOf(s) > -1) + ); + } + function absolutifyPointer(s, o) { + const [i, u] = s.split('#'), + _ = null != o ? o : '', + w = null != i ? i : ''; + let x; + if (isHttpUrl(_)) x = resolve(_, w); + else { + const s = resolve(Nc, _), + o = resolve(s, w).replace(Nc, ''); + x = w.startsWith('/') ? o : o.substring(1); + } + return u ? `${x}#${u}` : x; + } + const Hc = /^([a-z]+:\/\/|\/\/)/i; + class JSONRefError extends Jo {} + const Jc = {}, + Gc = new WeakMap(), + Qc = [ + (s) => 'paths' === s[0] && 'responses' === s[3] && 'examples' === s[5], + (s) => + 'paths' === s[0] && 'responses' === s[3] && 'content' === s[5] && 'example' === s[7], + (s) => + 'paths' === s[0] && + 'responses' === s[3] && + 'content' === s[5] && + 'examples' === s[7] && + 'value' === s[9], + (s) => + 'paths' === s[0] && + 'requestBody' === s[3] && + 'content' === s[4] && + 'example' === s[6], + (s) => + 'paths' === s[0] && + 'requestBody' === s[3] && + 'content' === s[4] && + 'examples' === s[6] && + 'value' === s[8], + (s) => 'paths' === s[0] && 'parameters' === s[2] && 'example' === s[4], + (s) => 'paths' === s[0] && 'parameters' === s[3] && 'example' === s[5], + (s) => + 'paths' === s[0] && 'parameters' === s[2] && 'examples' === s[4] && 'value' === s[6], + (s) => + 'paths' === s[0] && 'parameters' === s[3] && 'examples' === s[5] && 'value' === s[7], + (s) => + 'paths' === s[0] && 'parameters' === s[2] && 'content' === s[4] && 'example' === s[6], + (s) => + 'paths' === s[0] && + 'parameters' === s[2] && + 'content' === s[4] && + 'examples' === s[6] && + 'value' === s[8], + (s) => + 'paths' === s[0] && 'parameters' === s[3] && 'content' === s[4] && 'example' === s[7], + (s) => + 'paths' === s[0] && + 'parameters' === s[3] && + 'content' === s[5] && + 'examples' === s[7] && + 'value' === s[9] + ], + eu = { + key: '$ref', + plugin: (s, o, i, u) => { + const _ = u.getInstance(), + w = i.slice(0, -1); + if (isFreelyNamed(w) || ((s) => Qc.some((o) => o(s)))(w)) return; + const { baseDoc: x } = u.getContext(i); + if ('string' != typeof s) + return new JSONRefError('$ref: must be a string (JSON-Ref)', { + $ref: s, + baseDoc: x, + fullPath: i + }); + const C = refs_split(s), + j = C[0], + L = C[1] || ''; + let B, $, V; + try { + B = x || j ? absoluteify(j, x) : null; + } catch (o) { + return wrapError(o, { pointer: L, $ref: s, basePath: B, fullPath: i }); + } + if ( + (function pointerAlreadyInPath(s, o, i, u) { + let _ = Gc.get(u); + _ || ((_ = {}), Gc.set(u, _)); + const w = (function arrayToJsonPointer(s) { + if (0 === s.length) return ''; + return `/${s.map(escapeJsonPointerToken).join('/')}`; + })(i), + x = `${o || ''}#${s}`, + C = w.replace(/allOf\/\d+\/?/g, ''), + j = u.contextTree.get([]).baseDoc; + if (o === j && pointerIsAParent(C, s)) return !0; + let L = ''; + const B = i.some( + (s) => ( + (L = `${L}/${escapeJsonPointerToken(s)}`), + _[L] && _[L].some((s) => pointerIsAParent(s, x) || pointerIsAParent(x, s)) + ) + ); + if (B) return !0; + return void (_[C] = (_[C] || []).concat(x)); + })(L, B, w, u) && + !_.useCircularStructures + ) { + const o = absolutifyPointer(s, B); + return s === o ? null : zo.replace(i, o); + } + if ( + (null == B + ? ((V = jsonPointerToArray(L)), + ($ = u.get(V)), + void 0 === $ && + ($ = new JSONRefError(`Could not resolve reference: ${s}`, { + pointer: L, + $ref: s, + baseDoc: x, + fullPath: i + }))) + : (($ = extractFromDoc(B, L)), + ($ = + null != $.__value + ? $.__value + : $.catch((o) => { + throw wrapError(o, { pointer: L, $ref: s, baseDoc: x, fullPath: i }); + }))), + $ instanceof Error) + ) + return [zo.remove(i), $]; + const U = absolutifyPointer(s, B), + z = zo.replace(w, $, { $$ref: U }); + if (B && B !== x) return [z, zo.context(w, { baseDoc: B })]; + try { + if ( + !(function patchValueAlreadyInPath(s, o) { + const i = [s]; + return ( + o.path.reduce((s, o) => (i.push(s[o]), s[o]), s), + pointToAncestor(o.value) + ); + function pointToAncestor(s) { + return ( + zo.isObject(s) && + (i.indexOf(s) >= 0 || Object.keys(s).some((o) => pointToAncestor(s[o]))) + ); + } + })(u.state, z) || + _.useCircularStructures + ) + return z; + } catch (s) { + return null; + } + } + }, + tu = Object.assign(eu, { + docCache: Jc, + absoluteify, + clearCache: function clearCache(s) { + void 0 !== s + ? delete Jc[s] + : Object.keys(Jc).forEach((s) => { + delete Jc[s]; + }); + }, + JSONRefError, + wrapError, + getDoc, + split: refs_split, + extractFromDoc, + fetchJSON: function fetchJSON(s) { + return fetch(s, { headers: { Accept: Mc }, loadSpec: !0 }) + .then((s) => s.text()) + .then((s) => mn.load(s)); + }, + extract, + jsonPointerToArray, + unescapeJsonPointerToken + }), + ru = tu; + function absoluteify(s, o) { + if (!Hc.test(s)) { + if (!o) + throw new JSONRefError( + `Tried to resolve a relative URL, without having a basePath. path: '${s}' basePath: '${o}'` + ); + return resolve(o, s); + } + return s; + } + function wrapError(s, o) { + let i; + return ( + (i = + s && s.response && s.response.body + ? `${s.response.body.code} ${s.response.body.message}` + : s.message), + new JSONRefError(`Could not resolve reference: ${i}`, { ...o, cause: s }) + ); + } + function refs_split(s) { + return (s + '').split('#'); + } + function extractFromDoc(s, o) { + const i = Jc[s]; + if (i && !zo.isPromise(i)) + try { + const s = extract(o, i); + return Object.assign(Promise.resolve(s), { __value: s }); + } catch (s) { + return Promise.reject(s); + } + return getDoc(s).then((s) => extract(o, s)); + } + function getDoc(s) { + const o = Jc[s]; + return o + ? zo.isPromise(o) + ? o + : Promise.resolve(o) + : ((Jc[s] = tu.fetchJSON(s).then((o) => ((Jc[s] = o), o))), Jc[s]); + } + function extract(s, o) { + const i = jsonPointerToArray(s); + if (i.length < 1) return o; + const u = zo.getIn(o, i); + if (void 0 === u) + throw new JSONRefError(`Could not resolve pointer: ${s} does not exist in document`, { + pointer: s + }); + return u; + } + function jsonPointerToArray(s) { + if ('string' != typeof s) throw new TypeError('Expected a string, got a ' + typeof s); + return ( + '/' === s[0] && (s = s.substr(1)), + '' === s ? [] : s.split('/').map(unescapeJsonPointerToken) + ); + } + function unescapeJsonPointerToken(s) { + if ('string' != typeof s) return s; + return new URLSearchParams(`=${s.replace(/~1/g, '/').replace(/~0/g, '~')}`).get(''); + } + function escapeJsonPointerToken(s) { + return new URLSearchParams([['', s.replace(/~/g, '~0').replace(/\//g, '~1')]]) + .toString() + .slice(1); + } + const pointerBoundaryChar = (s) => !s || '/' === s || '#' === s; + function pointerIsAParent(s, o) { + if (pointerBoundaryChar(o)) return !0; + const i = s.charAt(o.length), + u = o.slice(-1); + return 0 === s.indexOf(o) && (!i || '/' === i || '#' === i) && '#' !== u; + } + const nu = { + key: 'allOf', + plugin: (s, o, i, u, _) => { + if (_.meta && _.meta.$$ref) return; + const w = i.slice(0, -1); + if (isFreelyNamed(w)) return; + if (!Array.isArray(s)) { + const s = new TypeError('allOf must be an array'); + return ((s.fullPath = i), s); + } + let x = !1, + C = _.value; + if ( + (w.forEach((s) => { + C && (C = C[s]); + }), + (C = { ...C }), + 0 === Object.keys(C).length) + ) + return; + delete C.allOf; + const j = []; + return ( + j.push(u.replace(w, {})), + s.forEach((s, o) => { + if (!u.isObject(s)) { + if (x) return null; + x = !0; + const s = new TypeError('Elements in allOf must be objects'); + return ((s.fullPath = i), j.push(s)); + } + j.push(u.mergeDeep(w, s)); + const _ = (function generateAbsoluteRefPatches( + s, + o, + { + specmap: i, + getBaseUrlForNodePath: u = (s) => i.getContext([...o, ...s]).baseDoc, + targetKeys: _ = ['$ref', '$$ref'] + } = {} + ) { + const w = []; + return ( + Pc(s).forEach(function callback() { + if (_.includes(this.key) && 'string' == typeof this.node) { + const s = this.path, + _ = o.concat(this.path), + x = absolutifyPointer(this.node, u(s)); + w.push(i.replace(_, x)); + } + }), + w + ); + })(s, i.slice(0, -1), { + getBaseUrlForNodePath: (s) => u.getContext([...i, o, ...s]).baseDoc, + specmap: u + }); + j.push(..._); + }), + C.example && j.push(u.remove([].concat(w, 'example'))), + j.push(u.mergeDeep(w, C)), + C.$$ref || j.push(u.remove([].concat(w, '$$ref'))), + j + ); + } + }, + su = { + key: 'parameters', + plugin: (s, o, i, u) => { + if (Array.isArray(s) && s.length) { + const o = Object.assign([], s), + _ = i.slice(0, -1), + w = { ...zo.getIn(u.spec, _) }; + for (let _ = 0; _ < s.length; _ += 1) { + const x = s[_]; + try { + o[_].default = u.parameterMacro(w, x); + } catch (s) { + const o = new Error(s); + return ((o.fullPath = i), o); + } + } + return zo.replace(i, o); + } + return zo.replace(i, s); + } + }, + ou = { + key: 'properties', + plugin: (s, o, i, u) => { + const _ = { ...s }; + for (const o in s) + try { + _[o].default = u.modelPropertyMacro(_[o]); + } catch (s) { + const o = new Error(s); + return ((o.fullPath = i), o); + } + return zo.replace(i, _); + } + }; + class ContextTree { + constructor(s) { + this.root = context_tree_createNode(s || {}); + } + set(s, o) { + const i = this.getParent(s, !0); + if (!i) return void context_tree_updateNode(this.root, o, null); + const u = s[s.length - 1], + { children: _ } = i; + _[u] ? context_tree_updateNode(_[u], o, i) : (_[u] = context_tree_createNode(o, i)); + } + get(s) { + if ((s = s || []).length < 1) return this.root.value; + let o, + i, + u = this.root; + for (let _ = 0; _ < s.length && ((i = s[_]), (o = u.children), o[i]); _ += 1) u = o[i]; + return u && u.protoValue; + } + getParent(s, o) { + return !s || s.length < 1 + ? null + : s.length < 2 + ? this.root + : s.slice(0, -1).reduce((s, i) => { + if (!s) return s; + const { children: u } = s; + return (!u[i] && o && (u[i] = context_tree_createNode(null, s)), u[i]); + }, this.root); + } + } + function context_tree_createNode(s, o) { + return context_tree_updateNode({ children: {} }, s, o); + } + function context_tree_updateNode(s, o, i) { + return ( + (s.value = o || {}), + (s.protoValue = i ? { ...i.protoValue, ...s.value } : s.value), + Object.keys(s.children).forEach((o) => { + const i = s.children[o]; + s.children[o] = context_tree_updateNode(i, i.value, s); + }), + s + ); + } + const specmap_noop = () => {}; + class SpecMap { + static getPluginName(s) { + return s.pluginName; + } + static getPatchesOfType(s, o) { + return s.filter(o); + } + constructor(s) { + (Object.assign( + this, + { + spec: '', + debugLevel: 'info', + plugins: [], + pluginHistory: {}, + errors: [], + mutations: [], + promisedPatches: [], + state: {}, + patches: [], + context: {}, + contextTree: new ContextTree(), + showDebug: !1, + allPatches: [], + pluginProp: 'specMap', + libMethods: Object.assign(Object.create(this), zo, { getInstance: () => this }), + allowMetaPatches: !1 + }, + s + ), + (this.get = this._get.bind(this)), + (this.getContext = this._getContext.bind(this)), + (this.hasRun = this._hasRun.bind(this)), + (this.wrappedPlugins = this.plugins + .map(this.wrapPlugin.bind(this)) + .filter(zo.isFunction)), + this.patches.push(zo.add([], this.spec)), + this.patches.push(zo.context([], this.context)), + this.updatePatches(this.patches)); + } + debug(s, ...o) { + this.debugLevel === s && console.log(...o); + } + verbose(s, ...o) { + 'verbose' === this.debugLevel && console.log(`[${s}] `, ...o); + } + wrapPlugin(s, o) { + const { pathDiscriminator: i } = this; + let u, + _ = null; + return ( + s[this.pluginProp] + ? ((_ = s), (u = s[this.pluginProp])) + : zo.isFunction(s) + ? (u = s) + : zo.isObject(s) && + (u = (function createKeyBasedPlugin(s) { + const isSubPath = (s, o) => + !Array.isArray(s) || s.every((s, i) => s === o[i]); + return function* generator(o, u) { + const _ = {}; + for (const [s, i] of o.filter(zo.isAdditiveMutation).entries()) { + if (!(s < 3e3)) return; + yield* traverse(i.value, i.path, i); + } + function* traverse(o, w, x) { + if (zo.isObject(o)) { + const C = w.length - 1, + j = w[C], + L = w.indexOf('properties'), + B = 'properties' === j && C === L, + $ = u.allowMetaPatches && _[o.$$ref]; + for (const C of Object.keys(o)) { + const j = o[C], + L = w.concat(C), + V = zo.isObject(j), + U = o.$$ref; + if ( + ($ || + (V && + (u.allowMetaPatches && U && (_[U] = !0), + yield* traverse(j, L, x))), + !B && C === s.key) + ) { + const o = isSubPath(i, w); + (i && !o) || (yield s.plugin(j, C, L, u, x)); + } + } + } else s.key === w[w.length - 1] && (yield s.plugin(o, s.key, w, u)); + } + }; + })(s)), + Object.assign(u.bind(_), { pluginName: s.name || o, isGenerator: zo.isGenerator(u) }) + ); + } + nextPlugin() { + return this.wrappedPlugins.find((s) => this.getMutationsForPlugin(s).length > 0); + } + nextPromisedPatch() { + if (this.promisedPatches.length > 0) + return Promise.race(this.promisedPatches.map((s) => s.value)); + } + getPluginHistory(s) { + const o = this.constructor.getPluginName(s); + return this.pluginHistory[o] || []; + } + getPluginRunCount(s) { + return this.getPluginHistory(s).length; + } + getPluginHistoryTip(s) { + const o = this.getPluginHistory(s); + return (o && o[o.length - 1]) || {}; + } + getPluginMutationIndex(s) { + const o = this.getPluginHistoryTip(s).mutationIndex; + return 'number' != typeof o ? -1 : o; + } + updatePluginHistory(s, o) { + const i = this.constructor.getPluginName(s); + ((this.pluginHistory[i] = this.pluginHistory[i] || []), this.pluginHistory[i].push(o)); + } + updatePatches(s) { + zo.normalizeArray(s).forEach((s) => { + if (s instanceof Error) this.errors.push(s); + else + try { + if (!zo.isObject(s)) + return void this.debug('updatePatches', 'Got a non-object patch', s); + if ((this.showDebug && this.allPatches.push(s), zo.isPromise(s.value))) + return (this.promisedPatches.push(s), void this.promisedPatchThen(s)); + if (zo.isContextPatch(s)) return void this.setContext(s.path, s.value); + zo.isMutation(s) && this.updateMutations(s); + } catch (s) { + (console.error(s), this.errors.push(s)); + } + }); + } + updateMutations(s) { + 'object' == typeof s.value && + !Array.isArray(s.value) && + this.allowMetaPatches && + (s.value = { ...s.value }); + const o = zo.applyPatch(this.state, s, { allowMetaPatches: this.allowMetaPatches }); + o && (this.mutations.push(s), (this.state = o)); + } + removePromisedPatch(s) { + const o = this.promisedPatches.indexOf(s); + o < 0 + ? this.debug("Tried to remove a promisedPatch that isn't there!") + : this.promisedPatches.splice(o, 1); + } + promisedPatchThen(s) { + return ( + (s.value = s.value + .then((o) => { + const i = { ...s, value: o }; + (this.removePromisedPatch(s), this.updatePatches(i)); + }) + .catch((o) => { + (this.removePromisedPatch(s), this.updatePatches(o)); + })), + s.value + ); + } + getMutations(s, o) { + return ( + (s = s || 0), + 'number' != typeof o && (o = this.mutations.length), + this.mutations.slice(s, o) + ); + } + getCurrentMutations() { + return this.getMutationsForPlugin(this.getCurrentPlugin()); + } + getMutationsForPlugin(s) { + const o = this.getPluginMutationIndex(s); + return this.getMutations(o + 1); + } + getCurrentPlugin() { + return this.currentPlugin; + } + getLib() { + return this.libMethods; + } + _get(s) { + return zo.getIn(this.state, s); + } + _getContext(s) { + return this.contextTree.get(s); + } + setContext(s, o) { + return this.contextTree.set(s, o); + } + _hasRun(s) { + return this.getPluginRunCount(this.getCurrentPlugin()) > (s || 0); + } + dispatch() { + const s = this, + o = this.nextPlugin(); + if (!o) { + const s = this.nextPromisedPatch(); + if (s) return s.then(() => this.dispatch()).catch(() => this.dispatch()); + const o = { spec: this.state, errors: this.errors }; + return (this.showDebug && (o.patches = this.allPatches), Promise.resolve(o)); + } + if ( + ((s.pluginCount = s.pluginCount || new WeakMap()), + s.pluginCount.set(o, (s.pluginCount.get(o) || 0) + 1), + s.pluginCount[o] > 100) + ) + return Promise.resolve({ + spec: s.state, + errors: s.errors.concat(new Error("We've reached a hard limit of 100 plugin runs")) + }); + if (o !== this.currentPlugin && this.promisedPatches.length) { + const s = this.promisedPatches.map((s) => s.value); + return Promise.all(s.map((s) => s.then(specmap_noop, specmap_noop))).then(() => + this.dispatch() + ); + } + return (function executePlugin() { + s.currentPlugin = o; + const i = s.getCurrentMutations(), + u = s.mutations.length - 1; + try { + if (o.isGenerator) for (const u of o(i, s.getLib())) updatePatches(u); + else { + updatePatches(o(i, s.getLib())); + } + } catch (s) { + (console.error(s), updatePatches([Object.assign(Object.create(s), { plugin: o })])); + } finally { + s.updatePluginHistory(o, { mutationIndex: u }); + } + return s.dispatch(); + })(); + function updatePatches(i) { + i && ((i = zo.fullyNormalizeArray(i)), s.updatePatches(i, o)); + } + } + } + const iu = { refs: ru, allOf: nu, parameters: su, properties: ou }; + function makeFetchJSON(s, o = {}) { + const { requestInterceptor: i, responseInterceptor: u } = o, + _ = s.withCredentials ? 'include' : 'same-origin'; + return (o) => + s({ + url: o, + loadSpec: !0, + requestInterceptor: i, + responseInterceptor: u, + headers: { Accept: Mc }, + credentials: _ + }).then((s) => s.body); + } + function isFile(s, o) { + return ( + o || 'undefined' == typeof navigator || (o = navigator), + o && 'ReactNative' === o.product + ? !(!s || 'object' != typeof s || 'string' != typeof s.uri) + : ('undefined' != typeof File && s instanceof File) || + ('undefined' != typeof Blob && s instanceof Blob) || + !!ArrayBuffer.isView(s) || + (null !== s && 'object' == typeof s && 'function' == typeof s.pipe) + ); + } + function isArrayOfFile(s, o) { + return Array.isArray(s) && s.some((s) => isFile(s, o)); + } + class FileWithData extends File { + constructor(s, o = '', i = {}) { + (super([s], o, i), (this.data = s)); + } + valueOf() { + return this.data; + } + toString() { + return this.valueOf(); + } + } + const isRfc3986Reserved = (s) => ":/?#[]@!$&'()*+,;=".indexOf(s) > -1, + isRfc3986Unreserved = (s) => /^[a-z0-9\-._~]+$/i.test(s); + function encodeCharacters(s, o = 'reserved') { + return [...s] + .map((s) => { + if (isRfc3986Unreserved(s)) return s; + if (isRfc3986Reserved(s) && 'unsafe' === o) return s; + const i = new TextEncoder(); + return Array.from(i.encode(s)) + .map((s) => `0${s.toString(16).toUpperCase()}`.slice(-2)) + .map((s) => `%${s}`) + .join(''); + }) + .join(''); + } + function stylize(s) { + const { value: o } = s; + return Array.isArray(o) + ? (function encodeArray({ key: s, value: o, style: i, explode: u, escape: _ }) { + if ('simple' === i) return o.map((s) => valueEncoder(s, _)).join(','); + if ('label' === i) return `.${o.map((s) => valueEncoder(s, _)).join('.')}`; + if ('matrix' === i) + return o + .map((s) => valueEncoder(s, _)) + .reduce((o, i) => (!o || u ? `${o || ''};${s}=${i}` : `${o},${i}`), ''); + if ('form' === i) { + const i = u ? `&${s}=` : ','; + return o.map((s) => valueEncoder(s, _)).join(i); + } + if ('spaceDelimited' === i) { + const i = u ? `${s}=` : ''; + return o.map((s) => valueEncoder(s, _)).join(` ${i}`); + } + if ('pipeDelimited' === i) { + const i = u ? `${s}=` : ''; + return o.map((s) => valueEncoder(s, _)).join(`|${i}`); + } + return; + })(s) + : 'object' == typeof o + ? (function encodeObject({ key: s, value: o, style: i, explode: u, escape: _ }) { + const w = Object.keys(o); + if ('simple' === i) + return w.reduce((s, i) => { + const w = valueEncoder(o[i], _); + return `${s ? `${s},` : ''}${i}${u ? '=' : ','}${w}`; + }, ''); + if ('label' === i) + return w.reduce((s, i) => { + const w = valueEncoder(o[i], _); + return `${s ? `${s}.` : '.'}${i}${u ? '=' : '.'}${w}`; + }, ''); + if ('matrix' === i && u) + return w.reduce( + (s, i) => `${s ? `${s};` : ';'}${i}=${valueEncoder(o[i], _)}`, + '' + ); + if ('matrix' === i) + return w.reduce((i, u) => { + const w = valueEncoder(o[u], _); + return `${i ? `${i},` : `;${s}=`}${u},${w}`; + }, ''); + if ('form' === i) + return w.reduce((s, i) => { + const w = valueEncoder(o[i], _); + return `${s ? `${s}${u ? '&' : ','}` : ''}${i}${u ? '=' : ','}${w}`; + }, ''); + return; + })(s) + : (function encodePrimitive({ key: s, value: o, style: i, escape: u }) { + if ('simple' === i) return valueEncoder(o, u); + if ('label' === i) return `.${valueEncoder(o, u)}`; + if ('matrix' === i) return `;${s}=${valueEncoder(o, u)}`; + if ('form' === i) return valueEncoder(o, u); + if ('deepObject' === i) return valueEncoder(o, u); + return; + })(s); + } + function valueEncoder(s, o = !1) { + return ( + Array.isArray(s) || (null !== s && 'object' == typeof s) + ? (s = JSON.stringify(s)) + : ('number' != typeof s && 'boolean' != typeof s) || (s = String(s)), + o && s.length > 0 ? encodeCharacters(s, o) : s + ); + } + const au = { form: ',', spaceDelimited: '%20', pipeDelimited: '|' }, + lu = { csv: ',', ssv: '%20', tsv: '%09', pipes: '|' }; + function formatKeyValue(s, o, i = !1) { + const { + collectionFormat: u, + allowEmptyValue: _, + serializationOption: w, + encoding: x + } = o, + C = 'object' != typeof o || Array.isArray(o) ? o : o.value, + j = i ? (s) => s.toString() : (s) => encodeURIComponent(s), + L = j(s); + if (void 0 === C && _) return [[L, '']]; + if (isFile(C) || isArrayOfFile(C)) return [[L, C]]; + if (w) return formatKeyValueBySerializationOption(s, C, i, w); + if (x) { + if ( + [typeof x.style, typeof x.explode, typeof x.allowReserved].some( + (s) => 'undefined' !== s + ) + ) { + const { style: o, explode: u, allowReserved: _ } = x; + return formatKeyValueBySerializationOption(s, C, i, { + style: o, + explode: u, + allowReserved: _ + }); + } + if ('string' == typeof x.contentType) { + if (x.contentType.startsWith('application/json')) { + const s = j('string' == typeof C ? C : JSON.stringify(C)); + return [[L, new FileWithData(s, 'blob', { type: x.contentType })]]; + } + const s = j(String(C)); + return [[L, new FileWithData(s, 'blob', { type: x.contentType })]]; + } + return 'object' != typeof C + ? [[L, j(C)]] + : Array.isArray(C) && C.every((s) => 'object' != typeof s) + ? [[L, C.map(j).join(',')]] + : [[L, j(JSON.stringify(C))]]; + } + return 'object' != typeof C + ? [[L, j(C)]] + : Array.isArray(C) + ? 'multi' === u + ? [[L, C.map(j)]] + : [[L, C.map(j).join(lu[u || 'csv'])]] + : [[L, '']]; + } + function formatKeyValueBySerializationOption(s, o, i, u) { + const _ = u.style || 'form', + w = void 0 === u.explode ? 'form' === _ : u.explode, + x = !i && (u && u.allowReserved ? 'unsafe' : 'reserved'), + encodeFn = (s) => valueEncoder(s, x), + C = i ? (s) => s : (s) => encodeFn(s); + return 'object' != typeof o + ? [[C(s), encodeFn(o)]] + : Array.isArray(o) + ? w + ? [[C(s), o.map(encodeFn)]] + : [[C(s), o.map(encodeFn).join(au[_])]] + : 'deepObject' === _ + ? Object.keys(o).map((i) => [C(`${s}[${i}]`), encodeFn(o[i])]) + : w + ? Object.keys(o).map((s) => [C(s), encodeFn(o[s])]) + : [ + [ + C(s), + Object.keys(o) + .map((s) => [`${C(s)},${encodeFn(o[s])}`]) + .join(',') + ] + ]; + } + function encodeFormOrQuery(s) { + return ((s, { encode: o = !0 } = {}) => { + const buildNestedParams = (s, o, i) => ( + null == i + ? s.append(o, '') + : Array.isArray(i) + ? i.reduce((i, u) => buildNestedParams(s, o, u), s) + : i instanceof Date + ? s.append(o, i.toISOString()) + : 'object' == typeof i + ? Object.entries(i).reduce( + (i, [u, _]) => buildNestedParams(s, `${o}[${u}]`, _), + s + ) + : s.append(o, i), + s + ), + i = Object.entries(s).reduce( + (s, [o, i]) => buildNestedParams(s, o, i), + new URLSearchParams() + ), + u = String(i); + return o ? u : decodeURIComponent(u); + })( + Object.keys(s).reduce((o, i) => { + for (const [u, _] of formatKeyValue(i, s[i])) + o[u] = _ instanceof FileWithData ? _.valueOf() : _; + return o; + }, {}), + { encode: !1 } + ); + } + function serializeRequest(s = {}) { + const { url: o = '', query: i, form: u } = s; + if (u) { + const o = Object.keys(u).some((s) => { + const { value: o } = u[s]; + return isFile(o) || isArrayOfFile(o); + }), + i = s.headers['content-type'] || s.headers['Content-Type']; + if (o || /multipart\/form-data/i.test(i)) { + const o = (function request_buildFormData(s) { + return Object.entries(s).reduce((s, [o, i]) => { + for (const [u, _] of formatKeyValue(o, i, !0)) + if (Array.isArray(_)) + for (const o of _) + if (ArrayBuffer.isView(o)) { + const i = new Blob([o]); + s.append(u, i); + } else s.append(u, o); + else if (ArrayBuffer.isView(_)) { + const o = new Blob([_]); + s.append(u, o); + } else s.append(u, _); + return s; + }, new FormData()); + })(s.form); + ((s.formdata = o), (s.body = o)); + } else s.body = encodeFormOrQuery(u); + delete s.form; + } + if (i) { + const [u, _] = o.split('?'); + let w = ''; + if (_) { + const s = new URLSearchParams(_); + (Object.keys(i).forEach((o) => s.delete(o)), (w = String(s))); + } + const x = ((...s) => { + const o = s.filter((s) => s).join('&'); + return o ? `?${o}` : ''; + })(w, encodeFormOrQuery(i)); + ((s.url = u + x), delete s.query); + } + return s; + } + function serializeHeaders(s = {}) { + return 'function' != typeof s.entries + ? {} + : Array.from(s.entries()).reduce( + (s, [o, i]) => ( + (s[o] = (function serializeHeaderValue(s) { + return s.includes(', ') ? s.split(', ') : s; + })(i)), + s + ), + {} + ); + } + function serializeResponse(s, o, { loadSpec: i = !1 } = {}) { + const u = { + ok: s.ok, + url: s.url || o, + status: s.status, + statusText: s.statusText, + headers: serializeHeaders(s.headers) + }, + _ = u.headers['content-type'], + w = i || ((s = '') => /(json|xml|yaml|text)\b/.test(s))(_); + return (w ? s.text : s.blob || s.buffer).call(s).then((s) => { + if (((u.text = s), (u.data = s), w)) + try { + const o = (function parseBody(s, o) { + return o && (0 === o.indexOf('application/json') || o.indexOf('+json') > 0) + ? JSON.parse(s) + : mn.load(s); + })(s, _); + ((u.body = o), (u.obj = o)); + } catch (s) { + u.parseError = s; + } + return u; + }); + } + async function http_http(s, o = {}) { + ('object' == typeof s && (s = (o = s).url), + (o.headers = o.headers || {}), + (o = serializeRequest(o)).headers && + Object.keys(o.headers).forEach((s) => { + const i = o.headers[s]; + 'string' == typeof i && (o.headers[s] = i.replace(/\n+/g, ' ')); + }), + o.requestInterceptor && (o = (await o.requestInterceptor(o)) || o)); + const i = o.headers['content-type'] || o.headers['Content-Type']; + let u; + /multipart\/form-data/i.test(i) && + (delete o.headers['content-type'], delete o.headers['Content-Type']); + try { + ((u = await (o.userFetch || fetch)(o.url, o)), + (u = await serializeResponse(u, s, o)), + o.responseInterceptor && (u = (await o.responseInterceptor(u)) || u)); + } catch (s) { + if (!u) throw s; + const o = new Error(u.statusText || `response status is ${u.status}`); + throw ((o.status = u.status), (o.statusCode = u.status), (o.responseError = s), o); + } + if (!u.ok) { + const s = new Error(u.statusText || `response status is ${u.status}`); + throw ((s.status = u.status), (s.statusCode = u.status), (s.response = u), s); + } + return u; + } + const options_retrievalURI = (s) => { + var o, i; + const { baseDoc: u, url: _ } = s, + w = null !== (o = null != u ? u : _) && void 0 !== o ? o : ''; + return 'string' == + typeof (null === (i = globalThis.document) || void 0 === i ? void 0 : i.baseURI) + ? String(new URL(w, globalThis.document.baseURI)) + : w; + }, + options_httpClient = (s) => { + const { fetch: o, http: i } = s; + return o || i || http_http; + }; + async function resolveGenericStrategy(s) { + const { + spec: o, + mode: i, + allowMetaPatches: u = !0, + pathDiscriminator: _, + modelPropertyMacro: w, + parameterMacro: x, + requestInterceptor: C, + responseInterceptor: j, + skipNormalization: L = !1, + useCircularStructures: B, + strategies: $ + } = s, + V = options_retrievalURI(s), + U = options_httpClient(s), + z = $.find((s) => s.match(o)); + return (async function doResolve(s) { + V && (iu.refs.docCache[V] = s); + iu.refs.fetchJSON = makeFetchJSON(U, { requestInterceptor: C, responseInterceptor: j }); + const o = [iu.refs]; + 'function' == typeof x && o.push(iu.parameters); + 'function' == typeof w && o.push(iu.properties); + 'strict' !== i && o.push(iu.allOf); + const $ = await (function mapSpec(s) { + return new SpecMap(s).dispatch(); + })({ + spec: s, + context: { baseDoc: V }, + plugins: o, + allowMetaPatches: u, + pathDiscriminator: _, + parameterMacro: x, + modelPropertyMacro: w, + useCircularStructures: B + }); + L || ($.spec = z.normalize($.spec)); + return $; + })(o); + } + const replace_special_chars_with_underscore = (s) => s.replace(/\W/gi, '_'); + function opId(s, o, i = '', { v2OperationIdCompatibilityMode: u } = {}) { + if (!s || 'object' != typeof s) return null; + return (s.operationId || '').replace(/\s/g, '').length + ? replace_special_chars_with_underscore(s.operationId) + : (function idFromPathMethod(s, o, { v2OperationIdCompatibilityMode: i } = {}) { + if (i) { + let i = `${o.toLowerCase()}_${s}`.replace( + /[\s!@#$%^&*()_+=[{\]};:<>|./?,\\'""-]/g, + '_' + ); + return ( + (i = i || `${s.substring(1)}_${o}`), + i + .replace(/((_){2,})/g, '_') + .replace(/^(_)*/g, '') + .replace(/([_])*$/g, '') + ); + } + return `${o.toLowerCase()}${replace_special_chars_with_underscore(s)}`; + })(o, i, { v2OperationIdCompatibilityMode: u }); + } + function normalize(s) { + const { spec: o } = s, + { paths: i } = o, + u = {}; + if (!i || o.$$normalized) return s; + for (const s in i) { + const _ = i[s]; + if (null == _ || !['object', 'function'].includes(typeof _)) continue; + const w = _.parameters; + for (const i in _) { + const x = _[i]; + if (null == x || !['object', 'function'].includes(typeof x)) continue; + const C = opId(x, s, i); + if (C) { + u[C] ? u[C].push(x) : (u[C] = [x]); + const s = u[C]; + if (s.length > 1) + s.forEach((s, o) => { + ((s.__originalOperationId = s.__originalOperationId || s.operationId), + (s.operationId = `${C}${o + 1}`)); + }); + else if (void 0 !== x.operationId) { + const o = s[0]; + ((o.__originalOperationId = o.__originalOperationId || x.operationId), + (o.operationId = C)); + } + } + if ('parameters' !== i) { + const s = [], + i = {}; + for (const u in o) + ('produces' !== u && 'consumes' !== u && 'security' !== u) || + ((i[u] = o[u]), s.push(i)); + if ((w && ((i.parameters = w), s.push(i)), s.length)) + for (const o of s) + for (const s in o) + if (x[s]) { + if ('parameters' === s) + for (const i of o[s]) { + x[s].some( + (s) => + (s.name && s.name === i.name) || + (s.$ref && s.$ref === i.$ref) || + (s.$$ref && s.$$ref === i.$$ref) || + s === i + ) || x[s].push(i); + } + } else x[s] = o[s]; + } + } + } + return ((o.$$normalized = !0), s); + } + const cu = { + name: 'generic', + match: () => !0, + normalize(s) { + const { spec: o } = normalize({ spec: s }); + return o; + }, + resolve: async (s) => resolveGenericStrategy(s) + }, + uu = cu; + const isOpenAPI30 = (s) => { + try { + const { openapi: o } = s; + return 'string' == typeof o && /^3\.0\.([0123])(?:-rc[012])?$/.test(o); + } catch { + return !1; + } + }, + isOpenAPI31 = (s) => { + try { + const { openapi: o } = s; + return 'string' == typeof o && /^3\.1\.(?:[1-9]\d*|0)$/.test(o); + } catch { + return !1; + } + }, + isOpenAPI3 = (s) => isOpenAPI30(s) || isOpenAPI31(s), + pu = { + name: 'openapi-2', + match: (s) => + ((s) => { + try { + const { swagger: o } = s; + return '2.0' === o; + } catch { + return !1; + } + })(s), + normalize(s) { + const { spec: o } = normalize({ spec: s }); + return o; + }, + resolve: async (s) => + (async function resolveOpenAPI2Strategy(s) { + return resolveGenericStrategy(s); + })(s) + }, + hu = pu; + const du = { + name: 'openapi-3-0', + match: (s) => isOpenAPI30(s), + normalize(s) { + const { spec: o } = normalize({ spec: s }); + return o; + }, + resolve: async (s) => + (async function resolveOpenAPI30Strategy(s) { + return resolveGenericStrategy(s); + })(s) + }, + fu = du; + const mu = _curry2(function and(s, o) { + return s && o; + }); + const gu = _curry2(function both(s, o) { + return _isFunction(s) + ? function _both() { + return s.apply(this, arguments) && o.apply(this, arguments); + } + : Pl(mu)(s, o); + }); + const yu = ra(null); + const vu = Ml(yu); + function isOfTypeObject_typeof(s) { + return ( + (isOfTypeObject_typeof = + 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator + ? function (s) { + return typeof s; + } + : function (s) { + return s && + 'function' == typeof Symbol && + s.constructor === Symbol && + s !== Symbol.prototype + ? 'symbol' + : typeof s; + }), + isOfTypeObject_typeof(s) + ); + } + const bu = function isOfTypeObject(s) { + return 'object' === isOfTypeObject_typeof(s); + }; + const _u = za(1, gu(vu, bu)); + var Eu = pipe(ea, Vl('Object')), + wu = pipe(ma, ra(ma(Object))), + Su = Xo(gu(Wl, wu), ['constructor']), + xu = za(1, function (s) { + if (!_u(s) || !Eu(s)) return !1; + var o = Object.getPrototypeOf(s); + return !!yu(o) || Su(o); + }); + const ku = xu; + var Cu = __webpack_require__(34035); + function _reduced(s) { + return s && s['@@transducer/reduced'] + ? s + : { '@@transducer/value': s, '@@transducer/reduced': !0 }; + } + var Ou = (function () { + function XAll(s, o) { + ((this.xf = o), (this.f = s), (this.all = !0)); + } + return ( + (XAll.prototype['@@transducer/init'] = _xfBase_init), + (XAll.prototype['@@transducer/result'] = function (s) { + return ( + this.all && (s = this.xf['@@transducer/step'](s, !0)), + this.xf['@@transducer/result'](s) + ); + }), + (XAll.prototype['@@transducer/step'] = function (s, o) { + return ( + this.f(o) || ((this.all = !1), (s = _reduced(this.xf['@@transducer/step'](s, !1)))), + s + ); + }), + XAll + ); + })(); + function _xall(s) { + return function (o) { + return new Ou(s, o); + }; + } + var Au = _curry2( + _dispatchable(['all'], _xall, function all(s, o) { + for (var i = 0; i < o.length; ) { + if (!s(o[i])) return !1; + i += 1; + } + return !0; + }) + ); + const ju = Au; + class Annotation extends Cu.Om { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'annotation')); + } + get code() { + return this.attributes.get('code'); + } + set code(s) { + this.attributes.set('code', s); + } + } + const Iu = Annotation; + class Comment extends Cu.Om { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'comment')); + } + } + const Pu = Comment; + class ParseResult extends Cu.wE { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'parseResult')); + } + get api() { + return this.children.filter((s) => s.classes.contains('api')).first; + } + get results() { + return this.children.filter((s) => s.classes.contains('result')); + } + get result() { + return this.results.first; + } + get annotations() { + return this.children.filter((s) => 'annotation' === s.element); + } + get warnings() { + return this.children.filter( + (s) => 'annotation' === s.element && s.classes.contains('warning') + ); + } + get errors() { + return this.children.filter( + (s) => 'annotation' === s.element && s.classes.contains('error') + ); + } + get isEmpty() { + return this.children.reject((s) => 'annotation' === s.element).isEmpty; + } + replaceResult(s) { + const { result: o } = this; + if (Rl(o)) return !1; + const i = this.content.findIndex((s) => s === o); + return -1 !== i && ((this.content[i] = s), !0); + } + } + const Mu = ParseResult; + class SourceMap extends Cu.wE { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'sourceMap')); + } + get positionStart() { + return this.children.filter((s) => s.classes.contains('position')).get(0); + } + get positionEnd() { + return this.children.filter((s) => s.classes.contains('position')).get(1); + } + set position(s) { + if (void 0 === s) return; + const o = new Cu.wE([s.start.row, s.start.column, s.start.char]), + i = new Cu.wE([s.end.row, s.end.column, s.end.char]); + (o.classes.push('position'), i.classes.push('position'), this.push(o).push(i)); + } + } + const Tu = SourceMap, + hasMethod = (s, o) => + 'object' == typeof o && null !== o && s in o && 'function' == typeof o[s], + hasBasicElementProps = (s) => + 'object' == typeof s && + null != s && + '_storedElement' in s && + 'string' == typeof s._storedElement && + '_content' in s, + primitiveEq = (s, o) => + 'object' == typeof o && + null !== o && + 'primitive' in o && + 'function' == typeof o.primitive && + o.primitive() === s, + hasClass = (s, o) => + 'object' == typeof o && + null !== o && + 'classes' in o && + (Array.isArray(o.classes) || o.classes instanceof Cu.wE) && + o.classes.includes(s), + isElementType = (s, o) => + 'object' == typeof o && null !== o && 'element' in o && o.element === s, + helpers = (s) => + s({ hasMethod, hasBasicElementProps, primitiveEq, isElementType, hasClass }), + Nu = helpers( + ({ hasBasicElementProps: s, primitiveEq: o }) => + (i) => + i instanceof Cu.Hg || (s(i) && o(void 0, i)) + ), + Ru = helpers( + ({ hasBasicElementProps: s, primitiveEq: o }) => + (i) => + i instanceof Cu.Om || (s(i) && o('string', i)) + ), + Du = helpers( + ({ hasBasicElementProps: s, primitiveEq: o }) => + (i) => + i instanceof Cu.kT || (s(i) && o('number', i)) + ), + Lu = helpers( + ({ hasBasicElementProps: s, primitiveEq: o }) => + (i) => + i instanceof Cu.Os || (s(i) && o('null', i)) + ), + Bu = helpers( + ({ hasBasicElementProps: s, primitiveEq: o }) => + (i) => + i instanceof Cu.bd || (s(i) && o('boolean', i)) + ), + Fu = helpers( + ({ hasBasicElementProps: s, primitiveEq: o, hasMethod: i }) => + (u) => + u instanceof Cu.Sh || + (s(u) && o('object', u) && i('keys', u) && i('values', u) && i('items', u)) + ), + qu = helpers( + ({ hasBasicElementProps: s, primitiveEq: o, hasMethod: i }) => + (u) => + (u instanceof Cu.wE && !(u instanceof Cu.Sh)) || + (s(u) && + o('array', u) && + i('push', u) && + i('unshift', u) && + i('map', u) && + i('reduce', u)) + ), + $u = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Cu.Pr || (s(u) && o('member', u) && i(void 0, u)) + ), + Vu = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Cu.Ft || (s(u) && o('link', u) && i(void 0, u)) + ), + Uu = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Cu.sI || (s(u) && o('ref', u) && i(void 0, u)) + ), + zu = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Iu || (s(u) && o('annotation', u) && i('array', u)) + ), + Wu = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Pu || (s(u) && o('comment', u) && i('string', u)) + ), + Ku = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Mu || (s(u) && o('parseResult', u) && i('array', u)) + ), + Hu = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Tu || (s(u) && o('sourceMap', u) && i('array', u)) + ), + isPrimitiveElement = (s) => + isElementType('object', s) || + isElementType('array', s) || + isElementType('boolean', s) || + isElementType('number', s) || + isElementType('string', s) || + isElementType('null', s) || + isElementType('member', s), + hasElementSourceMap = (s) => Hu(s.meta.get('sourceMap')), + includesSymbols = (s, o) => { + if (0 === s.length) return !0; + const i = o.attributes.get('symbols'); + return !!qu(i) && ju(_l(i.toValue()), s); + }, + includesClasses = (s, o) => 0 === s.length || ju(_l(o.classes.toValue()), s); + const es_T = function () { + return !0; + }; + const es_F = function () { + return !1; + }, + getVisitFn = (s, o, i) => { + const u = s[o]; + if (null != u) { + if (!i && 'function' == typeof u) return u; + const s = i ? u.leave : u.enter; + if ('function' == typeof s) return s; + } else { + const u = i ? s.leave : s.enter; + if (null != u) { + if ('function' == typeof u) return u; + const s = u[o]; + if ('function' == typeof s) return s; + } + } + return null; + }, + Ju = {}, + getNodeType = (s) => (null == s ? void 0 : s.type), + isNode = (s) => 'string' == typeof getNodeType(s), + cloneNode = (s) => + Object.create(Object.getPrototypeOf(s), Object.getOwnPropertyDescriptors(s)), + mergeAll = ( + s, + { + visitFnGetter: o = getVisitFn, + nodeTypeGetter: i = getNodeType, + breakSymbol: u = Ju, + deleteNodeSymbol: _ = null, + skipVisitingNodeSymbol: w = !1, + exposeEdits: x = !1 + } = {} + ) => { + const C = Symbol('skip'), + j = new Array(s.length).fill(C); + return { + enter(L, B, $, V, U, z) { + let Y = L, + Z = !1; + const ee = { + ...z, + replaceWith(s, o) { + (z.replaceWith(s, o), (Y = s)); + } + }; + for (let L = 0; L < s.length; L += 1) + if (j[L] === C) { + const C = o(s[L], i(Y), !1); + if ('function' == typeof C) { + const o = C.call(s[L], Y, B, $, V, U, ee); + if ('function' == typeof (null == o ? void 0 : o.then)) + throw new Jo('Async visitor not supported in sync mode', { + visitor: s[L], + visitFn: C + }); + if (o === w) j[L] = Y; + else if (o === u) j[L] = u; + else { + if (o === _) return o; + if (void 0 !== o) { + if (!x) return o; + ((Y = o), (Z = !0)); + } + } + } + } + return Z ? Y : void 0; + }, + leave(_, x, L, B, $, V) { + let U = _; + const z = { + ...V, + replaceWith(s, o) { + (V.replaceWith(s, o), (U = s)); + } + }; + for (let _ = 0; _ < s.length; _ += 1) + if (j[_] === C) { + const C = o(s[_], i(U), !0); + if ('function' == typeof C) { + const o = C.call(s[_], U, x, L, B, $, z); + if ('function' == typeof (null == o ? void 0 : o.then)) + throw new Jo('Async visitor not supported in sync mode', { + visitor: s[_], + visitFn: C + }); + if (o === u) j[_] = u; + else if (void 0 !== o && o !== w) return o; + } + } else j[_] === U && (j[_] = C); + } + }; + }; + mergeAll[Symbol.for('nodejs.util.promisify.custom')] = ( + s, + { + visitFnGetter: o = getVisitFn, + nodeTypeGetter: i = getNodeType, + breakSymbol: u = Ju, + deleteNodeSymbol: _ = null, + skipVisitingNodeSymbol: w = !1, + exposeEdits: x = !1 + } = {} + ) => { + const C = Symbol('skip'), + j = new Array(s.length).fill(C); + return { + async enter(L, B, $, V, U, z) { + let Y = L, + Z = !1; + const ee = { + ...z, + replaceWith(s, o) { + (z.replaceWith(s, o), (Y = s)); + } + }; + for (let L = 0; L < s.length; L += 1) + if (j[L] === C) { + const C = o(s[L], i(Y), !1); + if ('function' == typeof C) { + const o = await C.call(s[L], Y, B, $, V, U, ee); + if (o === w) j[L] = Y; + else if (o === u) j[L] = u; + else { + if (o === _) return o; + if (void 0 !== o) { + if (!x) return o; + ((Y = o), (Z = !0)); + } + } + } + } + return Z ? Y : void 0; + }, + async leave(_, x, L, B, $, V) { + let U = _; + const z = { + ...V, + replaceWith(s, o) { + (V.replaceWith(s, o), (U = s)); + } + }; + for (let _ = 0; _ < s.length; _ += 1) + if (j[_] === C) { + const C = o(s[_], i(U), !0); + if ('function' == typeof C) { + const o = await C.call(s[_], U, x, L, B, $, z); + if (o === u) j[_] = u; + else if (void 0 !== o && o !== w) return o; + } + } else j[_] === U && (j[_] = C); + } + }; + }; + const visit = ( + s, + o, + { + keyMap: i = null, + state: u = {}, + breakSymbol: _ = Ju, + deleteNodeSymbol: w = null, + skipVisitingNodeSymbol: x = !1, + visitFnGetter: C = getVisitFn, + nodeTypeGetter: j = getNodeType, + nodePredicate: L = isNode, + nodeCloneFn: B = cloneNode, + detectCycles: $ = !0 + } = {} + ) => { + const V = i || {}; + let U, + z, + Y = Array.isArray(s), + Z = [s], + ee = -1, + ie = [], + ae = s; + const le = [], + ce = []; + do { + ee += 1; + const s = ee === Z.length; + let i; + const fe = s && 0 !== ie.length; + if (s) { + if (((i = 0 === ce.length ? void 0 : le.pop()), (ae = z), (z = ce.pop()), fe)) + if (Y) { + ae = ae.slice(); + let s = 0; + for (const [o, i] of ie) { + const u = o - s; + i === w ? (ae.splice(u, 1), (s += 1)) : (ae[u] = i); + } + } else { + ae = B(ae); + for (const [s, o] of ie) ae[s] = o; + } + ((ee = U.index), (Z = U.keys), (ie = U.edits), (Y = U.inArray), (U = U.prev)); + } else if (z !== w && void 0 !== z) { + if (((i = Y ? ee : Z[ee]), (ae = z[i]), ae === w || void 0 === ae)) continue; + le.push(i); + } + let ye; + if (!Array.isArray(ae)) { + var pe; + if (!L(ae)) throw new Jo(`Invalid AST Node: ${String(ae)}`, { node: ae }); + if ($ && ce.includes(ae)) { + le.pop(); + continue; + } + const w = C(o, j(ae), s); + if (w) { + for (const [s, i] of Object.entries(u)) o[s] = i; + const _ = { + replaceWith(o, u) { + ('function' == typeof u ? u(o, ae, i, z, le, ce) : z && (z[i] = o), + s || (ae = o)); + } + }; + ye = w.call(o, ae, i, z, le, ce, _); + } + if ('function' == typeof (null === (pe = ye) || void 0 === pe ? void 0 : pe.then)) + throw new Jo('Async visitor not supported in sync mode', { + visitor: o, + visitFn: w + }); + if (ye === _) break; + if (ye === x) { + if (!s) { + le.pop(); + continue; + } + } else if (void 0 !== ye && (ie.push([i, ye]), !s)) { + if (!L(ye)) { + le.pop(); + continue; + } + ae = ye; + } + } + var de; + if ((void 0 === ye && fe && ie.push([i, ae]), !s)) + ((U = { inArray: Y, index: ee, keys: Z, edits: ie, prev: U }), + (Y = Array.isArray(ae)), + (Z = Y ? ae : null !== (de = V[j(ae)]) && void 0 !== de ? de : []), + (ee = -1), + (ie = []), + z !== w && void 0 !== z && ce.push(z), + (z = ae)); + } while (void 0 !== U); + return 0 !== ie.length ? ie[ie.length - 1][1] : s; + }; + visit[Symbol.for('nodejs.util.promisify.custom')] = async ( + s, + o, + { + keyMap: i = null, + state: u = {}, + breakSymbol: _ = Ju, + deleteNodeSymbol: w = null, + skipVisitingNodeSymbol: x = !1, + visitFnGetter: C = getVisitFn, + nodeTypeGetter: j = getNodeType, + nodePredicate: L = isNode, + nodeCloneFn: B = cloneNode, + detectCycles: $ = !0 + } = {} + ) => { + const V = i || {}; + let U, + z, + Y = Array.isArray(s), + Z = [s], + ee = -1, + ie = [], + ae = s; + const le = [], + ce = []; + do { + ee += 1; + const s = ee === Z.length; + let i; + const de = s && 0 !== ie.length; + if (s) { + if (((i = 0 === ce.length ? void 0 : le.pop()), (ae = z), (z = ce.pop()), de)) + if (Y) { + ae = ae.slice(); + let s = 0; + for (const [o, i] of ie) { + const u = o - s; + i === w ? (ae.splice(u, 1), (s += 1)) : (ae[u] = i); + } + } else { + ae = B(ae); + for (const [s, o] of ie) ae[s] = o; + } + ((ee = U.index), (Z = U.keys), (ie = U.edits), (Y = U.inArray), (U = U.prev)); + } else if (z !== w && void 0 !== z) { + if (((i = Y ? ee : Z[ee]), (ae = z[i]), ae === w || void 0 === ae)) continue; + le.push(i); + } + let fe; + if (!Array.isArray(ae)) { + if (!L(ae)) throw new Jo(`Invalid AST Node: ${String(ae)}`, { node: ae }); + if ($ && ce.includes(ae)) { + le.pop(); + continue; + } + const w = C(o, j(ae), s); + if (w) { + for (const [s, i] of Object.entries(u)) o[s] = i; + const _ = { + replaceWith(o, u) { + ('function' == typeof u ? u(o, ae, i, z, le, ce) : z && (z[i] = o), + s || (ae = o)); + } + }; + fe = await w.call(o, ae, i, z, le, ce, _); + } + if (fe === _) break; + if (fe === x) { + if (!s) { + le.pop(); + continue; + } + } else if (void 0 !== fe && (ie.push([i, fe]), !s)) { + if (!L(fe)) { + le.pop(); + continue; + } + ae = fe; + } + } + var pe; + if ((void 0 === fe && de && ie.push([i, ae]), !s)) + ((U = { inArray: Y, index: ee, keys: Z, edits: ie, prev: U }), + (Y = Array.isArray(ae)), + (Z = Y ? ae : null !== (pe = V[j(ae)]) && void 0 !== pe ? pe : []), + (ee = -1), + (ie = []), + z !== w && void 0 !== z && ce.push(z), + (z = ae)); + } while (void 0 !== U); + return 0 !== ie.length ? ie[ie.length - 1][1] : s; + }; + const Gu = class CloneError extends Jo { + value; + constructor(s, o) { + (super(s, o), void 0 !== o && (this.value = o.value)); + } + }; + const Yu = class DeepCloneError extends Gu {}; + const Xu = class ShallowCloneError extends Gu {}, + cloneDeep = (s, o = {}) => { + const { visited: i = new WeakMap() } = o, + u = { ...o, visited: i }; + if (i.has(s)) return i.get(s); + if (s instanceof Cu.KeyValuePair) { + const { key: o, value: _ } = s, + w = Nu(o) ? cloneDeep(o, u) : o, + x = Nu(_) ? cloneDeep(_, u) : _, + C = new Cu.KeyValuePair(w, x); + return (i.set(s, C), C); + } + if (s instanceof Cu.ot) { + const mapper = (s) => cloneDeep(s, u), + o = [...s].map(mapper), + _ = new Cu.ot(o); + return (i.set(s, _), _); + } + if (s instanceof Cu.G6) { + const mapper = (s) => cloneDeep(s, u), + o = [...s].map(mapper), + _ = new Cu.G6(o); + return (i.set(s, _), _); + } + if (Nu(s)) { + const o = cloneShallow(s); + if ((i.set(s, o), s.content)) + if (Nu(s.content)) o.content = cloneDeep(s.content, u); + else if (s.content instanceof Cu.KeyValuePair) o.content = cloneDeep(s.content, u); + else if (Array.isArray(s.content)) { + const mapper = (s) => cloneDeep(s, u); + o.content = s.content.map(mapper); + } else o.content = s.content; + else o.content = s.content; + return o; + } + throw new Yu("Value provided to cloneDeep function couldn't be cloned", { value: s }); + }; + cloneDeep.safe = (s) => { + try { + return cloneDeep(s); + } catch { + return s; + } + }; + const cloneShallowKeyValuePair = (s) => { + const { key: o, value: i } = s; + return new Cu.KeyValuePair(o, i); + }, + cloneShallowElement = (s) => { + const o = new s.constructor(); + if ( + ((o.element = s.element), + s.meta.length > 0 && (o._meta = cloneDeep(s.meta)), + s.attributes.length > 0 && (o._attributes = cloneDeep(s.attributes)), + Nu(s.content)) + ) { + const i = s.content; + o.content = cloneShallowElement(i); + } else + Array.isArray(s.content) + ? (o.content = [...s.content]) + : s.content instanceof Cu.KeyValuePair + ? (o.content = cloneShallowKeyValuePair(s.content)) + : (o.content = s.content); + return o; + }, + cloneShallow = (s) => { + if (s instanceof Cu.KeyValuePair) return cloneShallowKeyValuePair(s); + if (s instanceof Cu.ot) + return ((s) => { + const o = [...s]; + return new Cu.ot(o); + })(s); + if (s instanceof Cu.G6) + return ((s) => { + const o = [...s]; + return new Cu.G6(o); + })(s); + if (Nu(s)) return cloneShallowElement(s); + throw new Xu("Value provided to cloneShallow function couldn't be cloned", { + value: s + }); + }; + cloneShallow.safe = (s) => { + try { + return cloneShallow(s); + } catch { + return s; + } + }; + const visitor_getNodeType = (s) => + Fu(s) + ? 'ObjectElement' + : qu(s) + ? 'ArrayElement' + : $u(s) + ? 'MemberElement' + : Ru(s) + ? 'StringElement' + : Bu(s) + ? 'BooleanElement' + : Du(s) + ? 'NumberElement' + : Lu(s) + ? 'NullElement' + : Vu(s) + ? 'LinkElement' + : Uu(s) + ? 'RefElement' + : void 0, + visitor_cloneNode = (s) => (Nu(s) ? cloneShallow(s) : cloneNode(s)), + Zu = pipe(visitor_getNodeType, Yl), + Qu = { + ObjectElement: ['content'], + ArrayElement: ['content'], + MemberElement: ['key', 'value'], + StringElement: [], + BooleanElement: [], + NumberElement: [], + NullElement: [], + RefElement: [], + LinkElement: [], + Annotation: [], + Comment: [], + ParseResultElement: ['content'], + SourceMap: ['content'] + }; + class PredicateVisitor { + result; + predicate; + returnOnTrue; + returnOnFalse; + constructor({ predicate: s = es_F, returnOnTrue: o, returnOnFalse: i } = {}) { + ((this.result = []), + (this.predicate = s), + (this.returnOnTrue = o), + (this.returnOnFalse = i)); + } + enter(s) { + return this.predicate(s) + ? (this.result.push(s), this.returnOnTrue) + : this.returnOnFalse; + } + } + const visitor_visit = (s, o, { keyMap: i = Qu, ...u } = {}) => + visit(s, o, { + keyMap: i, + nodeTypeGetter: visitor_getNodeType, + nodePredicate: Zu, + nodeCloneFn: visitor_cloneNode, + ...u + }); + visitor_visit[Symbol.for('nodejs.util.promisify.custom')] = async ( + s, + o, + { keyMap: i = Qu, ...u } = {} + ) => + visit[Symbol.for('nodejs.util.promisify.custom')](s, o, { + keyMap: i, + nodeTypeGetter: visitor_getNodeType, + nodePredicate: Zu, + nodeCloneFn: visitor_cloneNode, + ...u + }); + const nodeTypeGetter = (s) => + 'string' == typeof (null == s ? void 0 : s.type) ? s.type : visitor_getNodeType(s), + ep = { EphemeralObject: ['content'], EphemeralArray: ['content'], ...Qu }, + value_visitor_visit = (s, o, { keyMap: i = ep, ...u } = {}) => + visitor_visit(s, o, { + keyMap: i, + nodeTypeGetter, + nodePredicate: es_T, + detectCycles: !1, + deleteNodeSymbol: Symbol.for('delete-node'), + skipVisitingNodeSymbol: Symbol.for('skip-visiting-node'), + ...u + }); + value_visitor_visit[Symbol.for('nodejs.util.promisify.custom')] = async ( + s, + { keyMap: o = ep, ...i } = {} + ) => + visitor_visit[Symbol.for('nodejs.util.promisify.custom')](s, visitor, { + keyMap: o, + nodeTypeGetter, + nodePredicate: es_T, + detectCycles: !1, + deleteNodeSymbol: Symbol.for('delete-node'), + skipVisitingNodeSymbol: Symbol.for('skip-visiting-node'), + ...i + }); + const tp = class EphemeralArray { + type = 'EphemeralArray'; + content = []; + reference = void 0; + constructor(s) { + ((this.content = s), (this.reference = [])); + } + toReference() { + return this.reference; + } + toArray() { + return (this.reference.push(...this.content), this.reference); + } + }; + const rp = class EphemeralObject { + type = 'EphemeralObject'; + content = []; + reference = void 0; + constructor(s) { + ((this.content = s), (this.reference = {})); + } + toReference() { + return this.reference; + } + toObject() { + return Object.assign(this.reference, Object.fromEntries(this.content)); + } + }; + class Visitor { + ObjectElement = { + enter: (s) => { + if (this.references.has(s)) return this.references.get(s).toReference(); + const o = new rp(s.content); + return (this.references.set(s, o), o); + } + }; + EphemeralObject = { leave: (s) => s.toObject() }; + MemberElement = { enter: (s) => [s.key, s.value] }; + ArrayElement = { + enter: (s) => { + if (this.references.has(s)) return this.references.get(s).toReference(); + const o = new tp(s.content); + return (this.references.set(s, o), o); + } + }; + EphemeralArray = { leave: (s) => s.toArray() }; + references = new WeakMap(); + BooleanElement(s) { + return s.toValue(); + } + NumberElement(s) { + return s.toValue(); + } + StringElement(s) { + return s.toValue(); + } + NullElement() { + return null; + } + RefElement(s, ...o) { + var i; + const u = o[3]; + return 'EphemeralObject' === + (null === (i = u[u.length - 1]) || void 0 === i ? void 0 : i.type) + ? Symbol.for('delete-node') + : String(s.toValue()); + } + LinkElement(s) { + return Ru(s.href) ? s.href.toValue() : ''; + } + } + const serializers_value = (s) => + Nu(s) + ? Ru(s) || Du(s) || Bu(s) || Lu(s) + ? s.toValue() + : value_visitor_visit(s, new Visitor()) + : s; + var np = _curry3(function mergeWithKey(s, o, i) { + var u, + _ = {}; + for (u in ((i = i || {}), (o = o || {}))) + _has(u, o) && (_[u] = _has(u, i) ? s(u, o[u], i[u]) : o[u]); + for (u in i) _has(u, i) && !_has(u, _) && (_[u] = i[u]); + return _; + }); + const sp = np; + var op = _curry3(function mergeDeepWithKey(s, o, i) { + return sp( + function (o, i, u) { + return _isObject(i) && _isObject(u) ? mergeDeepWithKey(s, i, u) : s(o, i, u); + }, + o, + i + ); + }); + const ip = op; + const lp = _curry2(function mergeDeepRight(s, o) { + return ip( + function (s, o, i) { + return i; + }, + s, + o + ); + }); + const cp = _curry2(_path); + const up = ja(0, -1); + var pp = _curry2(function apply(s, o) { + return s.apply(this, o); + }); + const hp = pp; + const dp = Ml(Wl); + var fp = _curry1(function empty(s) { + return null != s && 'function' == typeof s['fantasy-land/empty'] + ? s['fantasy-land/empty']() + : null != s && + null != s.constructor && + 'function' == typeof s.constructor['fantasy-land/empty'] + ? s.constructor['fantasy-land/empty']() + : null != s && 'function' == typeof s.empty + ? s.empty() + : null != s && null != s.constructor && 'function' == typeof s.constructor.empty + ? s.constructor.empty() + : aa(s) + ? [] + : _isString(s) + ? '' + : _isObject(s) + ? {} + : _i(s) + ? (function () { + return arguments; + })() + : (function _isTypedArray(s) { + var o = Object.prototype.toString.call(s); + return ( + '[object Uint8ClampedArray]' === o || + '[object Int8Array]' === o || + '[object Uint8Array]' === o || + '[object Int16Array]' === o || + '[object Uint16Array]' === o || + '[object Int32Array]' === o || + '[object Uint32Array]' === o || + '[object Float32Array]' === o || + '[object Float64Array]' === o || + '[object BigInt64Array]' === o || + '[object BigUint64Array]' === o + ); + })(s) + ? s.constructor.from('') + : void 0; + }); + const mp = fp; + const gp = _curry1(function isEmpty(s) { + return null != s && ra(s, mp(s)); + }); + const yp = za(1, Wl(Array.isArray) ? Array.isArray : pipe(ea, Vl('Array'))); + const vp = gu(yp, gp); + var bp = za(3, function (s, o, i) { + var u = cp(s, i), + _ = cp(up(s), i); + if (!dp(u) && !vp(s)) { + var w = Ea(u, _); + return hp(w, o); + } + }); + const _p = bp; + class Namespace extends Cu.g$ { + constructor() { + (super(), + this.register('annotation', Iu), + this.register('comment', Pu), + this.register('parseResult', Mu), + this.register('sourceMap', Tu)); + } + } + const Ep = new Namespace(), + createNamespace = (s) => { + const o = new Namespace(); + return (ku(s) && o.use(s), o); + }, + wp = Ep, + toolbox = () => ({ predicates: { ...le }, namespace: wp }), + Sp = { + toolboxCreator: toolbox, + visitorOptions: { nodeTypeGetter: visitor_getNodeType, exposeEdits: !0 } + }, + dispatchPluginsSync = (s, o, i = {}) => { + if (0 === o.length) return s; + const u = lp(Sp, i), + { toolboxCreator: _, visitorOptions: w } = u, + x = _(), + C = o.map((s) => s(x)), + j = mergeAll(C.map(La({}, 'visitor')), { ...w }); + C.forEach(_p(['pre'], [])); + const L = visitor_visit(s, j, w); + return (C.forEach(_p(['post'], [])), L); + }; + dispatchPluginsSync[Symbol.for('nodejs.util.promisify.custom')] = async (s, o, i = {}) => { + if (0 === o.length) return s; + const u = lp(Sp, i), + { toolboxCreator: _, visitorOptions: w } = u, + x = _(), + C = o.map((s) => s(x)), + j = mergeAll[Symbol.for('nodejs.util.promisify.custom')], + L = visitor_visit[Symbol.for('nodejs.util.promisify.custom')], + B = j(C.map(La({}, 'visitor')), { ...w }); + await Promise.allSettled(C.map(_p(['pre'], []))); + const $ = await L(s, B, w); + return (await Promise.allSettled(C.map(_p(['post'], []))), $); + }; + const refract = (s, { Type: o, plugins: i = [] }) => { + const u = new o(s); + return ( + Nu(s) && + (s.meta.length > 0 && (u.meta = cloneDeep(s.meta)), + s.attributes.length > 0 && (u.attributes = cloneDeep(s.attributes))), + dispatchPluginsSync(u, i, { + toolboxCreator: toolbox, + visitorOptions: { nodeTypeGetter: visitor_getNodeType } + }) + ); + }, + createRefractor = + (s) => + (o, i = {}) => + refract(o, { ...i, Type: s }); + ((Cu.Sh.refract = createRefractor(Cu.Sh)), + (Cu.wE.refract = createRefractor(Cu.wE)), + (Cu.Om.refract = createRefractor(Cu.Om)), + (Cu.bd.refract = createRefractor(Cu.bd)), + (Cu.Os.refract = createRefractor(Cu.Os)), + (Cu.kT.refract = createRefractor(Cu.kT)), + (Cu.Ft.refract = createRefractor(Cu.Ft)), + (Cu.sI.refract = createRefractor(Cu.sI)), + (Iu.refract = createRefractor(Iu)), + (Pu.refract = createRefractor(Pu)), + (Mu.refract = createRefractor(Mu)), + (Tu.refract = createRefractor(Tu))); + const computeEdges = (s, o = new WeakMap()) => ( + $u(s) + ? (o.set(s.key, s), computeEdges(s.key, o), o.set(s.value, s), computeEdges(s.value, o)) + : s.children.forEach((i) => { + (o.set(i, s), computeEdges(i, o)); + }), + o + ); + const xp = class Transcluder_Transcluder { + element; + edges; + constructor({ element: s }) { + this.element = s; + } + transclude(s, o) { + var i; + if (s === this.element) return o; + if (s === o) return this.element; + this.edges = + null !== (i = this.edges) && void 0 !== i ? i : computeEdges(this.element); + const u = this.edges.get(s); + return Rl(u) + ? void 0 + : (Fu(u) + ? ((s, o, i) => { + const u = i.get(s); + Fu(u) && + (u.content = u.map((_, w, x) => + x === s ? (i.delete(s), i.set(o, u), o) : x + )); + })(s, o, this.edges) + : qu(u) + ? ((s, o, i) => { + const u = i.get(s); + qu(u) && + (u.content = u.map((_) => + _ === s ? (i.delete(s), i.set(o, u), o) : _ + )); + })(s, o, this.edges) + : $u(u) && + ((s, o, i) => { + const u = i.get(s); + $u(u) && + (u.key === s && ((u.key = o), i.delete(s), i.set(o, u)), + u.value === s && ((u.value = o), i.delete(s), i.set(o, u))); + })(s, o, this.edges), + this.element); + } + }, + kp = pipe(Hl(/~/g, '~0'), Hl(/\//g, '~1'), encodeURIComponent); + const Cp = class JsonPointerError extends Jo {}; + const Op = class CompilationJsonPointerError extends Cp { + tokens; + constructor(s, o) { + (super(s, o), void 0 !== o && (this.tokens = [...o.tokens])); + } + }, + es_compile = (s) => { + try { + return 0 === s.length ? '' : `/${s.map(kp).join('/')}`; + } catch (o) { + throw new Op('JSON Pointer compilation of tokens encountered an error.', { + tokens: s, + cause: o + }); + } + }; + var Ap = _curry2(function converge(s, o) { + return za(Ca(Ll, 0, Fl('length', o)), function () { + var i = arguments, + u = this; + return s.apply( + u, + _map(function (s) { + return s.apply(u, i); + }, o) + ); + }); + }); + const jp = Ap; + function _identity(s) { + return s; + } + const Ip = _curry1(_identity); + var Pp = gu(za(1, pipe(ea, Vl('Number'))), isFinite); + var Mp = za(1, Pp); + var Tp = gu( + Wl(Number.isFinite) ? za(1, Ea(Number.isFinite, Number)) : Mp, + jp(ra, [Math.floor, Ip]) + ); + var Np = za(1, Tp); + const Rp = Wl(Number.isInteger) ? za(1, Ea(Number.isInteger, Number)) : Np; + var Dp = (function () { + function XTake(s, o) { + ((this.xf = o), (this.n = s), (this.i = 0)); + } + return ( + (XTake.prototype['@@transducer/init'] = _xfBase_init), + (XTake.prototype['@@transducer/result'] = _xfBase_result), + (XTake.prototype['@@transducer/step'] = function (s, o) { + this.i += 1; + var i = 0 === this.n ? s : this.xf['@@transducer/step'](s, o); + return this.n >= 0 && this.i >= this.n ? _reduced(i) : i; + }), + XTake + ); + })(); + function _xtake(s) { + return function (o) { + return new Dp(s, o); + }; + } + const Lp = _curry2( + _dispatchable(['take'], _xtake, function take(s, o) { + return ja(0, s < 0 ? 1 / 0 : s, o); + }) + ); + var Bp = _curry2(function (s, o) { + return ra(Lp(s.length, o), s); + }); + const Fp = Bp; + const qp = ra(''); + var $p = (function () { + function XDropWhile(s, o) { + ((this.xf = o), (this.f = s)); + } + return ( + (XDropWhile.prototype['@@transducer/init'] = _xfBase_init), + (XDropWhile.prototype['@@transducer/result'] = _xfBase_result), + (XDropWhile.prototype['@@transducer/step'] = function (s, o) { + if (this.f) { + if (this.f(o)) return s; + this.f = null; + } + return this.xf['@@transducer/step'](s, o); + }), + XDropWhile + ); + })(); + function _xdropWhile(s) { + return function (o) { + return new $p(s, o); + }; + } + const Vp = _curry2( + _dispatchable(['dropWhile'], _xdropWhile, function dropWhile(s, o) { + for (var i = 0, u = o.length; i < u && s(o[i]); ) i += 1; + return ja(i, 1 / 0, o); + }) + ); + const Up = Ja(function (s, o) { + return pipe(tl(''), Vp(_l(s)), yl(''))(o); + }), + zp = pipe(Hl(/~1/g, '/'), Hl(/~0/g, '~'), (s) => { + try { + return decodeURIComponent(s); + } catch { + return s; + } + }); + const Wp = class InvalidJsonPointerError extends Cp { + pointer; + constructor(s, o) { + (super(s, o), void 0 !== o && (this.pointer = o.pointer)); + } + }, + uriToPointer = (s) => { + const o = ((s) => { + const o = s.indexOf('#'); + return -1 !== o ? s.substring(o) : '#'; + })(s); + return Up('#', o); + }, + es_parse = (s) => { + if (qp(s)) return []; + if (!Fp('/', s)) + throw new Wp(`Invalid JSON Pointer "${s}". JSON Pointers must begin with "/"`, { + pointer: s + }); + try { + const o = pipe(tl('/'), kl(zp))(s); + return Ia(o); + } catch (o) { + throw new Wp(`JSON Pointer parsing of "${s}" encountered an error.`, { + pointer: s, + cause: o + }); + } + }; + const Kp = class EvaluationJsonPointerError extends Cp { + pointer; + tokens; + failedToken; + failedTokenPosition; + element; + constructor(s, o) { + (super(s, o), + void 0 !== o && + ((this.pointer = o.pointer), + Array.isArray(o.tokens) && (this.tokens = [...o.tokens]), + (this.failedToken = o.failedToken), + (this.failedTokenPosition = o.failedTokenPosition), + (this.element = o.element))); + } + }, + es_evaluate = (s, o) => { + let i; + try { + i = es_parse(s); + } catch (i) { + throw new Kp(`JSON Pointer evaluation failed while parsing the pointer "${s}".`, { + pointer: s, + element: cloneDeep(o), + cause: i + }); + } + return i.reduce((o, u, _) => { + if (Fu(o)) { + if (!o.hasKey(u)) + throw new Kp( + `JSON Pointer evaluation failed while evaluating token "${u}" against an ObjectElement`, + { + pointer: s, + tokens: i, + failedToken: u, + failedTokenPosition: _, + element: cloneDeep(o) + } + ); + return o.get(u); + } + if (qu(o)) { + if (!(u in o.content) || !Rp(Number(u))) + throw new Kp( + `JSON Pointer evaluation failed while evaluating token "${u}" against an ArrayElement`, + { + pointer: s, + tokens: i, + failedToken: u, + failedTokenPosition: _, + element: cloneDeep(o) + } + ); + return o.get(Number(u)); + } + throw new Kp( + `JSON Pointer evaluation failed while evaluating token "${u}" against an unexpected Element`, + { + pointer: s, + tokens: i, + failedToken: u, + failedTokenPosition: _, + element: cloneDeep(o) + } + ); + }, o); + }; + class Callback extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'callback')); + } + } + const Hp = Callback; + class Components extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'components')); + } + get schemas() { + return this.get('schemas'); + } + set schemas(s) { + this.set('schemas', s); + } + get responses() { + return this.get('responses'); + } + set responses(s) { + this.set('responses', s); + } + get parameters() { + return this.get('parameters'); + } + set parameters(s) { + this.set('parameters', s); + } + get examples() { + return this.get('examples'); + } + set examples(s) { + this.set('examples', s); + } + get requestBodies() { + return this.get('requestBodies'); + } + set requestBodies(s) { + this.set('requestBodies', s); + } + get headers() { + return this.get('headers'); + } + set headers(s) { + this.set('headers', s); + } + get securitySchemes() { + return this.get('securitySchemes'); + } + set securitySchemes(s) { + this.set('securitySchemes', s); + } + get links() { + return this.get('links'); + } + set links(s) { + this.set('links', s); + } + get callbacks() { + return this.get('callbacks'); + } + set callbacks(s) { + this.set('callbacks', s); + } + } + const Jp = Components; + class Contact extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'contact')); + } + get name() { + return this.get('name'); + } + set name(s) { + this.set('name', s); + } + get url() { + return this.get('url'); + } + set url(s) { + this.set('url', s); + } + get email() { + return this.get('email'); + } + set email(s) { + this.set('email', s); + } + } + const Gp = Contact; + class Discriminator extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'discriminator')); + } + get propertyName() { + return this.get('propertyName'); + } + set propertyName(s) { + this.set('propertyName', s); + } + get mapping() { + return this.get('mapping'); + } + set mapping(s) { + this.set('mapping', s); + } + } + const Yp = Discriminator; + class Encoding extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'encoding')); + } + get contentType() { + return this.get('contentType'); + } + set contentType(s) { + this.set('contentType', s); + } + get headers() { + return this.get('headers'); + } + set headers(s) { + this.set('headers', s); + } + get style() { + return this.get('style'); + } + set style(s) { + this.set('style', s); + } + get explode() { + return this.get('explode'); + } + set explode(s) { + this.set('explode', s); + } + get allowedReserved() { + return this.get('allowedReserved'); + } + set allowedReserved(s) { + this.set('allowedReserved', s); + } + } + const Xp = Encoding; + class Example extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'example')); + } + get summary() { + return this.get('summary'); + } + set summary(s) { + this.set('summary', s); + } + get description() { + return this.get('description'); + } + set description(s) { + this.set('description', s); + } + get value() { + return this.get('value'); + } + set value(s) { + this.set('value', s); + } + get externalValue() { + return this.get('externalValue'); + } + set externalValue(s) { + this.set('externalValue', s); + } + } + const Zp = Example; + class ExternalDocumentation extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'externalDocumentation')); + } + get description() { + return this.get('description'); + } + set description(s) { + this.set('description', s); + } + get url() { + return this.get('url'); + } + set url(s) { + this.set('url', s); + } + } + const Qp = ExternalDocumentation; + class Header extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'header')); + } + get required() { + return this.hasKey('required') ? this.get('required') : new Cu.bd(!1); + } + set required(s) { + this.set('required', s); + } + get deprecated() { + return this.hasKey('deprecated') ? this.get('deprecated') : new Cu.bd(!1); + } + set deprecated(s) { + this.set('deprecated', s); + } + get allowEmptyValue() { + return this.get('allowEmptyValue'); + } + set allowEmptyValue(s) { + this.set('allowEmptyValue', s); + } + get style() { + return this.get('style'); + } + set style(s) { + this.set('style', s); + } + get explode() { + return this.get('explode'); + } + set explode(s) { + this.set('explode', s); + } + get allowReserved() { + return this.get('allowReserved'); + } + set allowReserved(s) { + this.set('allowReserved', s); + } + get schema() { + return this.get('schema'); + } + set schema(s) { + this.set('schema', s); + } + get example() { + return this.get('example'); + } + set example(s) { + this.set('example', s); + } + get examples() { + return this.get('examples'); + } + set examples(s) { + this.set('examples', s); + } + get contentProp() { + return this.get('content'); + } + set contentProp(s) { + this.set('content', s); + } + } + Object.defineProperty(Header.prototype, 'description', { + get() { + return this.get('description'); + }, + set(s) { + this.set('description', s); + }, + enumerable: !0 + }); + const th = Header; + class Info extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'info'), this.classes.push('info')); + } + get title() { + return this.get('title'); + } + set title(s) { + this.set('title', s); + } + get description() { + return this.get('description'); + } + set description(s) { + this.set('description', s); + } + get termsOfService() { + return this.get('termsOfService'); + } + set termsOfService(s) { + this.set('termsOfService', s); + } + get contact() { + return this.get('contact'); + } + set contact(s) { + this.set('contact', s); + } + get license() { + return this.get('license'); + } + set license(s) { + this.set('license', s); + } + get version() { + return this.get('version'); + } + set version(s) { + this.set('version', s); + } + } + const rh = Info; + class License extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'license')); + } + get name() { + return this.get('name'); + } + set name(s) { + this.set('name', s); + } + get url() { + return this.get('url'); + } + set url(s) { + this.set('url', s); + } + } + const uh = License; + class Link extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'link')); + } + get operationRef() { + return this.get('operationRef'); + } + set operationRef(s) { + this.set('operationRef', s); + } + get operationId() { + return this.get('operationId'); + } + set operationId(s) { + this.set('operationId', s); + } + get operation() { + var s, o; + return Ru(this.operationRef) + ? null === (s = this.operationRef) || void 0 === s + ? void 0 + : s.meta.get('operation') + : Ru(this.operationId) + ? null === (o = this.operationId) || void 0 === o + ? void 0 + : o.meta.get('operation') + : void 0; + } + set operation(s) { + this.set('operation', s); + } + get parameters() { + return this.get('parameters'); + } + set parameters(s) { + this.set('parameters', s); + } + get requestBody() { + return this.get('requestBody'); + } + set requestBody(s) { + this.set('requestBody', s); + } + get description() { + return this.get('description'); + } + set description(s) { + this.set('description', s); + } + get server() { + return this.get('server'); + } + set server(s) { + this.set('server', s); + } + } + const dh = Link; + class MediaType extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'mediaType')); + } + get schema() { + return this.get('schema'); + } + set schema(s) { + this.set('schema', s); + } + get example() { + return this.get('example'); + } + set example(s) { + this.set('example', s); + } + get examples() { + return this.get('examples'); + } + set examples(s) { + this.set('examples', s); + } + get encoding() { + return this.get('encoding'); + } + set encoding(s) { + this.set('encoding', s); + } + } + const fh = MediaType; + class OAuthFlow extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'oAuthFlow')); + } + get authorizationUrl() { + return this.get('authorizationUrl'); + } + set authorizationUrl(s) { + this.set('authorizationUrl', s); + } + get tokenUrl() { + return this.get('tokenUrl'); + } + set tokenUrl(s) { + this.set('tokenUrl', s); + } + get refreshUrl() { + return this.get('refreshUrl'); + } + set refreshUrl(s) { + this.set('refreshUrl', s); + } + get scopes() { + return this.get('scopes'); + } + set scopes(s) { + this.set('scopes', s); + } + } + const vh = OAuthFlow; + class OAuthFlows extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'oAuthFlows')); + } + get implicit() { + return this.get('implicit'); + } + set implicit(s) { + this.set('implicit', s); + } + get password() { + return this.get('password'); + } + set password(s) { + this.set('password', s); + } + get clientCredentials() { + return this.get('clientCredentials'); + } + set clientCredentials(s) { + this.set('clientCredentials', s); + } + get authorizationCode() { + return this.get('authorizationCode'); + } + set authorizationCode(s) { + this.set('authorizationCode', s); + } + } + const _h = OAuthFlows; + class Openapi extends Cu.Om { + constructor(s, o, i) { + (super(s, o, i), + (this.element = 'openapi'), + this.classes.push('spec-version'), + this.classes.push('version')); + } + } + const wh = Openapi; + class OpenApi3_0 extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'openApi3_0'), this.classes.push('api')); + } + get openapi() { + return this.get('openapi'); + } + set openapi(s) { + this.set('openapi', s); + } + get info() { + return this.get('info'); + } + set info(s) { + this.set('info', s); + } + get servers() { + return this.get('servers'); + } + set servers(s) { + this.set('servers', s); + } + get paths() { + return this.get('paths'); + } + set paths(s) { + this.set('paths', s); + } + get components() { + return this.get('components'); + } + set components(s) { + this.set('components', s); + } + get security() { + return this.get('security'); + } + set security(s) { + this.set('security', s); + } + get tags() { + return this.get('tags'); + } + set tags(s) { + this.set('tags', s); + } + get externalDocs() { + return this.get('externalDocs'); + } + set externalDocs(s) { + this.set('externalDocs', s); + } + } + const Oh = OpenApi3_0; + class Operation extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'operation')); + } + get tags() { + return this.get('tags'); + } + set tags(s) { + this.set('tags', s); + } + get summary() { + return this.get('summary'); + } + set summary(s) { + this.set('summary', s); + } + get description() { + return this.get('description'); + } + set description(s) { + this.set('description', s); + } + set externalDocs(s) { + this.set('externalDocs', s); + } + get externalDocs() { + return this.get('externalDocs'); + } + get operationId() { + return this.get('operationId'); + } + set operationId(s) { + this.set('operationId', s); + } + get parameters() { + return this.get('parameters'); + } + set parameters(s) { + this.set('parameters', s); + } + get requestBody() { + return this.get('requestBody'); + } + set requestBody(s) { + this.set('requestBody', s); + } + get responses() { + return this.get('responses'); + } + set responses(s) { + this.set('responses', s); + } + get callbacks() { + return this.get('callbacks'); + } + set callbacks(s) { + this.set('callbacks', s); + } + get deprecated() { + return this.hasKey('deprecated') ? this.get('deprecated') : new Cu.bd(!1); + } + set deprecated(s) { + this.set('deprecated', s); + } + get security() { + return this.get('security'); + } + set security(s) { + this.set('security', s); + } + get servers() { + return this.get('severs'); + } + set servers(s) { + this.set('servers', s); + } + } + const jh = Operation; + class Parameter extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'parameter')); + } + get name() { + return this.get('name'); + } + set name(s) { + this.set('name', s); + } + get in() { + return this.get('in'); + } + set in(s) { + this.set('in', s); + } + get required() { + return this.hasKey('required') ? this.get('required') : new Cu.bd(!1); + } + set required(s) { + this.set('required', s); + } + get deprecated() { + return this.hasKey('deprecated') ? this.get('deprecated') : new Cu.bd(!1); + } + set deprecated(s) { + this.set('deprecated', s); + } + get allowEmptyValue() { + return this.get('allowEmptyValue'); + } + set allowEmptyValue(s) { + this.set('allowEmptyValue', s); + } + get style() { + return this.get('style'); + } + set style(s) { + this.set('style', s); + } + get explode() { + return this.get('explode'); + } + set explode(s) { + this.set('explode', s); + } + get allowReserved() { + return this.get('allowReserved'); + } + set allowReserved(s) { + this.set('allowReserved', s); + } + get schema() { + return this.get('schema'); + } + set schema(s) { + this.set('schema', s); + } + get example() { + return this.get('example'); + } + set example(s) { + this.set('example', s); + } + get examples() { + return this.get('examples'); + } + set examples(s) { + this.set('examples', s); + } + get contentProp() { + return this.get('content'); + } + set contentProp(s) { + this.set('content', s); + } + } + Object.defineProperty(Parameter.prototype, 'description', { + get() { + return this.get('description'); + }, + set(s) { + this.set('description', s); + }, + enumerable: !0 + }); + const Ih = Parameter; + class PathItem extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'pathItem')); + } + get $ref() { + return this.get('$ref'); + } + set $ref(s) { + this.set('$ref', s); + } + get summary() { + return this.get('summary'); + } + set summary(s) { + this.set('summary', s); + } + get description() { + return this.get('description'); + } + set description(s) { + this.set('description', s); + } + get GET() { + return this.get('get'); + } + set GET(s) { + this.set('GET', s); + } + get PUT() { + return this.get('put'); + } + set PUT(s) { + this.set('PUT', s); + } + get POST() { + return this.get('post'); + } + set POST(s) { + this.set('POST', s); + } + get DELETE() { + return this.get('delete'); + } + set DELETE(s) { + this.set('DELETE', s); + } + get OPTIONS() { + return this.get('options'); + } + set OPTIONS(s) { + this.set('OPTIONS', s); + } + get HEAD() { + return this.get('head'); + } + set HEAD(s) { + this.set('HEAD', s); + } + get PATCH() { + return this.get('patch'); + } + set PATCH(s) { + this.set('PATCH', s); + } + get TRACE() { + return this.get('trace'); + } + set TRACE(s) { + this.set('TRACE', s); + } + get servers() { + return this.get('servers'); + } + set servers(s) { + this.set('servers', s); + } + get parameters() { + return this.get('parameters'); + } + set parameters(s) { + this.set('parameters', s); + } + } + const Ph = PathItem; + class Paths extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'paths')); + } + } + const Rh = Paths; + class Reference extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'reference'), this.classes.push('openapi-reference')); + } + get $ref() { + return this.get('$ref'); + } + set $ref(s) { + this.set('$ref', s); + } + } + const Dh = Reference; + class RequestBody extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'requestBody')); + } + get description() { + return this.get('description'); + } + set description(s) { + this.set('description', s); + } + get contentProp() { + return this.get('content'); + } + set contentProp(s) { + this.set('content', s); + } + get required() { + return this.hasKey('required') ? this.get('required') : new Cu.bd(!1); + } + set required(s) { + this.set('required', s); + } + } + const Lh = RequestBody; + class Response_Response extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'response')); + } + get description() { + return this.get('description'); + } + set description(s) { + this.set('description', s); + } + get headers() { + return this.get('headers'); + } + set headers(s) { + this.set('headers', s); + } + get contentProp() { + return this.get('content'); + } + set contentProp(s) { + this.set('content', s); + } + get links() { + return this.get('links'); + } + set links(s) { + this.set('links', s); + } + } + const Fh = Response_Response; + class Responses extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'responses')); + } + get default() { + return this.get('default'); + } + set default(s) { + this.set('default', s); + } + } + const Kh = Responses; + const Hh = class UnsupportedOperationError extends Ho {}; + class JSONSchema extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'JSONSchemaDraft4')); + } + get idProp() { + return this.get('id'); + } + set idProp(s) { + this.set('id', s); + } + get $schema() { + return this.get('$schema'); + } + set $schema(s) { + this.set('$schema', s); + } + get multipleOf() { + return this.get('multipleOf'); + } + set multipleOf(s) { + this.set('multipleOf', s); + } + get maximum() { + return this.get('maximum'); + } + set maximum(s) { + this.set('maximum', s); + } + get exclusiveMaximum() { + return this.get('exclusiveMaximum'); + } + set exclusiveMaximum(s) { + this.set('exclusiveMaximum', s); + } + get minimum() { + return this.get('minimum'); + } + set minimum(s) { + this.set('minimum', s); + } + get exclusiveMinimum() { + return this.get('exclusiveMinimum'); + } + set exclusiveMinimum(s) { + this.set('exclusiveMinimum', s); + } + get maxLength() { + return this.get('maxLength'); + } + set maxLength(s) { + this.set('maxLength', s); + } + get minLength() { + return this.get('minLength'); + } + set minLength(s) { + this.set('minLength', s); + } + get pattern() { + return this.get('pattern'); + } + set pattern(s) { + this.set('pattern', s); + } + get additionalItems() { + return this.get('additionalItems'); + } + set additionalItems(s) { + this.set('additionalItems', s); + } + get items() { + return this.get('items'); + } + set items(s) { + this.set('items', s); + } + get maxItems() { + return this.get('maxItems'); + } + set maxItems(s) { + this.set('maxItems', s); + } + get minItems() { + return this.get('minItems'); + } + set minItems(s) { + this.set('minItems', s); + } + get uniqueItems() { + return this.get('uniqueItems'); + } + set uniqueItems(s) { + this.set('uniqueItems', s); + } + get maxProperties() { + return this.get('maxProperties'); + } + set maxProperties(s) { + this.set('maxProperties', s); + } + get minProperties() { + return this.get('minProperties'); + } + set minProperties(s) { + this.set('minProperties', s); + } + get required() { + return this.get('required'); + } + set required(s) { + this.set('required', s); + } + get properties() { + return this.get('properties'); + } + set properties(s) { + this.set('properties', s); + } + get additionalProperties() { + return this.get('additionalProperties'); + } + set additionalProperties(s) { + this.set('additionalProperties', s); + } + get patternProperties() { + return this.get('patternProperties'); + } + set patternProperties(s) { + this.set('patternProperties', s); + } + get dependencies() { + return this.get('dependencies'); + } + set dependencies(s) { + this.set('dependencies', s); + } + get enum() { + return this.get('enum'); + } + set enum(s) { + this.set('enum', s); + } + get type() { + return this.get('type'); + } + set type(s) { + this.set('type', s); + } + get allOf() { + return this.get('allOf'); + } + set allOf(s) { + this.set('allOf', s); + } + get anyOf() { + return this.get('anyOf'); + } + set anyOf(s) { + this.set('anyOf', s); + } + get oneOf() { + return this.get('oneOf'); + } + set oneOf(s) { + this.set('oneOf', s); + } + get not() { + return this.get('not'); + } + set not(s) { + this.set('not', s); + } + get definitions() { + return this.get('definitions'); + } + set definitions(s) { + this.set('definitions', s); + } + get title() { + return this.get('title'); + } + set title(s) { + this.set('title', s); + } + get description() { + return this.get('description'); + } + set description(s) { + this.set('description', s); + } + get default() { + return this.get('default'); + } + set default(s) { + this.set('default', s); + } + get format() { + return this.get('format'); + } + set format(s) { + this.set('format', s); + } + get base() { + return this.get('base'); + } + set base(s) { + this.set('base', s); + } + get links() { + return this.get('links'); + } + set links(s) { + this.set('links', s); + } + get media() { + return this.get('media'); + } + set media(s) { + this.set('media', s); + } + get readOnly() { + return this.get('readOnly'); + } + set readOnly(s) { + this.set('readOnly', s); + } + } + const Jh = JSONSchema; + class JSONReference extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'JSONReference'), this.classes.push('json-reference')); + } + get $ref() { + return this.get('$ref'); + } + set $ref(s) { + this.set('$ref', s); + } + } + const Gh = JSONReference; + class Media extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'media')); + } + get binaryEncoding() { + return this.get('binaryEncoding'); + } + set binaryEncoding(s) { + this.set('binaryEncoding', s); + } + get type() { + return this.get('type'); + } + set type(s) { + this.set('type', s); + } + } + const Qh = Media; + class LinkDescription extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'linkDescription')); + } + get href() { + return this.get('href'); + } + set href(s) { + this.set('href', s); + } + get rel() { + return this.get('rel'); + } + set rel(s) { + this.set('rel', s); + } + get title() { + return this.get('title'); + } + set title(s) { + this.set('title', s); + } + get targetSchema() { + return this.get('targetSchema'); + } + set targetSchema(s) { + this.set('targetSchema', s); + } + get mediaType() { + return this.get('mediaType'); + } + set mediaType(s) { + this.set('mediaType', s); + } + get method() { + return this.get('method'); + } + set method(s) { + this.set('method', s); + } + get encType() { + return this.get('encType'); + } + set encType(s) { + this.set('encType', s); + } + get schema() { + return this.get('schema'); + } + set schema(s) { + this.set('schema', s); + } + } + const td = LinkDescription; + var sd = _curry2(function mapObjIndexed(s, o) { + return _arrayReduce( + function (i, u) { + return ((i[u] = s(o[u], u, o)), i); + }, + {}, + Wi(o) + ); + }); + const id = sd; + const ld = _curry1(function isNil(s) { + return null == s; + }); + var cd = _curry2(function hasPath(s, o) { + if (0 === s.length || ld(o)) return !1; + for (var i = o, u = 0; u < s.length; ) { + if (ld(i) || !_has(s[u], i)) return !1; + ((i = i[s[u]]), (u += 1)); + } + return !0; + }); + const ud = cd; + var dd = _curry2(function has(s, o) { + return ud([s], o); + }); + const md = dd; + const yd = _curry3(function propSatisfies(s, o, i) { + return s(Da(o, i)); + }), + dereference = (s, o) => { + const i = Na(s, o); + return id((s) => { + if (ku(s) && md('$ref', s) && yd(Yl, '$ref', s)) { + const o = cp(['$ref'], s), + u = Up('#/', o); + return cp(u.split('/'), i); + } + return ku(s) ? dereference(s, i) : s; + }, s); + }, + emptyElement = (s) => { + const o = s.meta.length > 0 ? cloneDeep(s.meta) : void 0, + i = s.attributes.length > 0 ? cloneDeep(s.attributes) : void 0; + return new s.constructor(void 0, o, i); + }, + cloneUnlessOtherwiseSpecified = (s, o) => + o.clone && o.isMergeableElement(s) ? deepmerge(emptyElement(s), s, o) : s, + getMetaMergeFunction = (s) => + 'function' != typeof s.customMetaMerge ? (s) => cloneDeep(s) : s.customMetaMerge, + getAttributesMergeFunction = (s) => + 'function' != typeof s.customAttributesMerge + ? (s) => cloneDeep(s) + : s.customAttributesMerge, + vd = { + clone: !0, + isMergeableElement: (s) => Fu(s) || qu(s), + arrayElementMerge: (s, o, i) => + s.concat(o)['fantasy-land/map']((s) => cloneUnlessOtherwiseSpecified(s, i)), + objectElementMerge: (s, o, i) => { + const u = Fu(s) ? emptyElement(s) : emptyElement(o); + return ( + Fu(s) && + s.forEach((s, o, _) => { + const w = cloneShallow(_); + ((w.value = cloneUnlessOtherwiseSpecified(s, i)), u.content.push(w)); + }), + o.forEach((o, _, w) => { + const x = serializers_value(_); + let C; + if (Fu(s) && s.hasKey(x) && i.isMergeableElement(o)) { + const u = s.get(x); + ((C = cloneShallow(w)), + (C.value = ((s, o) => { + if ('function' != typeof o.customMerge) return deepmerge; + const i = o.customMerge(s, o); + return 'function' == typeof i ? i : deepmerge; + })(_, i)(u, o))); + } else ((C = cloneShallow(w)), (C.value = cloneUnlessOtherwiseSpecified(o, i))); + (u.remove(x), u.content.push(C)); + }), + u + ); + }, + customMerge: void 0, + customMetaMerge: void 0, + customAttributesMerge: void 0 + }; + function deepmerge(s, o, i) { + var u, _, w; + const x = { ...vd, ...i }; + ((x.isMergeableElement = + null !== (u = x.isMergeableElement) && void 0 !== u ? u : vd.isMergeableElement), + (x.arrayElementMerge = + null !== (_ = x.arrayElementMerge) && void 0 !== _ ? _ : vd.arrayElementMerge), + (x.objectElementMerge = + null !== (w = x.objectElementMerge) && void 0 !== w ? w : vd.objectElementMerge)); + const C = qu(o); + if (!(C === qu(s))) return cloneUnlessOtherwiseSpecified(o, x); + const j = + C && 'function' == typeof x.arrayElementMerge + ? x.arrayElementMerge(s, o, x) + : x.objectElementMerge(s, o, x); + return ( + (j.meta = getMetaMergeFunction(x)(s.meta, o.meta)), + (j.attributes = getAttributesMergeFunction(x)(s.attributes, o.attributes)), + j + ); + } + deepmerge.all = (s, o) => { + if (!Array.isArray(s)) + throw new TypeError('First argument of deepmerge should be an array.'); + return 0 === s.length + ? new Cu.Sh() + : s.reduce((s, i) => deepmerge(s, i, o), emptyElement(s[0])); + }; + const _d = class Visitor_Visitor { + element; + constructor(s) { + Object.assign(this, s); + } + copyMetaAndAttributes(s, o) { + ((s.meta.length > 0 || o.meta.length > 0) && + ((o.meta = deepmerge(o.meta, s.meta)), + hasElementSourceMap(s) && o.meta.set('sourceMap', s.meta.get('sourceMap'))), + (s.attributes.length > 0 || s.meta.length > 0) && + (o.attributes = deepmerge(o.attributes, s.attributes))); + } + }; + const Ed = class FallbackVisitor extends _d { + enter(s) { + return ((this.element = cloneDeep(s)), Ju); + } + }, + copyProps = (s, o, i = []) => { + const u = Object.getOwnPropertyDescriptors(o); + for (let s of i) delete u[s]; + Object.defineProperties(s, u); + }, + protoChain = (s, o = [s]) => { + const i = Object.getPrototypeOf(s); + return null === i ? o : protoChain(i, [...o, i]); + }, + hardMixProtos = (s, o, i = []) => { + var u; + const _ = + null !== + (u = ((...s) => { + if (0 === s.length) return; + let o; + const i = s.map((s) => protoChain(s)); + for (; i.every((s) => s.length > 0); ) { + const s = i.map((s) => s.pop()), + u = s[0]; + if (!s.every((s) => s === u)) break; + o = u; + } + return o; + })(...s)) && void 0 !== u + ? u + : Object.prototype, + w = Object.create(_), + x = protoChain(_); + for (let o of s) { + let s = protoChain(o); + for (let o = s.length - 1; o >= 0; o--) { + let u = s[o]; + -1 === x.indexOf(u) && (copyProps(w, u, ['constructor', ...i]), x.push(u)); + } + } + return ((w.constructor = o), w); + }, + unique = (s) => s.filter((o, i) => s.indexOf(o) == i), + getIngredientWithProp = (s, o) => { + const i = o.map((s) => protoChain(s)); + let u = 0, + _ = !0; + for (; _; ) { + _ = !1; + for (let w = o.length - 1; w >= 0; w--) { + const o = i[w][u]; + if (null != o && ((_ = !0), null != Object.getOwnPropertyDescriptor(o, s))) + return i[w][0]; + } + u++; + } + }, + proxyMix = (s, o = Object.prototype) => + new Proxy( + {}, + { + getPrototypeOf: () => o, + setPrototypeOf() { + throw Error('Cannot set prototype of Proxies created by ts-mixer'); + }, + getOwnPropertyDescriptor: (o, i) => + Object.getOwnPropertyDescriptor(getIngredientWithProp(i, s) || {}, i), + defineProperty() { + throw new Error('Cannot define new properties on Proxies created by ts-mixer'); + }, + has: (i, u) => void 0 !== getIngredientWithProp(u, s) || void 0 !== o[u], + get: (i, u) => (getIngredientWithProp(u, s) || o)[u], + set(o, i, u) { + const _ = getIngredientWithProp(i, s); + if (void 0 === _) + throw new Error('Cannot set new properties on Proxies created by ts-mixer'); + return ((_[i] = u), !0); + }, + deleteProperty() { + throw new Error('Cannot delete properties on Proxies created by ts-mixer'); + }, + ownKeys: () => + s + .map(Object.getOwnPropertyNames) + .reduce((s, o) => o.concat(s.filter((s) => o.indexOf(s) < 0))) + } + ), + wd = null, + Sd = 'copy', + xd = 'copy', + kd = 'deep', + Cd = new WeakMap(), + getMixinsForClass = (s) => Cd.get(s), + mergeObjectsOfDecorators = (s, o) => { + var i, u; + const _ = unique([...Object.getOwnPropertyNames(s), ...Object.getOwnPropertyNames(o)]), + w = {}; + for (let x of _) + w[x] = unique([ + ...(null !== (i = null == s ? void 0 : s[x]) && void 0 !== i ? i : []), + ...(null !== (u = null == o ? void 0 : o[x]) && void 0 !== u ? u : []) + ]); + return w; + }, + mergePropertyAndMethodDecorators = (s, o) => { + var i, u, _, w; + return { + property: mergeObjectsOfDecorators( + null !== (i = null == s ? void 0 : s.property) && void 0 !== i ? i : {}, + null !== (u = null == o ? void 0 : o.property) && void 0 !== u ? u : {} + ), + method: mergeObjectsOfDecorators( + null !== (_ = null == s ? void 0 : s.method) && void 0 !== _ ? _ : {}, + null !== (w = null == o ? void 0 : o.method) && void 0 !== w ? w : {} + ) + }; + }, + mergeDecorators = (s, o) => { + var i, u, _, w, x, C; + return { + class: unique([ + ...(null !== (i = null == s ? void 0 : s.class) && void 0 !== i ? i : []), + ...(null !== (u = null == o ? void 0 : o.class) && void 0 !== u ? u : []) + ]), + static: mergePropertyAndMethodDecorators( + null !== (_ = null == s ? void 0 : s.static) && void 0 !== _ ? _ : {}, + null !== (w = null == o ? void 0 : o.static) && void 0 !== w ? w : {} + ), + instance: mergePropertyAndMethodDecorators( + null !== (x = null == s ? void 0 : s.instance) && void 0 !== x ? x : {}, + null !== (C = null == o ? void 0 : o.instance) && void 0 !== C ? C : {} + ) + }; + }, + Od = new Map(), + deepDecoratorSearch = (...s) => { + const o = ((...s) => { + var o; + const i = new Set(), + u = new Set([...s]); + for (; u.size > 0; ) + for (let s of u) { + const _ = protoChain(s.prototype).map((s) => s.constructor), + w = [ + ..._, + ...(null !== (o = getMixinsForClass(s)) && void 0 !== o ? o : []) + ].filter((s) => !i.has(s)); + for (let s of w) u.add(s); + (i.add(s), u.delete(s)); + } + return [...i]; + })(...s) + .map((s) => Od.get(s)) + .filter((s) => !!s); + return 0 == o.length + ? {} + : 1 == o.length + ? o[0] + : o.reduce((s, o) => mergeDecorators(s, o)); + }, + getDecoratorsForClass = (s) => { + let o = Od.get(s); + return (o || ((o = {}), Od.set(s, o)), o); + }; + function Mixin(...s) { + var o, i, u; + const _ = s.map((s) => s.prototype), + w = wd; + if (null !== w) { + const s = _.map((s) => s[w]).filter((s) => 'function' == typeof s), + combinedInitFunction = function (...o) { + for (let i of s) i.apply(this, o); + }, + o = { [w]: combinedInitFunction }; + _.push(o); + } + function MixedClass(...o) { + for (const i of s) copyProps(this, new i(...o)); + null !== w && 'function' == typeof this[w] && this[w].apply(this, o); + } + var x, C; + ((MixedClass.prototype = + 'copy' === xd + ? hardMixProtos(_, MixedClass) + : ((x = _), (C = MixedClass), proxyMix([...x, { constructor: C }]))), + Object.setPrototypeOf( + MixedClass, + 'copy' === Sd + ? hardMixProtos(s, null, ['prototype']) + : proxyMix(s, Function.prototype) + )); + let j = MixedClass; + if ('none' !== kd) { + const _ = + 'deep' === kd + ? deepDecoratorSearch(...s) + : ((...s) => { + const o = s.map((s) => getDecoratorsForClass(s)); + return 0 === o.length + ? {} + : 1 === o.length + ? o[0] + : o.reduce((s, o) => mergeDecorators(s, o)); + })(...s); + for (let s of null !== (o = null == _ ? void 0 : _.class) && void 0 !== o ? o : []) { + const o = s(j); + o && (j = o); + } + (applyPropAndMethodDecorators( + null !== (i = null == _ ? void 0 : _.static) && void 0 !== i ? i : {}, + j + ), + applyPropAndMethodDecorators( + null !== (u = null == _ ? void 0 : _.instance) && void 0 !== u ? u : {}, + j.prototype + )); + } + var L, B; + return ((L = j), (B = s), Cd.set(L, B), j); + } + const applyPropAndMethodDecorators = (s, o) => { + const i = s.property, + u = s.method; + if (i) for (let s in i) for (let u of i[s]) u(o, s); + if (u) + for (let s in u) for (let i of u[s]) i(o, s, Object.getOwnPropertyDescriptor(o, s)); + }; + const Ad = _curry2(function pick(s, o) { + for (var i = {}, u = 0; u < s.length; ) (s[u] in o && (i[s[u]] = o[s[u]]), (u += 1)); + return i; + }); + const Id = class SpecificationVisitor extends _d { + specObj; + passingOptionsNames = ['specObj']; + constructor({ specObj: s, ...o }) { + (super({ ...o }), (this.specObj = s)); + } + retrievePassingOptions() { + return Ad(this.passingOptionsNames, this); + } + retrieveFixedFields(s) { + const o = cp(['visitors', ...s, 'fixedFields'], this.specObj); + return 'object' == typeof o && null !== o ? Object.keys(o) : []; + } + retrieveVisitor(s) { + return Xo(Wl, ['visitors', ...s], this.specObj) + ? cp(['visitors', ...s], this.specObj) + : cp(['visitors', ...s, '$visitor'], this.specObj); + } + retrieveVisitorInstance(s, o = {}) { + const i = this.retrievePassingOptions(); + return new (this.retrieveVisitor(s))({ ...i, ...o }); + } + toRefractedElement(s, o, i = {}) { + const u = this.retrieveVisitorInstance(s, i); + return u instanceof Ed && (null == u ? void 0 : u.constructor) === Ed + ? cloneDeep(o) + : (visitor_visit(o, u, i), u.element); + } + }; + const Md = class FixedFieldsVisitor extends Id { + specPath; + ignoredFields; + constructor({ specPath: s, ignoredFields: o, ...i }) { + (super({ ...i }), (this.specPath = s), (this.ignoredFields = o || [])); + } + ObjectElement(s) { + const o = this.specPath(s), + i = this.retrieveFixedFields(o); + return ( + s.forEach((s, u, _) => { + if ( + Ru(u) && + i.includes(serializers_value(u)) && + !this.ignoredFields.includes(serializers_value(u)) + ) { + const i = this.toRefractedElement([...o, 'fixedFields', serializers_value(u)], s), + w = new Cu.Pr(cloneDeep(u), i); + (this.copyMetaAndAttributes(_, w), + w.classes.push('fixed-field'), + this.element.content.push(w)); + } else + this.ignoredFields.includes(serializers_value(u)) || + this.element.content.push(cloneDeep(_)); + }), + this.copyMetaAndAttributes(s, this.element), + Ju + ); + } + }; + class JSONSchemaVisitor extends Mixin(Md, Ed) { + constructor(s) { + (super(s), + (this.element = new Jh()), + (this.specPath = Tl(['document', 'objects', 'JSONSchema']))); + } + } + const Td = JSONSchemaVisitor; + const Nd = class ParentSchemaAwareVisitor { + parent; + constructor({ parent: s }) { + this.parent = s; + } + }, + isJSONReferenceLikeElement = (s) => Fu(s) && s.hasKey('$ref'); + class ItemsVisitor extends Mixin(Id, Nd, Ed) { + ObjectElement(s) { + const o = isJSONReferenceLikeElement(s) + ? ['document', 'objects', 'JSONReference'] + : ['document', 'objects', 'JSONSchema']; + return ((this.element = this.toRefractedElement(o, s)), Ju); + } + ArrayElement(s) { + return ( + (this.element = new Cu.wE()), + this.element.classes.push('json-schema-items'), + s.forEach((s) => { + const o = isJSONReferenceLikeElement(s) + ? ['document', 'objects', 'JSONReference'] + : ['document', 'objects', 'JSONSchema'], + i = this.toRefractedElement(o, s); + this.element.push(i); + }), + this.copyMetaAndAttributes(s, this.element), + Ju + ); + } + } + const Rd = ItemsVisitor; + const Dd = class RequiredVisitor extends Ed { + ArrayElement(s) { + const o = this.enter(s); + return (this.element.classes.push('json-schema-required'), o); + } + }; + const Ld = _curry1(function allPass(s) { + return za(Ca(Ll, 0, Fl('length', s)), function () { + for (var o = 0, i = s.length; o < i; ) { + if (!s[o].apply(this, arguments)) return !1; + o += 1; + } + return !0; + }); + }); + const Bd = _curry1(function isNotEmpty(s) { + return !gp(s); + }); + const Fd = _curry2(function or(s, o) { + return s || o; + }); + var $d = Ml( + za( + 1, + gu( + vu, + _curry2(function either(s, o) { + return _isFunction(s) + ? function _either() { + return s.apply(this, arguments) || o.apply(this, arguments); + } + : Pl(Fd)(s, o); + })(bu, Wl) + ) + ) + ); + const Vd = Ld([Yl, $d, Bd]); + const Ud = class PatternedFieldsVisitor extends Id { + specPath; + ignoredFields; + fieldPatternPredicate = es_F; + constructor({ specPath: s, ignoredFields: o, fieldPatternPredicate: i, ...u }) { + (super({ ...u }), + (this.specPath = s), + (this.ignoredFields = o || []), + 'function' == typeof i && (this.fieldPatternPredicate = i)); + } + ObjectElement(s) { + return ( + s.forEach((s, o, i) => { + if ( + !this.ignoredFields.includes(serializers_value(o)) && + this.fieldPatternPredicate(serializers_value(o)) + ) { + const u = this.specPath(s), + _ = this.toRefractedElement(u, s), + w = new Cu.Pr(cloneDeep(o), _); + (this.copyMetaAndAttributes(i, w), + w.classes.push('patterned-field'), + this.element.content.push(w)); + } else + this.ignoredFields.includes(serializers_value(o)) || + this.element.content.push(cloneDeep(i)); + }), + this.copyMetaAndAttributes(s, this.element), + Ju + ); + } + }; + const Wd = class MapVisitor extends Ud { + constructor(s) { + (super(s), (this.fieldPatternPredicate = Vd)); + } + }; + class PropertiesVisitor extends Mixin(Wd, Nd, Ed) { + constructor(s) { + (super(s), + (this.element = new Cu.Sh()), + this.element.classes.push('json-schema-properties'), + (this.specPath = (s) => + isJSONReferenceLikeElement(s) + ? ['document', 'objects', 'JSONReference'] + : ['document', 'objects', 'JSONSchema'])); + } + } + const Kd = PropertiesVisitor; + class PatternPropertiesVisitor extends Mixin(Wd, Nd, Ed) { + constructor(s) { + (super(s), + (this.element = new Cu.Sh()), + this.element.classes.push('json-schema-patternProperties'), + (this.specPath = (s) => + isJSONReferenceLikeElement(s) + ? ['document', 'objects', 'JSONReference'] + : ['document', 'objects', 'JSONSchema'])); + } + } + const Hd = PatternPropertiesVisitor; + class DependenciesVisitor extends Mixin(Wd, Nd, Ed) { + constructor(s) { + (super(s), + (this.element = new Cu.Sh()), + this.element.classes.push('json-schema-dependencies'), + (this.specPath = (s) => + isJSONReferenceLikeElement(s) + ? ['document', 'objects', 'JSONReference'] + : ['document', 'objects', 'JSONSchema'])); + } + } + const Jd = DependenciesVisitor; + const Gd = class EnumVisitor extends Ed { + ArrayElement(s) { + const o = this.enter(s); + return (this.element.classes.push('json-schema-enum'), o); + } + }; + const Yd = class TypeVisitor extends Ed { + StringElement(s) { + const o = this.enter(s); + return (this.element.classes.push('json-schema-type'), o); + } + ArrayElement(s) { + const o = this.enter(s); + return (this.element.classes.push('json-schema-type'), o); + } + }; + class AllOfVisitor extends Mixin(Id, Nd, Ed) { + constructor(s) { + (super(s), + (this.element = new Cu.wE()), + this.element.classes.push('json-schema-allOf')); + } + ArrayElement(s) { + return ( + s.forEach((s) => { + const o = isJSONReferenceLikeElement(s) + ? ['document', 'objects', 'JSONReference'] + : ['document', 'objects', 'JSONSchema'], + i = this.toRefractedElement(o, s); + this.element.push(i); + }), + this.copyMetaAndAttributes(s, this.element), + Ju + ); + } + } + const Xd = AllOfVisitor; + class AnyOfVisitor extends Mixin(Id, Nd, Ed) { + constructor(s) { + (super(s), + (this.element = new Cu.wE()), + this.element.classes.push('json-schema-anyOf')); + } + ArrayElement(s) { + return ( + s.forEach((s) => { + const o = isJSONReferenceLikeElement(s) + ? ['document', 'objects', 'JSONReference'] + : ['document', 'objects', 'JSONSchema'], + i = this.toRefractedElement(o, s); + this.element.push(i); + }), + this.copyMetaAndAttributes(s, this.element), + Ju + ); + } + } + const Zd = AnyOfVisitor; + class OneOfVisitor extends Mixin(Id, Nd, Ed) { + constructor(s) { + (super(s), + (this.element = new Cu.wE()), + this.element.classes.push('json-schema-oneOf')); + } + ArrayElement(s) { + return ( + s.forEach((s) => { + const o = isJSONReferenceLikeElement(s) + ? ['document', 'objects', 'JSONReference'] + : ['document', 'objects', 'JSONSchema'], + i = this.toRefractedElement(o, s); + this.element.push(i); + }), + this.copyMetaAndAttributes(s, this.element), + Ju + ); + } + } + const Qd = OneOfVisitor; + class DefinitionsVisitor extends Mixin(Wd, Nd, Ed) { + constructor(s) { + (super(s), + (this.element = new Cu.Sh()), + this.element.classes.push('json-schema-definitions'), + (this.specPath = (s) => + isJSONReferenceLikeElement(s) + ? ['document', 'objects', 'JSONReference'] + : ['document', 'objects', 'JSONSchema'])); + } + } + const ef = DefinitionsVisitor; + class LinksVisitor extends Mixin(Id, Nd, Ed) { + constructor(s) { + (super(s), + (this.element = new Cu.wE()), + this.element.classes.push('json-schema-links')); + } + ArrayElement(s) { + return ( + s.forEach((s) => { + const o = this.toRefractedElement(['document', 'objects', 'LinkDescription'], s); + this.element.push(o); + }), + this.copyMetaAndAttributes(s, this.element), + Ju + ); + } + } + const rf = LinksVisitor; + class JSONReferenceVisitor extends Mixin(Md, Ed) { + constructor(s) { + (super(s), + (this.element = new Gh()), + (this.specPath = Tl(['document', 'objects', 'JSONReference']))); + } + ObjectElement(s) { + const o = Md.prototype.ObjectElement.call(this, s); + return (Ru(this.element.$ref) && this.element.classes.push('reference-element'), o); + } + } + const of = JSONReferenceVisitor; + const af = class $RefVisitor extends Ed { + StringElement(s) { + const o = this.enter(s); + return (this.element.classes.push('reference-value'), o); + } + }; + const lf = _curry3(function ifElse(s, o, i) { + return za(Math.max(s.length, o.length, i.length), function _ifElse() { + return s.apply(this, arguments) ? o.apply(this, arguments) : i.apply(this, arguments); + }); + }); + const cf = _curry1(function comparator(s) { + return function (o, i) { + return s(o, i) ? -1 : s(i, o) ? 1 : 0; + }; + }); + var uf = _curry2(function sort(s, o) { + return Array.prototype.slice.call(o, 0).sort(s); + }); + const hf = uf; + var df = _curry1(function (s) { + return _nth(0, s); + }); + const mf = df; + const gf = _curry1(_reduced); + const yf = Ml(ld); + const bf = gu(yp, Bd); + function _toConsumableArray(s) { + return ( + (function _arrayWithoutHoles(s) { + if (Array.isArray(s)) return _arrayLikeToArray(s); + })(s) || + (function _iterableToArray(s) { + if ( + ('undefined' != typeof Symbol && null != s[Symbol.iterator]) || + null != s['@@iterator'] + ) + return Array.from(s); + })(s) || + (function _unsupportedIterableToArray(s, o) { + if (s) { + if ('string' == typeof s) return _arrayLikeToArray(s, o); + var i = {}.toString.call(s).slice(8, -1); + return ( + 'Object' === i && s.constructor && (i = s.constructor.name), + 'Map' === i || 'Set' === i + ? Array.from(s) + : 'Arguments' === i || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i) + ? _arrayLikeToArray(s, o) + : void 0 + ); + } + })(s) || + (function _nonIterableSpread() { + throw new TypeError( + 'Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' + ); + })() + ); + } + function _arrayLikeToArray(s, o) { + (null == o || o > s.length) && (o = s.length); + for (var i = 0, u = Array(o); i < o; i++) u[i] = s[i]; + return u; + } + var _f = pipe( + hf( + cf(function (s, o) { + return s.length > o.length; + }) + ), + mf, + Da('length') + ), + Sf = Ja(function (s, o, i) { + var u = i.apply(void 0, _toConsumableArray(s)); + return yf(u) ? gf(u) : o; + }); + const xf = lf( + bf, + function dispatchImpl(s) { + var o = _f(s); + return za(o, function () { + for (var o = arguments.length, i = new Array(o), u = 0; u < o; u++) + i[u] = arguments[u]; + return Ca(Sf(i), void 0, s); + }); + }, + Nl + ); + const kf = class AlternatingVisitor extends Id { + alternator; + constructor({ alternator: s, ...o }) { + (super({ ...o }), (this.alternator = s)); + } + enter(s) { + const o = this.alternator.map(({ predicate: s, specPath: o }) => lf(s, Tl(o), Nl)), + i = xf(o)(s); + return ((this.element = this.toRefractedElement(i, s)), Ju); + } + }; + const Cf = class SchemaOrReferenceVisitor extends kf { + constructor(s) { + (super(s), + (this.alternator = [ + { + predicate: isJSONReferenceLikeElement, + specPath: ['document', 'objects', 'JSONReference'] + }, + { predicate: es_T, specPath: ['document', 'objects', 'JSONSchema'] } + ])); + } + }; + class MediaVisitor extends Mixin(Md, Ed) { + constructor(s) { + (super(s), + (this.element = new Qh()), + (this.specPath = Tl(['document', 'objects', 'Media']))); + } + } + const Of = MediaVisitor; + class LinkDescriptionVisitor extends Mixin(Md, Ed) { + constructor(s) { + (super(s), + (this.element = new td()), + (this.specPath = Tl(['document', 'objects', 'LinkDescription']))); + } + } + const jf = { + visitors: { + value: Ed, + JSONSchemaOrJSONReferenceVisitor: Cf, + document: { + objects: { + JSONSchema: { + $visitor: Td, + fixedFields: { + id: { $ref: '#/visitors/value' }, + $schema: { $ref: '#/visitors/value' }, + multipleOf: { $ref: '#/visitors/value' }, + maximum: { $ref: '#/visitors/value' }, + exclusiveMaximum: { $ref: '#/visitors/value' }, + minimum: { $ref: '#/visitors/value' }, + exclusiveMinimum: { $ref: '#/visitors/value' }, + maxLength: { $ref: '#/visitors/value' }, + minLength: { $ref: '#/visitors/value' }, + pattern: { $ref: '#/visitors/value' }, + additionalItems: Cf, + items: Rd, + maxItems: { $ref: '#/visitors/value' }, + minItems: { $ref: '#/visitors/value' }, + uniqueItems: { $ref: '#/visitors/value' }, + maxProperties: { $ref: '#/visitors/value' }, + minProperties: { $ref: '#/visitors/value' }, + required: Dd, + properties: Kd, + additionalProperties: Cf, + patternProperties: Hd, + dependencies: Jd, + enum: Gd, + type: Yd, + allOf: Xd, + anyOf: Zd, + oneOf: Qd, + not: Cf, + definitions: ef, + title: { $ref: '#/visitors/value' }, + description: { $ref: '#/visitors/value' }, + default: { $ref: '#/visitors/value' }, + format: { $ref: '#/visitors/value' }, + base: { $ref: '#/visitors/value' }, + links: rf, + media: { $ref: '#/visitors/document/objects/Media' }, + readOnly: { $ref: '#/visitors/value' } + } + }, + JSONReference: { $visitor: of, fixedFields: { $ref: af } }, + Media: { + $visitor: Of, + fixedFields: { + binaryEncoding: { $ref: '#/visitors/value' }, + type: { $ref: '#/visitors/value' } + } + }, + LinkDescription: { + $visitor: LinkDescriptionVisitor, + fixedFields: { + href: { $ref: '#/visitors/value' }, + rel: { $ref: '#/visitors/value' }, + title: { $ref: '#/visitors/value' }, + targetSchema: Cf, + mediaType: { $ref: '#/visitors/value' }, + method: { $ref: '#/visitors/value' }, + encType: { $ref: '#/visitors/value' }, + schema: Cf + } + } + } + } + } + }, + traversal_visitor_getNodeType = (s) => { + if (Nu(s)) return `${s.element.charAt(0).toUpperCase() + s.element.slice(1)}Element`; + }, + Pf = { + JSONSchemaDraft4Element: ['content'], + JSONReferenceElement: ['content'], + MediaElement: ['content'], + LinkDescriptionElement: ['content'], + ...Qu + }, + Tf = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Jh || (s(u) && o('JSONSchemaDraft4', u) && i('object', u)) + ), + Nf = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Gh || (s(u) && o('JSONReference', u) && i('object', u)) + ), + Rf = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Qh || (s(u) && o('media', u) && i('object', u)) + ), + Df = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof td || (s(u) && o('linkDescription', u) && i('object', u)) + ), + Ff = { + namespace: (s) => { + const { base: o } = s; + return ( + o.register('jSONSchemaDraft4', Jh), + o.register('jSONReference', Gh), + o.register('media', Qh), + o.register('linkDescription', td), + o + ); + } + }, + Vf = Ff, + refractor_toolbox = () => { + const s = createNamespace(Vf); + return { predicates: { ...ce, isStringElement: Ru }, namespace: s }; + }, + refractor_refract = ( + s, + { + specPath: o = ['visitors', 'document', 'objects', 'JSONSchema', '$visitor'], + plugins: i = [], + specificationObj: u = jf + } = {} + ) => { + const _ = (0, Cu.e)(s), + w = dereference(u), + x = new (cp(o, w))({ specObj: w }); + return ( + visitor_visit(_, x), + dispatchPluginsSync(x.element, i, { + toolboxCreator: refractor_toolbox, + visitorOptions: { keyMap: Pf, nodeTypeGetter: traversal_visitor_getNodeType } + }) + ); + }, + refractor_createRefractor = + (s) => + (o, i = {}) => + refractor_refract(o, { specPath: s, ...i }); + ((Jh.refract = refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'JSONSchema', + '$visitor' + ])), + (Gh.refract = refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'JSONReference', + '$visitor' + ])), + (Qh.refract = refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Media', + '$visitor' + ])), + (td.refract = refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'LinkDescription', + '$visitor' + ]))); + const Wf = class Schema_Schema extends Jh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'schema'), this.classes.push('json-schema-draft-4')); + } + get idProp() { + throw new Hh('idProp getter in Schema class is not not supported.'); + } + set idProp(s) { + throw new Hh('idProp setter in Schema class is not not supported.'); + } + get $schema() { + throw new Hh('$schema getter in Schema class is not not supported.'); + } + set $schema(s) { + throw new Hh('$schema setter in Schema class is not not supported.'); + } + get additionalItems() { + return this.get('additionalItems'); + } + set additionalItems(s) { + this.set('additionalItems', s); + } + get items() { + return this.get('items'); + } + set items(s) { + this.set('items', s); + } + get additionalProperties() { + return this.get('additionalProperties'); + } + set additionalProperties(s) { + this.set('additionalProperties', s); + } + get patternProperties() { + throw new Hh('patternProperties getter in Schema class is not not supported.'); + } + set patternProperties(s) { + throw new Hh('patternProperties setter in Schema class is not not supported.'); + } + get dependencies() { + throw new Hh('dependencies getter in Schema class is not not supported.'); + } + set dependencies(s) { + throw new Hh('dependencies setter in Schema class is not not supported.'); + } + get type() { + return this.get('type'); + } + set type(s) { + this.set('type', s); + } + get not() { + return this.get('not'); + } + set not(s) { + this.set('not', s); + } + get definitions() { + throw new Hh('definitions getter in Schema class is not not supported.'); + } + set definitions(s) { + throw new Hh('definitions setter in Schema class is not not supported.'); + } + get base() { + throw new Hh('base getter in Schema class is not not supported.'); + } + set base(s) { + throw new Hh('base setter in Schema class is not not supported.'); + } + get links() { + throw new Hh('links getter in Schema class is not not supported.'); + } + set links(s) { + throw new Hh('links setter in Schema class is not not supported.'); + } + get media() { + throw new Hh('media getter in Schema class is not not supported.'); + } + set media(s) { + throw new Hh('media setter in Schema class is not not supported.'); + } + get nullable() { + return this.get('nullable'); + } + set nullable(s) { + this.set('nullable', s); + } + get discriminator() { + return this.get('discriminator'); + } + set discriminator(s) { + this.set('discriminator', s); + } + get writeOnly() { + return this.get('writeOnly'); + } + set writeOnly(s) { + this.set('writeOnly', s); + } + get xml() { + return this.get('xml'); + } + set xml(s) { + this.set('xml', s); + } + get externalDocs() { + return this.get('externalDocs'); + } + set externalDocs(s) { + this.set('externalDocs', s); + } + get example() { + return this.get('example'); + } + set example(s) { + this.set('example', s); + } + get deprecated() { + return this.get('deprecated'); + } + set deprecated(s) { + this.set('deprecated', s); + } + }; + class SecurityRequirement extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'securityRequirement')); + } + } + const Hf = SecurityRequirement; + class SecurityScheme extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'securityScheme')); + } + get type() { + return this.get('type'); + } + set type(s) { + this.set('type', s); + } + get description() { + return this.get('description'); + } + set description(s) { + this.set('description', s); + } + get name() { + return this.get('name'); + } + set name(s) { + this.set('name', s); + } + get in() { + return this.get('in'); + } + set in(s) { + this.set('in', s); + } + get scheme() { + return this.get('scheme'); + } + set scheme(s) { + this.set('scheme', s); + } + get bearerFormat() { + return this.get('bearerFormat'); + } + set bearerFormat(s) { + this.set('bearerFormat', s); + } + get flows() { + return this.get('flows'); + } + set flows(s) { + this.set('flows', s); + } + get openIdConnectUrl() { + return this.get('openIdConnectUrl'); + } + set openIdConnectUrl(s) { + this.set('openIdConnectUrl', s); + } + } + const Jf = SecurityScheme; + class Server extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'server')); + } + get url() { + return this.get('url'); + } + set url(s) { + this.set('url', s); + } + get description() { + return this.get('description'); + } + set description(s) { + this.set('description', s); + } + get variables() { + return this.get('variables'); + } + set variables(s) { + this.set('variables', s); + } + } + const Gf = Server; + class ServerVariable extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'serverVariable')); + } + get enum() { + return this.get('enum'); + } + set enum(s) { + this.set('enum', s); + } + get default() { + return this.get('default'); + } + set default(s) { + this.set('default', s); + } + get description() { + return this.get('description'); + } + set description(s) { + this.set('description', s); + } + } + const Xf = ServerVariable; + class Tag extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'tag')); + } + get name() { + return this.get('name'); + } + set name(s) { + this.set('name', s); + } + get description() { + return this.get('description'); + } + set description(s) { + this.set('description', s); + } + get externalDocs() { + return this.get('externalDocs'); + } + set externalDocs(s) { + this.set('externalDocs', s); + } + } + const Qf = Tag; + class Xml extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'xml')); + } + get name() { + return this.get('name'); + } + set name(s) { + this.set('name', s); + } + get namespace() { + return this.get('namespace'); + } + set namespace(s) { + this.set('namespace', s); + } + get prefix() { + return this.get('prefix'); + } + set prefix(s) { + this.set('prefix', s); + } + get attribute() { + return this.get('attribute'); + } + set attribute(s) { + this.set('attribute', s); + } + get wrapped() { + return this.get('wrapped'); + } + set wrapped(s) { + this.set('wrapped', s); + } + } + const em = Xml; + const tm = class visitors_Visitor_Visitor { + element; + constructor(s = {}) { + Object.assign(this, s); + } + copyMetaAndAttributes(s, o) { + ((s.meta.length > 0 || o.meta.length > 0) && + ((o.meta = deepmerge(o.meta, s.meta)), + hasElementSourceMap(s) && o.meta.set('sourceMap', s.meta.get('sourceMap'))), + (s.attributes.length > 0 || s.meta.length > 0) && + (o.attributes = deepmerge(o.attributes, s.attributes))); + } + }; + const rm = class FallbackVisitor_FallbackVisitor extends tm { + enter(s) { + return ((this.element = cloneDeep(s)), Ju); + } + }; + const nm = class SpecificationVisitor_SpecificationVisitor extends tm { + specObj; + passingOptionsNames = ['specObj', 'openApiGenericElement', 'openApiSemanticElement']; + openApiGenericElement; + openApiSemanticElement; + constructor({ + specObj: s, + passingOptionsNames: o, + openApiGenericElement: i, + openApiSemanticElement: u, + ..._ + }) { + (super({ ..._ }), + (this.specObj = s), + (this.openApiGenericElement = i), + (this.openApiSemanticElement = u), + Array.isArray(o) && (this.passingOptionsNames = o)); + } + retrievePassingOptions() { + return Ad(this.passingOptionsNames, this); + } + retrieveFixedFields(s) { + const o = cp(['visitors', ...s, 'fixedFields'], this.specObj); + return 'object' == typeof o && null !== o ? Object.keys(o) : []; + } + retrieveVisitor(s) { + return Xo(Wl, ['visitors', ...s], this.specObj) + ? cp(['visitors', ...s], this.specObj) + : cp(['visitors', ...s, '$visitor'], this.specObj); + } + retrieveVisitorInstance(s, o = {}) { + const i = this.retrievePassingOptions(); + return new (this.retrieveVisitor(s))({ ...i, ...o }); + } + toRefractedElement(s, o, i = {}) { + const u = this.retrieveVisitorInstance(s, i); + return u instanceof rm && (null == u ? void 0 : u.constructor) === rm + ? cloneDeep(o) + : (visitor_visit(o, u, i), u.element); + } + }, + isReferenceLikeElement = (s) => Fu(s) && s.hasKey('$ref'), + sm = Fu, + om = Fu, + isOpenApiExtension = (s) => Ru(s.key) && Fp('x-', serializers_value(s.key)); + const im = class FixedFieldsVisitor_FixedFieldsVisitor extends nm { + specPath; + ignoredFields; + canSupportSpecificationExtensions = !0; + specificationExtensionPredicate = isOpenApiExtension; + constructor({ + specPath: s, + ignoredFields: o, + canSupportSpecificationExtensions: i, + specificationExtensionPredicate: u, + ..._ + }) { + (super({ ..._ }), + (this.specPath = s), + (this.ignoredFields = o || []), + 'boolean' == typeof i && (this.canSupportSpecificationExtensions = i), + 'function' == typeof u && (this.specificationExtensionPredicate = u)); + } + ObjectElement(s) { + const o = this.specPath(s), + i = this.retrieveFixedFields(o); + return ( + s.forEach((s, u, _) => { + if ( + Ru(u) && + i.includes(serializers_value(u)) && + !this.ignoredFields.includes(serializers_value(u)) + ) { + const i = this.toRefractedElement([...o, 'fixedFields', serializers_value(u)], s), + w = new Cu.Pr(cloneDeep(u), i); + (this.copyMetaAndAttributes(_, w), + w.classes.push('fixed-field'), + this.element.content.push(w)); + } else if ( + this.canSupportSpecificationExtensions && + this.specificationExtensionPredicate(_) + ) { + const s = this.toRefractedElement(['document', 'extension'], _); + this.element.content.push(s); + } else + this.ignoredFields.includes(serializers_value(u)) || + this.element.content.push(cloneDeep(_)); + }), + this.copyMetaAndAttributes(s, this.element), + Ju + ); + } + }; + class OpenApi3_0Visitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new Oh()), + (this.specPath = Tl(['document', 'objects', 'OpenApi'])), + (this.canSupportSpecificationExtensions = !0)); + } + ObjectElement(s) { + return im.prototype.ObjectElement.call(this, s); + } + } + const am = OpenApi3_0Visitor; + class OpenapiVisitor extends Mixin(nm, rm) { + StringElement(s) { + const o = new wh(serializers_value(s)); + return (this.copyMetaAndAttributes(s, o), (this.element = o), Ju); + } + } + const lm = OpenapiVisitor; + const cm = class SpecificationExtensionVisitor extends nm { + MemberElement(s) { + return ( + (this.element = cloneDeep(s)), + this.element.classes.push('specification-extension'), + Ju + ); + } + }; + class InfoVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new rh()), + (this.specPath = Tl(['document', 'objects', 'Info'])), + (this.canSupportSpecificationExtensions = !0)); + } + } + const um = InfoVisitor; + const pm = class VersionVisitor extends rm { + StringElement(s) { + const o = super.enter(s); + return ( + this.element.classes.push('api-version'), + this.element.classes.push('version'), + o + ); + } + }; + class ContactVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new Gp()), + (this.specPath = Tl(['document', 'objects', 'Contact'])), + (this.canSupportSpecificationExtensions = !0)); + } + } + const hm = ContactVisitor; + class LicenseVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new uh()), + (this.specPath = Tl(['document', 'objects', 'License'])), + (this.canSupportSpecificationExtensions = !0)); + } + } + const dm = LicenseVisitor; + class LinkVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new dh()), + (this.specPath = Tl(['document', 'objects', 'Link'])), + (this.canSupportSpecificationExtensions = !0)); + } + ObjectElement(s) { + const o = im.prototype.ObjectElement.call(this, s); + return ( + (Ru(this.element.operationId) || Ru(this.element.operationRef)) && + this.element.classes.push('reference-element'), + o + ); + } + } + const fm = LinkVisitor; + const mm = class OperationRefVisitor extends rm { + StringElement(s) { + const o = super.enter(s); + return (this.element.classes.push('reference-value'), o); + } + }; + const gm = class OperationIdVisitor extends rm { + StringElement(s) { + const o = super.enter(s); + return (this.element.classes.push('reference-value'), o); + } + }; + const ym = class PatternedFieldsVisitor_PatternedFieldsVisitor extends nm { + specPath; + ignoredFields; + fieldPatternPredicate = es_F; + canSupportSpecificationExtensions = !1; + specificationExtensionPredicate = isOpenApiExtension; + constructor({ + specPath: s, + ignoredFields: o, + fieldPatternPredicate: i, + canSupportSpecificationExtensions: u, + specificationExtensionPredicate: _, + ...w + }) { + (super({ ...w }), + (this.specPath = s), + (this.ignoredFields = o || []), + 'function' == typeof i && (this.fieldPatternPredicate = i), + 'boolean' == typeof u && (this.canSupportSpecificationExtensions = u), + 'function' == typeof _ && (this.specificationExtensionPredicate = _)); + } + ObjectElement(s) { + return ( + s.forEach((s, o, i) => { + if ( + this.canSupportSpecificationExtensions && + this.specificationExtensionPredicate(i) + ) { + const s = this.toRefractedElement(['document', 'extension'], i); + this.element.content.push(s); + } else if ( + !this.ignoredFields.includes(serializers_value(o)) && + this.fieldPatternPredicate(serializers_value(o)) + ) { + const u = this.specPath(s), + _ = this.toRefractedElement(u, s), + w = new Cu.Pr(cloneDeep(o), _); + (this.copyMetaAndAttributes(i, w), + w.classes.push('patterned-field'), + this.element.content.push(w)); + } else + this.ignoredFields.includes(serializers_value(o)) || + this.element.content.push(cloneDeep(i)); + }), + this.copyMetaAndAttributes(s, this.element), + Ju + ); + } + }; + const vm = class MapVisitor_MapVisitor extends ym { + constructor(s) { + (super(s), (this.fieldPatternPredicate = Vd)); + } + }; + class LinkParameters extends Cu.Sh { + static primaryClass = 'link-parameters'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(LinkParameters.primaryClass)); + } + } + const bm = LinkParameters; + class ParametersVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), (this.element = new bm()), (this.specPath = Tl(['value']))); + } + } + const _m = ParametersVisitor; + class ServerVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new Gf()), + (this.specPath = Tl(['document', 'objects', 'Server'])), + (this.canSupportSpecificationExtensions = !0)); + } + } + const Em = ServerVisitor; + const wm = class UrlVisitor extends rm { + StringElement(s) { + const o = super.enter(s); + return (this.element.classes.push('server-url'), o); + } + }; + class Servers extends Cu.wE { + static primaryClass = 'servers'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(Servers.primaryClass)); + } + } + const Sm = Servers; + class ServersVisitor extends Mixin(nm, rm) { + constructor(s) { + (super(s), (this.element = new Sm())); + } + ArrayElement(s) { + return ( + s.forEach((s) => { + const o = sm(s) ? ['document', 'objects', 'Server'] : ['value'], + i = this.toRefractedElement(o, s); + this.element.push(i); + }), + this.copyMetaAndAttributes(s, this.element), + Ju + ); + } + } + const xm = ServersVisitor; + class ServerVariableVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new Xf()), + (this.specPath = Tl(['document', 'objects', 'ServerVariable'])), + (this.canSupportSpecificationExtensions = !0)); + } + } + const km = ServerVariableVisitor; + class ServerVariables extends Cu.Sh { + static primaryClass = 'server-variables'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(ServerVariables.primaryClass)); + } + } + const Cm = ServerVariables; + class VariablesVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new Cm()), + (this.specPath = Tl(['document', 'objects', 'ServerVariable']))); + } + } + const Om = VariablesVisitor; + class MediaTypeVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new fh()), + (this.specPath = Tl(['document', 'objects', 'MediaType'])), + (this.canSupportSpecificationExtensions = !0)); + } + } + const Am = MediaTypeVisitor; + const jm = class AlternatingVisitor_AlternatingVisitor extends nm { + alternator; + constructor({ alternator: s, ...o }) { + (super({ ...o }), (this.alternator = s || [])); + } + enter(s) { + const o = this.alternator.map(({ predicate: s, specPath: o }) => lf(s, Tl(o), Nl)), + i = xf(o)(s); + return ((this.element = this.toRefractedElement(i, s)), Ju); + } + }, + Im = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Hp || (s(u) && o('callback', u) && i('object', u)) + ), + Pm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Jp || (s(u) && o('components', u) && i('object', u)) + ), + Mm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Gp || (s(u) && o('contact', u) && i('object', u)) + ), + Tm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Zp || (s(u) && o('example', u) && i('object', u)) + ), + Nm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Qp || (s(u) && o('externalDocumentation', u) && i('object', u)) + ), + Rm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof th || (s(u) && o('header', u) && i('object', u)) + ), + Dm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof rh || (s(u) && o('info', u) && i('object', u)) + ), + Lm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof uh || (s(u) && o('license', u) && i('object', u)) + ), + Bm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof dh || (s(u) && o('link', u) && i('object', u)) + ), + Fm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof wh || (s(u) && o('openapi', u) && i('string', u)) + ), + qm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i, hasClass: u }) => + (_) => + _ instanceof Oh || (s(_) && o('openApi3_0', _) && i('object', _) && u('api', _)) + ), + $m = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof jh || (s(u) && o('operation', u) && i('object', u)) + ), + Vm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Ih || (s(u) && o('parameter', u) && i('object', u)) + ), + Um = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Ph || (s(u) && o('pathItem', u) && i('object', u)) + ), + zm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Rh || (s(u) && o('paths', u) && i('object', u)) + ), + Wm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Dh || (s(u) && o('reference', u) && i('object', u)) + ), + Km = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Lh || (s(u) && o('requestBody', u) && i('object', u)) + ), + Hm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Fh || (s(u) && o('response', u) && i('object', u)) + ), + Jm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Kh || (s(u) && o('responses', u) && i('object', u)) + ), + Gm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Wf || (s(u) && o('schema', u) && i('object', u)) + ), + isBooleanJsonSchemaElement = (s) => Bu(s) && s.classes.includes('boolean-json-schema'), + Ym = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Hf || (s(u) && o('securityRequirement', u) && i('object', u)) + ), + Xm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Jf || (s(u) && o('securityScheme', u) && i('object', u)) + ), + Zm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Gf || (s(u) && o('server', u) && i('object', u)) + ), + Qm = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Xf || (s(u) && o('serverVariable', u) && i('object', u)) + ), + eg = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof fh || (s(u) && o('mediaType', u) && i('object', u)) + ), + rg = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i, hasClass: u }) => + (_) => + _ instanceof Sm || (s(_) && o('array', _) && i('array', _) && u('servers', _)) + ); + class SchemaVisitor extends Mixin(jm, rm) { + constructor(s) { + (super(s), + (this.alternator = [ + { + predicate: isReferenceLikeElement, + specPath: ['document', 'objects', 'Reference'] + }, + { predicate: es_T, specPath: ['document', 'objects', 'Schema'] } + ])); + } + ObjectElement(s) { + const o = jm.prototype.enter.call(this, s); + return ( + Wm(this.element) && this.element.setMetaProperty('referenced-element', 'schema'), + o + ); + } + } + const ng = SchemaVisitor; + class ExamplesVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new Cu.Sh()), + this.element.classes.push('examples'), + (this.specPath = (s) => + isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'Example']), + (this.canSupportSpecificationExtensions = !0)); + } + ObjectElement(s) { + const o = vm.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'example'); + }), + o + ); + } + } + const sg = ExamplesVisitor; + class MediaTypeExamples extends Cu.Sh { + static primaryClass = 'media-type-examples'; + constructor(s, o, i) { + (super(s, o, i), + this.classes.push(MediaTypeExamples.primaryClass), + this.classes.push('examples')); + } + } + const og = MediaTypeExamples; + const lg = class ExamplesVisitor_ExamplesVisitor extends sg { + constructor(s) { + (super(s), (this.element = new og())); + } + }; + class MediaTypeEncoding extends Cu.Sh { + static primaryClass = 'media-type-encoding'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(MediaTypeEncoding.primaryClass)); + } + } + const pg = MediaTypeEncoding; + class EncodingVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new pg()), + (this.specPath = Tl(['document', 'objects', 'Encoding']))); + } + } + const fg = EncodingVisitor; + class SecurityRequirementVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), (this.element = new Hf()), (this.specPath = Tl(['value']))); + } + } + const mg = SecurityRequirementVisitor; + class Security extends Cu.wE { + static primaryClass = 'security'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(Security.primaryClass)); + } + } + const gg = Security; + class SecurityVisitor extends Mixin(nm, rm) { + constructor(s) { + (super(s), (this.element = new gg())); + } + ArrayElement(s) { + return ( + s.forEach((s) => { + if (Fu(s)) { + const o = this.toRefractedElement( + ['document', 'objects', 'SecurityRequirement'], + s + ); + this.element.push(o); + } else this.element.push(cloneDeep(s)); + }), + this.copyMetaAndAttributes(s, this.element), + Ju + ); + } + } + const yg = SecurityVisitor; + class ComponentsVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new Jp()), + (this.specPath = Tl(['document', 'objects', 'Components'])), + (this.canSupportSpecificationExtensions = !0)); + } + } + const _g = ComponentsVisitor; + class TagVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new Qf()), + (this.specPath = Tl(['document', 'objects', 'Tag'])), + (this.canSupportSpecificationExtensions = !0)); + } + } + const xg = TagVisitor; + class ReferenceVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new Dh()), + (this.specPath = Tl(['document', 'objects', 'Reference'])), + (this.canSupportSpecificationExtensions = !1)); + } + ObjectElement(s) { + const o = im.prototype.ObjectElement.call(this, s); + return (Ru(this.element.$ref) && this.element.classes.push('reference-element'), o); + } + } + const kg = ReferenceVisitor; + const qg = class $RefVisitor_$RefVisitor extends rm { + StringElement(s) { + const o = super.enter(s); + return (this.element.classes.push('reference-value'), o); + } + }; + class ParameterVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new Ih()), + (this.specPath = Tl(['document', 'objects', 'Parameter'])), + (this.canSupportSpecificationExtensions = !0)); + } + ObjectElement(s) { + const o = im.prototype.ObjectElement.call(this, s); + return ( + Fu(this.element.contentProp) && + this.element.contentProp.filter(eg).forEach((s, o) => { + s.setMetaProperty('media-type', serializers_value(o)); + }), + o + ); + } + } + const Vg = ParameterVisitor; + class SchemaVisitor_SchemaVisitor extends Mixin(jm, rm) { + constructor(s) { + (super(s), + (this.alternator = [ + { + predicate: isReferenceLikeElement, + specPath: ['document', 'objects', 'Reference'] + }, + { predicate: es_T, specPath: ['document', 'objects', 'Schema'] } + ])); + } + ObjectElement(s) { + const o = jm.prototype.enter.call(this, s); + return ( + Wm(this.element) && this.element.setMetaProperty('referenced-element', 'schema'), + o + ); + } + } + const Ug = SchemaVisitor_SchemaVisitor; + class HeaderVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new th()), + (this.specPath = Tl(['document', 'objects', 'Header'])), + (this.canSupportSpecificationExtensions = !0)); + } + } + const zg = HeaderVisitor; + class header_SchemaVisitor_SchemaVisitor extends Mixin(jm, rm) { + constructor(s) { + (super(s), + (this.alternator = [ + { + predicate: isReferenceLikeElement, + specPath: ['document', 'objects', 'Reference'] + }, + { predicate: es_T, specPath: ['document', 'objects', 'Schema'] } + ])); + } + ObjectElement(s) { + const o = jm.prototype.enter.call(this, s); + return ( + Wm(this.element) && this.element.setMetaProperty('referenced-element', 'schema'), + o + ); + } + } + const Wg = header_SchemaVisitor_SchemaVisitor; + class HeaderExamples extends Cu.Sh { + static primaryClass = 'header-examples'; + constructor(s, o, i) { + (super(s, o, i), + this.classes.push(HeaderExamples.primaryClass), + this.classes.push('examples')); + } + } + const Kg = HeaderExamples; + const Yg = class header_ExamplesVisitor_ExamplesVisitor extends sg { + constructor(s) { + (super(s), (this.element = new Kg())); + } + }; + class ContentVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new Cu.Sh()), + this.element.classes.push('content'), + (this.specPath = Tl(['document', 'objects', 'MediaType']))); + } + } + const Xg = ContentVisitor; + class HeaderContent extends Cu.Sh { + static primaryClass = 'header-content'; + constructor(s, o, i) { + (super(s, o, i), + this.classes.push(HeaderContent.primaryClass), + this.classes.push('content')); + } + } + const Zg = HeaderContent; + const ey = class ContentVisitor_ContentVisitor extends Xg { + constructor(s) { + (super(s), (this.element = new Zg())); + } + }; + class schema_SchemaVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new Wf()), + (this.specPath = Tl(['document', 'objects', 'Schema'])), + (this.canSupportSpecificationExtensions = !0)); + } + } + const ty = schema_SchemaVisitor, + { allOf: ry } = jf.visitors.document.objects.JSONSchema.fixedFields; + const ny = class AllOfVisitor_AllOfVisitor extends ry { + ArrayElement(s) { + const o = ry.prototype.ArrayElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'schema'); + }), + o + ); + } + }, + { anyOf: sy } = jf.visitors.document.objects.JSONSchema.fixedFields; + const oy = class AnyOfVisitor_AnyOfVisitor extends sy { + ArrayElement(s) { + const o = sy.prototype.ArrayElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'schema'); + }), + o + ); + } + }, + { oneOf: iy } = jf.visitors.document.objects.JSONSchema.fixedFields; + const ay = class OneOfVisitor_OneOfVisitor extends iy { + ArrayElement(s) { + const o = iy.prototype.ArrayElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'schema'); + }), + o + ); + } + }, + { items: ly } = jf.visitors.document.objects.JSONSchema.fixedFields; + const cy = class ItemsVisitor_ItemsVisitor extends ly { + ObjectElement(s) { + const o = ly.prototype.ObjectElement.call(this, s); + return ( + Wm(this.element) && this.element.setMetaProperty('referenced-element', 'schema'), + o + ); + } + ArrayElement(s) { + return this.enter(s); + } + }, + { properties: uy } = jf.visitors.document.objects.JSONSchema.fixedFields; + const py = class PropertiesVisitor_PropertiesVisitor extends uy { + ObjectElement(s) { + const o = uy.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'schema'); + }), + o + ); + } + }, + { type: hy } = jf.visitors.document.objects.JSONSchema.fixedFields; + const dy = class TypeVisitor_TypeVisitor extends hy { + ArrayElement(s) { + return this.enter(s); + } + }, + { JSONSchemaOrJSONReferenceVisitor: fy } = jf.visitors; + const my = class SchemaOrReferenceVisitor_SchemaOrReferenceVisitor extends fy { + ObjectElement(s) { + const o = fy.prototype.enter.call(this, s); + return ( + Wm(this.element) && this.element.setMetaProperty('referenced-element', 'schema'), + o + ); + } + }; + class DiscriminatorVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new Yp()), + (this.specPath = Tl(['document', 'objects', 'Discriminator'])), + (this.canSupportSpecificationExtensions = !1)); + } + } + const gy = DiscriminatorVisitor; + class DiscriminatorMapping extends Cu.Sh { + static primaryClass = 'discriminator-mapping'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(DiscriminatorMapping.primaryClass)); + } + } + const yy = DiscriminatorMapping; + class MappingVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), (this.element = new yy()), (this.specPath = Tl(['value']))); + } + } + const vy = MappingVisitor; + class XmlVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new em()), + (this.specPath = Tl(['document', 'objects', 'XML'])), + (this.canSupportSpecificationExtensions = !0)); + } + } + const by = XmlVisitor; + class ParameterExamples extends Cu.Sh { + static primaryClass = 'parameter-examples'; + constructor(s, o, i) { + (super(s, o, i), + this.classes.push(ParameterExamples.primaryClass), + this.classes.push('examples')); + } + } + const _y = ParameterExamples; + const Ey = class parameter_ExamplesVisitor_ExamplesVisitor extends sg { + constructor(s) { + (super(s), (this.element = new _y())); + } + }; + class ParameterContent extends Cu.Sh { + static primaryClass = 'parameter-content'; + constructor(s, o, i) { + (super(s, o, i), + this.classes.push(ParameterContent.primaryClass), + this.classes.push('content')); + } + } + const wy = ParameterContent; + const Sy = class parameter_ContentVisitor_ContentVisitor extends Xg { + constructor(s) { + (super(s), (this.element = new wy())); + } + }; + class ComponentsSchemas extends Cu.Sh { + static primaryClass = 'components-schemas'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(ComponentsSchemas.primaryClass)); + } + } + const xy = ComponentsSchemas; + class SchemasVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new xy()), + (this.specPath = (s) => + isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'Schema'])); + } + ObjectElement(s) { + const o = vm.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'schema'); + }), + o + ); + } + } + const ky = SchemasVisitor; + class ComponentsResponses extends Cu.Sh { + static primaryClass = 'components-responses'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(ComponentsResponses.primaryClass)); + } + } + const Cy = ComponentsResponses; + class ResponsesVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new Cy()), + (this.specPath = (s) => + isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'Response'])); + } + ObjectElement(s) { + const o = vm.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'response'); + }), + this.element.filter(Hm).forEach((s, o) => { + s.setMetaProperty('http-status-code', serializers_value(o)); + }), + o + ); + } + } + const Oy = ResponsesVisitor; + class ComponentsParameters extends Cu.Sh { + static primaryClass = 'components-parameters'; + constructor(s, o, i) { + (super(s, o, i), + this.classes.push(ComponentsParameters.primaryClass), + this.classes.push('parameters')); + } + } + const Ay = ComponentsParameters; + class ParametersVisitor_ParametersVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new Ay()), + (this.specPath = (s) => + isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'Parameter'])); + } + ObjectElement(s) { + const o = vm.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'parameter'); + }), + o + ); + } + } + const jy = ParametersVisitor_ParametersVisitor; + class ComponentsExamples extends Cu.Sh { + static primaryClass = 'components-examples'; + constructor(s, o, i) { + (super(s, o, i), + this.classes.push(ComponentsExamples.primaryClass), + this.classes.push('examples')); + } + } + const Iy = ComponentsExamples; + class components_ExamplesVisitor_ExamplesVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new Iy()), + (this.specPath = (s) => + isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'Example'])); + } + ObjectElement(s) { + const o = vm.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'example'); + }), + o + ); + } + } + const Py = components_ExamplesVisitor_ExamplesVisitor; + class ComponentsRequestBodies extends Cu.Sh { + static primaryClass = 'components-request-bodies'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(ComponentsRequestBodies.primaryClass)); + } + } + const My = ComponentsRequestBodies; + class RequestBodiesVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new My()), + (this.specPath = (s) => + isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'RequestBody'])); + } + ObjectElement(s) { + const o = vm.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'requestBody'); + }), + o + ); + } + } + const Ty = RequestBodiesVisitor; + class ComponentsHeaders extends Cu.Sh { + static primaryClass = 'components-headers'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(ComponentsHeaders.primaryClass)); + } + } + const Ny = ComponentsHeaders; + class HeadersVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new Ny()), + (this.specPath = (s) => + isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'Header'])); + } + ObjectElement(s) { + const o = vm.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'header'); + }), + this.element.filter(Rm).forEach((s, o) => { + s.setMetaProperty('header-name', serializers_value(o)); + }), + o + ); + } + } + const Ry = HeadersVisitor; + class ComponentsSecuritySchemes extends Cu.Sh { + static primaryClass = 'components-security-schemes'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(ComponentsSecuritySchemes.primaryClass)); + } + } + const Dy = ComponentsSecuritySchemes; + class SecuritySchemesVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new Dy()), + (this.specPath = (s) => + isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'SecurityScheme'])); + } + ObjectElement(s) { + const o = vm.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'securityScheme'); + }), + o + ); + } + } + const Ly = SecuritySchemesVisitor; + class ComponentsLinks extends Cu.Sh { + static primaryClass = 'components-links'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(ComponentsLinks.primaryClass)); + } + } + const By = ComponentsLinks; + class LinksVisitor_LinksVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new By()), + (this.specPath = (s) => + isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'Link'])); + } + ObjectElement(s) { + const o = vm.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'link'); + }), + o + ); + } + } + const Fy = LinksVisitor_LinksVisitor; + class ComponentsCallbacks extends Cu.Sh { + static primaryClass = 'components-callbacks'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(ComponentsCallbacks.primaryClass)); + } + } + const qy = ComponentsCallbacks; + class CallbacksVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new qy()), + (this.specPath = (s) => + isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'Callback'])); + } + ObjectElement(s) { + const o = vm.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'callback'); + }), + o + ); + } + } + const $y = CallbacksVisitor; + class ExampleVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new Zp()), + (this.specPath = Tl(['document', 'objects', 'Example'])), + (this.canSupportSpecificationExtensions = !0)); + } + ObjectElement(s) { + const o = im.prototype.ObjectElement.call(this, s); + return ( + Ru(this.element.externalValue) && this.element.classes.push('reference-element'), + o + ); + } + } + const Vy = ExampleVisitor; + const Uy = class ExternalValueVisitor extends rm { + StringElement(s) { + const o = super.enter(s); + return (this.element.classes.push('reference-value'), o); + } + }; + class ExternalDocumentationVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new Qp()), + (this.specPath = Tl(['document', 'objects', 'ExternalDocumentation'])), + (this.canSupportSpecificationExtensions = !0)); + } + } + const zy = ExternalDocumentationVisitor; + class encoding_EncodingVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new Xp()), + (this.specPath = Tl(['document', 'objects', 'Encoding'])), + (this.canSupportSpecificationExtensions = !0)); + } + ObjectElement(s) { + const o = im.prototype.ObjectElement.call(this, s); + return ( + Fu(this.element.headers) && + this.element.headers.filter(Rm).forEach((s, o) => { + s.setMetaProperty('header-name', serializers_value(o)); + }), + o + ); + } + } + const Wy = encoding_EncodingVisitor; + class EncodingHeaders extends Cu.Sh { + static primaryClass = 'encoding-headers'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(EncodingHeaders.primaryClass)); + } + } + const Ky = EncodingHeaders; + class HeadersVisitor_HeadersVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new Ky()), + (this.specPath = (s) => + isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'Header'])); + } + ObjectElement(s) { + const o = vm.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'header'); + }), + this.element.forEach((s, o) => { + if (!Rm(s)) return; + const i = serializers_value(o); + s.setMetaProperty('headerName', i); + }), + o + ); + } + } + const Hy = HeadersVisitor_HeadersVisitor; + class PathsVisitor extends Mixin(ym, rm) { + constructor(s) { + (super(s), + (this.element = new Rh()), + (this.specPath = Tl(['document', 'objects', 'PathItem'])), + (this.canSupportSpecificationExtensions = !0), + (this.fieldPatternPredicate = es_T)); + } + ObjectElement(s) { + const o = ym.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(Um).forEach((s, o) => { + (o.classes.push('openapi-path-template'), + o.classes.push('path-template'), + s.setMetaProperty('path', cloneDeep(o))); + }), + o + ); + } + } + const Jy = PathsVisitor; + class RequestBodyVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new Lh()), + (this.specPath = Tl(['document', 'objects', 'RequestBody']))); + } + ObjectElement(s) { + const o = im.prototype.ObjectElement.call(this, s); + return ( + Fu(this.element.contentProp) && + this.element.contentProp.filter(eg).forEach((s, o) => { + s.setMetaProperty('media-type', serializers_value(o)); + }), + o + ); + } + } + const Gy = RequestBodyVisitor; + class RequestBodyContent extends Cu.Sh { + static primaryClass = 'request-body-content'; + constructor(s, o, i) { + (super(s, o, i), + this.classes.push(RequestBodyContent.primaryClass), + this.classes.push('content')); + } + } + const Yy = RequestBodyContent; + const Xy = class request_body_ContentVisitor_ContentVisitor extends Xg { + constructor(s) { + (super(s), (this.element = new Yy())); + } + }; + class CallbackVisitor extends Mixin(ym, rm) { + constructor(s) { + (super(s), + (this.element = new Hp()), + (this.specPath = Tl(['document', 'objects', 'PathItem'])), + (this.canSupportSpecificationExtensions = !0), + (this.fieldPatternPredicate = (s) => + /{(?[^}]{1,2083})}/.test(String(s)))); + } + ObjectElement(s) { + const o = vm.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(Um).forEach((s, o) => { + s.setMetaProperty('runtime-expression', serializers_value(o)); + }), + o + ); + } + } + const Zy = CallbackVisitor; + class ResponseVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new Fh()), + (this.specPath = Tl(['document', 'objects', 'Response']))); + } + ObjectElement(s) { + const o = im.prototype.ObjectElement.call(this, s); + return ( + Fu(this.element.contentProp) && + this.element.contentProp.filter(eg).forEach((s, o) => { + s.setMetaProperty('media-type', serializers_value(o)); + }), + Fu(this.element.headers) && + this.element.headers.filter(Rm).forEach((s, o) => { + s.setMetaProperty('header-name', serializers_value(o)); + }), + o + ); + } + } + const Qy = ResponseVisitor; + class ResponseHeaders extends Cu.Sh { + static primaryClass = 'response-headers'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(ResponseHeaders.primaryClass)); + } + } + const ev = ResponseHeaders; + class response_HeadersVisitor_HeadersVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new ev()), + (this.specPath = (s) => + isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'Header'])); + } + ObjectElement(s) { + const o = vm.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'header'); + }), + this.element.forEach((s, o) => { + if (!Rm(s)) return; + const i = serializers_value(o); + s.setMetaProperty('header-name', i); + }), + o + ); + } + } + const tv = response_HeadersVisitor_HeadersVisitor; + class ResponseContent extends Cu.Sh { + static primaryClass = 'response-content'; + constructor(s, o, i) { + (super(s, o, i), + this.classes.push(ResponseContent.primaryClass), + this.classes.push('content')); + } + } + const rv = ResponseContent; + const nv = class response_ContentVisitor_ContentVisitor extends Xg { + constructor(s) { + (super(s), (this.element = new rv())); + } + }; + class ResponseLinks extends Cu.Sh { + static primaryClass = 'response-links'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(ResponseLinks.primaryClass)); + } + } + const sv = ResponseLinks; + class response_LinksVisitor_LinksVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new sv()), + (this.specPath = (s) => + isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'Link'])); + } + ObjectElement(s) { + const o = vm.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'link'); + }), + o + ); + } + } + const ov = response_LinksVisitor_LinksVisitor; + function _isNumber(s) { + return '[object Number]' === Object.prototype.toString.call(s); + } + var iv = _curry2(function range(s, o) { + if (!_isNumber(s) || !_isNumber(o)) + throw new TypeError('Both arguments to range must be numbers'); + for ( + var i = Array(s < o ? o - s : 0), u = s < 0 ? o + Math.abs(s) : o - s, _ = 0; + _ < u; + ) + ((i[_] = _ + s), (_ += 1)); + return i; + }); + const av = iv; + function hasOrAdd(s, o, i) { + var u, + _ = typeof s; + switch (_) { + case 'string': + case 'number': + return 0 === s && 1 / s == -1 / 0 + ? !!i._items['-0'] || (o && (i._items['-0'] = !0), !1) + : null !== i._nativeSet + ? o + ? ((u = i._nativeSet.size), i._nativeSet.add(s), i._nativeSet.size === u) + : i._nativeSet.has(s) + : _ in i._items + ? s in i._items[_] || (o && (i._items[_][s] = !0), !1) + : (o && ((i._items[_] = {}), (i._items[_][s] = !0)), !1); + case 'boolean': + if (_ in i._items) { + var w = s ? 1 : 0; + return !!i._items[_][w] || (o && (i._items[_][w] = !0), !1); + } + return (o && (i._items[_] = s ? [!1, !0] : [!0, !1]), !1); + case 'function': + return null !== i._nativeSet + ? o + ? ((u = i._nativeSet.size), i._nativeSet.add(s), i._nativeSet.size === u) + : i._nativeSet.has(s) + : _ in i._items + ? !!_includes(s, i._items[_]) || (o && i._items[_].push(s), !1) + : (o && (i._items[_] = [s]), !1); + case 'undefined': + return !!i._items[_] || (o && (i._items[_] = !0), !1); + case 'object': + if (null === s) return !!i._items.null || (o && (i._items.null = !0), !1); + default: + return (_ = Object.prototype.toString.call(s)) in i._items + ? !!_includes(s, i._items[_]) || (o && i._items[_].push(s), !1) + : (o && (i._items[_] = [s]), !1); + } + } + const lv = (function () { + function _Set() { + ((this._nativeSet = 'function' == typeof Set ? new Set() : null), (this._items = {})); + } + return ( + (_Set.prototype.add = function (s) { + return !hasOrAdd(s, !0, this); + }), + (_Set.prototype.has = function (s) { + return hasOrAdd(s, !1, this); + }), + _Set + ); + })(); + var cv = _curry2(function difference(s, o) { + for (var i = [], u = 0, _ = s.length, w = o.length, x = new lv(), C = 0; C < w; C += 1) + x.add(o[C]); + for (; u < _; ) (x.add(s[u]) && (i[i.length] = s[u]), (u += 1)); + return i; + }); + const uv = cv; + class MixedFieldsVisitor extends Mixin(im, ym) { + specPathFixedFields; + specPathPatternedFields; + constructor({ specPathFixedFields: s, specPathPatternedFields: o, ...i }) { + (super({ ...i }), (this.specPathFixedFields = s), (this.specPathPatternedFields = o)); + } + ObjectElement(s) { + const { specPath: o, ignoredFields: i } = this; + try { + this.specPath = this.specPathFixedFields; + const o = this.retrieveFixedFields(this.specPath(s)); + ((this.ignoredFields = [...i, ...uv(s.keys(), o)]), + im.prototype.ObjectElement.call(this, s), + (this.specPath = this.specPathPatternedFields), + (this.ignoredFields = o), + ym.prototype.ObjectElement.call(this, s)); + } catch (s) { + throw ((this.specPath = o), s); + } + return Ju; + } + } + const pv = MixedFieldsVisitor; + class responses_ResponsesVisitor extends Mixin(pv, rm) { + constructor(s) { + (super(s), + (this.element = new Kh()), + (this.specPathFixedFields = Tl(['document', 'objects', 'Responses'])), + (this.canSupportSpecificationExtensions = !0), + (this.specPathPatternedFields = (s) => + isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'Response']), + (this.fieldPatternPredicate = (s) => + new RegExp(`^(1XX|2XX|3XX|4XX|5XX|${av(100, 600).join('|')})$`).test(String(s)))); + } + ObjectElement(s) { + const o = pv.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'response'); + }), + this.element.filter(Hm).forEach((s, o) => { + const i = cloneDeep(o); + this.fieldPatternPredicate(serializers_value(i)) && + s.setMetaProperty('http-status-code', i); + }), + o + ); + } + } + const hv = responses_ResponsesVisitor; + class DefaultVisitor extends Mixin(jm, rm) { + constructor(s) { + (super(s), + (this.alternator = [ + { + predicate: isReferenceLikeElement, + specPath: ['document', 'objects', 'Reference'] + }, + { predicate: es_T, specPath: ['document', 'objects', 'Response'] } + ])); + } + ObjectElement(s) { + const o = jm.prototype.enter.call(this, s); + return ( + Wm(this.element) + ? this.element.setMetaProperty('referenced-element', 'response') + : Hm(this.element) && this.element.setMetaProperty('http-status-code', 'default'), + o + ); + } + } + const dv = DefaultVisitor; + class OperationVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new jh()), + (this.specPath = Tl(['document', 'objects', 'Operation']))); + } + } + const fv = OperationVisitor; + class OperationTags extends Cu.wE { + static primaryClass = 'operation-tags'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(OperationTags.primaryClass)); + } + } + const mv = OperationTags; + const gv = class TagsVisitor extends rm { + constructor(s) { + (super(s), (this.element = new mv())); + } + ArrayElement(s) { + return ((this.element = this.element.concat(cloneDeep(s))), Ju); + } + }; + class OperationParameters extends Cu.wE { + static primaryClass = 'operation-parameters'; + constructor(s, o, i) { + (super(s, o, i), + this.classes.push(OperationParameters.primaryClass), + this.classes.push('parameters')); + } + } + const yv = OperationParameters; + class open_api_3_0_ParametersVisitor_ParametersVisitor extends Mixin(nm, rm) { + constructor(s) { + (super(s), (this.element = new Cu.wE()), this.element.classes.push('parameters')); + } + ArrayElement(s) { + return ( + s.forEach((s) => { + const o = isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'Parameter'], + i = this.toRefractedElement(o, s); + (Wm(i) && i.setMetaProperty('referenced-element', 'parameter'), + this.element.push(i)); + }), + this.copyMetaAndAttributes(s, this.element), + Ju + ); + } + } + const vv = open_api_3_0_ParametersVisitor_ParametersVisitor; + const bv = class operation_ParametersVisitor_ParametersVisitor extends vv { + constructor(s) { + (super(s), (this.element = new yv())); + } + }; + const _v = class RequestBodyVisitor_RequestBodyVisitor extends jm { + constructor(s) { + (super(s), + (this.alternator = [ + { + predicate: isReferenceLikeElement, + specPath: ['document', 'objects', 'Reference'] + }, + { predicate: es_T, specPath: ['document', 'objects', 'RequestBody'] } + ])); + } + ObjectElement(s) { + const o = jm.prototype.enter.call(this, s); + return ( + Wm(this.element) && this.element.setMetaProperty('referenced-element', 'requestBody'), + o + ); + } + }; + class OperationCallbacks extends Cu.Sh { + static primaryClass = 'operation-callbacks'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(OperationCallbacks.primaryClass)); + } + } + const Ev = OperationCallbacks; + class CallbacksVisitor_CallbacksVisitor extends Mixin(vm, rm) { + specPath; + constructor(s) { + (super(s), + (this.element = new Ev()), + (this.specPath = (s) => + isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'Callback'])); + } + ObjectElement(s) { + const o = vm.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(Wm).forEach((s) => { + s.setMetaProperty('referenced-element', 'callback'); + }), + o + ); + } + } + const wv = CallbacksVisitor_CallbacksVisitor; + class OperationSecurity extends Cu.wE { + static primaryClass = 'operation-security'; + constructor(s, o, i) { + (super(s, o, i), + this.classes.push(OperationSecurity.primaryClass), + this.classes.push('security')); + } + } + const Sv = OperationSecurity; + class SecurityVisitor_SecurityVisitor extends Mixin(nm, rm) { + constructor(s) { + (super(s), (this.element = new Sv())); + } + ArrayElement(s) { + return ( + s.forEach((s) => { + const o = Fu(s) ? ['document', 'objects', 'SecurityRequirement'] : ['value'], + i = this.toRefractedElement(o, s); + this.element.push(i); + }), + this.copyMetaAndAttributes(s, this.element), + Ju + ); + } + } + const xv = SecurityVisitor_SecurityVisitor; + class OperationServers extends Cu.wE { + static primaryClass = 'operation-servers'; + constructor(s, o, i) { + (super(s, o, i), + this.classes.push(OperationServers.primaryClass), + this.classes.push('servers')); + } + } + const kv = OperationServers; + const Cv = class ServersVisitor_ServersVisitor extends xm { + constructor(s) { + (super(s), (this.element = new kv())); + } + }; + class PathItemVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new Ph()), + (this.specPath = Tl(['document', 'objects', 'PathItem']))); + } + ObjectElement(s) { + const o = im.prototype.ObjectElement.call(this, s); + return ( + this.element.filter($m).forEach((s, o) => { + const i = cloneDeep(o); + ((i.content = serializers_value(i).toUpperCase()), + s.setMetaProperty('http-method', i)); + }), + Ru(this.element.$ref) && this.element.classes.push('reference-element'), + o + ); + } + } + const Ov = PathItemVisitor; + const Av = class path_item_$RefVisitor_$RefVisitor extends rm { + StringElement(s) { + const o = super.enter(s); + return (this.element.classes.push('reference-value'), o); + } + }; + class PathItemServers extends Cu.wE { + static primaryClass = 'path-item-servers'; + constructor(s, o, i) { + (super(s, o, i), + this.classes.push(PathItemServers.primaryClass), + this.classes.push('servers')); + } + } + const jv = PathItemServers; + const Iv = class path_item_ServersVisitor_ServersVisitor extends xm { + constructor(s) { + (super(s), (this.element = new jv())); + } + }; + class PathItemParameters extends Cu.wE { + static primaryClass = 'path-item-parameters'; + constructor(s, o, i) { + (super(s, o, i), + this.classes.push(PathItemParameters.primaryClass), + this.classes.push('parameters')); + } + } + const Pv = PathItemParameters; + const Mv = class path_item_ParametersVisitor_ParametersVisitor extends vv { + constructor(s) { + (super(s), (this.element = new Pv())); + } + }; + class SecuritySchemeVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new Jf()), + (this.specPath = Tl(['document', 'objects', 'SecurityScheme'])), + (this.canSupportSpecificationExtensions = !0)); + } + } + const Tv = SecuritySchemeVisitor; + class OAuthFlowsVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new _h()), + (this.specPath = Tl(['document', 'objects', 'OAuthFlows'])), + (this.canSupportSpecificationExtensions = !0)); + } + } + const Nv = OAuthFlowsVisitor; + class OAuthFlowVisitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new vh()), + (this.specPath = Tl(['document', 'objects', 'OAuthFlow'])), + (this.canSupportSpecificationExtensions = !0)); + } + } + const Rv = OAuthFlowVisitor; + class OAuthFlowScopes extends Cu.Sh { + static primaryClass = 'oauth-flow-scopes'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(OAuthFlowScopes.primaryClass)); + } + } + const Dv = OAuthFlowScopes; + class ScopesVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), (this.element = new Dv()), (this.specPath = Tl(['value']))); + } + } + const Lv = ScopesVisitor; + class Tags extends Cu.wE { + static primaryClass = 'tags'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(Tags.primaryClass)); + } + } + const Bv = Tags; + class TagsVisitor_TagsVisitor extends Mixin(nm, rm) { + constructor(s) { + (super(s), (this.element = new Bv())); + } + ArrayElement(s) { + return ( + s.forEach((s) => { + const o = om(s) ? ['document', 'objects', 'Tag'] : ['value'], + i = this.toRefractedElement(o, s); + this.element.push(i); + }), + this.copyMetaAndAttributes(s, this.element), + Ju + ); + } + } + const Fv = TagsVisitor_TagsVisitor, + { fixedFields: qv } = jf.visitors.document.objects.JSONSchema, + $v = { + visitors: { + value: rm, + document: { + objects: { + OpenApi: { + $visitor: am, + fixedFields: { + openapi: lm, + info: { $ref: '#/visitors/document/objects/Info' }, + servers: xm, + paths: { $ref: '#/visitors/document/objects/Paths' }, + components: { $ref: '#/visitors/document/objects/Components' }, + security: yg, + tags: Fv, + externalDocs: { $ref: '#/visitors/document/objects/ExternalDocumentation' } + } + }, + Info: { + $visitor: um, + fixedFields: { + title: { $ref: '#/visitors/value' }, + description: { $ref: '#/visitors/value' }, + termsOfService: { $ref: '#/visitors/value' }, + contact: { $ref: '#/visitors/document/objects/Contact' }, + license: { $ref: '#/visitors/document/objects/License' }, + version: pm + } + }, + Contact: { + $visitor: hm, + fixedFields: { + name: { $ref: '#/visitors/value' }, + url: { $ref: '#/visitors/value' }, + email: { $ref: '#/visitors/value' } + } + }, + License: { + $visitor: dm, + fixedFields: { + name: { $ref: '#/visitors/value' }, + url: { $ref: '#/visitors/value' } + } + }, + Server: { + $visitor: Em, + fixedFields: { + url: wm, + description: { $ref: '#/visitors/value' }, + variables: Om + } + }, + ServerVariable: { + $visitor: km, + fixedFields: { + enum: { $ref: '#/visitors/value' }, + default: { $ref: '#/visitors/value' }, + description: { $ref: '#/visitors/value' } + } + }, + Components: { + $visitor: _g, + fixedFields: { + schemas: ky, + responses: Oy, + parameters: jy, + examples: Py, + requestBodies: Ty, + headers: Ry, + securitySchemes: Ly, + links: Fy, + callbacks: $y + } + }, + Paths: { $visitor: Jy }, + PathItem: { + $visitor: Ov, + fixedFields: { + $ref: Av, + summary: { $ref: '#/visitors/value' }, + description: { $ref: '#/visitors/value' }, + get: { $ref: '#/visitors/document/objects/Operation' }, + put: { $ref: '#/visitors/document/objects/Operation' }, + post: { $ref: '#/visitors/document/objects/Operation' }, + delete: { $ref: '#/visitors/document/objects/Operation' }, + options: { $ref: '#/visitors/document/objects/Operation' }, + head: { $ref: '#/visitors/document/objects/Operation' }, + patch: { $ref: '#/visitors/document/objects/Operation' }, + trace: { $ref: '#/visitors/document/objects/Operation' }, + servers: Iv, + parameters: Mv + } + }, + Operation: { + $visitor: fv, + fixedFields: { + tags: gv, + summary: { $ref: '#/visitors/value' }, + description: { $ref: '#/visitors/value' }, + externalDocs: { $ref: '#/visitors/document/objects/ExternalDocumentation' }, + operationId: { $ref: '#/visitors/value' }, + parameters: bv, + requestBody: _v, + responses: { $ref: '#/visitors/document/objects/Responses' }, + callbacks: wv, + deprecated: { $ref: '#/visitors/value' }, + security: xv, + servers: Cv + } + }, + ExternalDocumentation: { + $visitor: zy, + fixedFields: { + description: { $ref: '#/visitors/value' }, + url: { $ref: '#/visitors/value' } + } + }, + Parameter: { + $visitor: Vg, + fixedFields: { + name: { $ref: '#/visitors/value' }, + in: { $ref: '#/visitors/value' }, + description: { $ref: '#/visitors/value' }, + required: { $ref: '#/visitors/value' }, + deprecated: { $ref: '#/visitors/value' }, + allowEmptyValue: { $ref: '#/visitors/value' }, + style: { $ref: '#/visitors/value' }, + explode: { $ref: '#/visitors/value' }, + allowReserved: { $ref: '#/visitors/value' }, + schema: Ug, + example: { $ref: '#/visitors/value' }, + examples: Ey, + content: Sy + } + }, + RequestBody: { + $visitor: Gy, + fixedFields: { + description: { $ref: '#/visitors/value' }, + content: Xy, + required: { $ref: '#/visitors/value' } + } + }, + MediaType: { + $visitor: Am, + fixedFields: { + schema: ng, + example: { $ref: '#/visitors/value' }, + examples: lg, + encoding: fg + } + }, + Encoding: { + $visitor: Wy, + fixedFields: { + contentType: { $ref: '#/visitors/value' }, + headers: Hy, + style: { $ref: '#/visitors/value' }, + explode: { $ref: '#/visitors/value' }, + allowReserved: { $ref: '#/visitors/value' } + } + }, + Responses: { $visitor: hv, fixedFields: { default: dv } }, + Response: { + $visitor: Qy, + fixedFields: { + description: { $ref: '#/visitors/value' }, + headers: tv, + content: nv, + links: ov + } + }, + Callback: { $visitor: Zy }, + Example: { + $visitor: Vy, + fixedFields: { + summary: { $ref: '#/visitors/value' }, + description: { $ref: '#/visitors/value' }, + value: { $ref: '#/visitors/value' }, + externalValue: Uy + } + }, + Link: { + $visitor: fm, + fixedFields: { + operationRef: mm, + operationId: gm, + parameters: _m, + requestBody: { $ref: '#/visitors/value' }, + description: { $ref: '#/visitors/value' }, + server: { $ref: '#/visitors/document/objects/Server' } + } + }, + Header: { + $visitor: zg, + fixedFields: { + description: { $ref: '#/visitors/value' }, + required: { $ref: '#/visitors/value' }, + deprecated: { $ref: '#/visitors/value' }, + allowEmptyValue: { $ref: '#/visitors/value' }, + style: { $ref: '#/visitors/value' }, + explode: { $ref: '#/visitors/value' }, + allowReserved: { $ref: '#/visitors/value' }, + schema: Wg, + example: { $ref: '#/visitors/value' }, + examples: Yg, + content: ey + } + }, + Tag: { + $visitor: xg, + fixedFields: { + name: { $ref: '#/visitors/value' }, + description: { $ref: '#/visitors/value' }, + externalDocs: { $ref: '#/visitors/document/objects/ExternalDocumentation' } + } + }, + Reference: { $visitor: kg, fixedFields: { $ref: qg } }, + JSONSchema: { $ref: '#/visitors/document/objects/Schema' }, + JSONReference: { $ref: '#/visitors/document/objects/Reference' }, + Schema: { + $visitor: ty, + fixedFields: { + title: qv.title, + multipleOf: qv.multipleOf, + maximum: qv.maximum, + exclusiveMaximum: qv.exclusiveMaximum, + minimum: qv.minimum, + exclusiveMinimum: qv.exclusiveMinimum, + maxLength: qv.maxLength, + minLength: qv.minLength, + pattern: qv.pattern, + maxItems: qv.maxItems, + minItems: qv.minItems, + uniqueItems: qv.uniqueItems, + maxProperties: qv.maxProperties, + minProperties: qv.minProperties, + required: qv.required, + enum: qv.enum, + type: dy, + allOf: ny, + anyOf: oy, + oneOf: ay, + not: my, + items: cy, + properties: py, + additionalProperties: my, + description: qv.description, + format: qv.format, + default: qv.default, + nullable: { $ref: '#/visitors/value' }, + discriminator: { $ref: '#/visitors/document/objects/Discriminator' }, + writeOnly: { $ref: '#/visitors/value' }, + xml: { $ref: '#/visitors/document/objects/XML' }, + externalDocs: { $ref: '#/visitors/document/objects/ExternalDocumentation' }, + example: { $ref: '#/visitors/value' }, + deprecated: { $ref: '#/visitors/value' } + } + }, + Discriminator: { + $visitor: gy, + fixedFields: { propertyName: { $ref: '#/visitors/value' }, mapping: vy } + }, + XML: { + $visitor: by, + fixedFields: { + name: { $ref: '#/visitors/value' }, + namespace: { $ref: '#/visitors/value' }, + prefix: { $ref: '#/visitors/value' }, + attribute: { $ref: '#/visitors/value' }, + wrapped: { $ref: '#/visitors/value' } + } + }, + SecurityScheme: { + $visitor: Tv, + fixedFields: { + type: { $ref: '#/visitors/value' }, + description: { $ref: '#/visitors/value' }, + name: { $ref: '#/visitors/value' }, + in: { $ref: '#/visitors/value' }, + scheme: { $ref: '#/visitors/value' }, + bearerFormat: { $ref: '#/visitors/value' }, + flows: { $ref: '#/visitors/document/objects/OAuthFlows' }, + openIdConnectUrl: { $ref: '#/visitors/value' } + } + }, + OAuthFlows: { + $visitor: Nv, + fixedFields: { + implicit: { $ref: '#/visitors/document/objects/OAuthFlow' }, + password: { $ref: '#/visitors/document/objects/OAuthFlow' }, + clientCredentials: { $ref: '#/visitors/document/objects/OAuthFlow' }, + authorizationCode: { $ref: '#/visitors/document/objects/OAuthFlow' } + } + }, + OAuthFlow: { + $visitor: Rv, + fixedFields: { + authorizationUrl: { $ref: '#/visitors/value' }, + tokenUrl: { $ref: '#/visitors/value' }, + refreshUrl: { $ref: '#/visitors/value' }, + scopes: Lv + } + }, + SecurityRequirement: { $visitor: mg } + }, + extension: { $visitor: cm } + } + } + }, + es_traversal_visitor_getNodeType = (s) => { + if (Nu(s)) return `${s.element.charAt(0).toUpperCase() + s.element.slice(1)}Element`; + }, + Vv = { + CallbackElement: ['content'], + ComponentsElement: ['content'], + ContactElement: ['content'], + DiscriminatorElement: ['content'], + Encoding: ['content'], + Example: ['content'], + ExternalDocumentationElement: ['content'], + HeaderElement: ['content'], + InfoElement: ['content'], + LicenseElement: ['content'], + MediaTypeElement: ['content'], + OAuthFlowElement: ['content'], + OAuthFlowsElement: ['content'], + OpenApi3_0Element: ['content'], + OperationElement: ['content'], + ParameterElement: ['content'], + PathItemElement: ['content'], + PathsElement: ['content'], + ReferenceElement: ['content'], + RequestBodyElement: ['content'], + ResponseElement: ['content'], + ResponsesElement: ['content'], + SchemaElement: ['content'], + SecurityRequirementElement: ['content'], + SecuritySchemeElement: ['content'], + ServerElement: ['content'], + ServerVariableElement: ['content'], + TagElement: ['content'], + ...Qu + }, + Uv = { + namespace: (s) => { + const { base: o } = s; + return ( + o.register('callback', Hp), + o.register('components', Jp), + o.register('contact', Gp), + o.register('discriminator', Yp), + o.register('encoding', Xp), + o.register('example', Zp), + o.register('externalDocumentation', Qp), + o.register('header', th), + o.register('info', rh), + o.register('license', uh), + o.register('link', dh), + o.register('mediaType', fh), + o.register('oAuthFlow', vh), + o.register('oAuthFlows', _h), + o.register('openapi', wh), + o.register('openApi3_0', Oh), + o.register('operation', jh), + o.register('parameter', Ih), + o.register('pathItem', Ph), + o.register('paths', Rh), + o.register('reference', Dh), + o.register('requestBody', Lh), + o.register('response', Fh), + o.register('responses', Kh), + o.register('schema', Wf), + o.register('securityRequirement', Hf), + o.register('securityScheme', Jf), + o.register('server', Gf), + o.register('serverVariable', Xf), + o.register('tag', Qf), + o.register('xml', em), + o + ); + } + }, + zv = Uv, + es_refractor_toolbox = () => { + const s = createNamespace(zv); + return { + predicates: { + ...pe, + isElement: Nu, + isStringElement: Ru, + isArrayElement: qu, + isObjectElement: Fu, + isMemberElement: $u, + includesClasses, + hasElementSourceMap + }, + namespace: s + }; + }, + es_refractor_refract = ( + s, + { + specPath: o = ['visitors', 'document', 'objects', 'OpenApi', '$visitor'], + plugins: i = [] + } = {} + ) => { + const u = (0, Cu.e)(s), + _ = dereference($v), + w = new (cp(o, _))({ specObj: _ }); + return ( + visitor_visit(u, w), + dispatchPluginsSync(w.element, i, { + toolboxCreator: es_refractor_toolbox, + visitorOptions: { keyMap: Vv, nodeTypeGetter: es_traversal_visitor_getNodeType } + }) + ); + }, + es_refractor_createRefractor = + (s) => + (o, i = {}) => + es_refractor_refract(o, { specPath: s, ...i }); + ((Hp.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Callback', + '$visitor' + ])), + (Jp.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Components', + '$visitor' + ])), + (Gp.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Contact', + '$visitor' + ])), + (Zp.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Example', + '$visitor' + ])), + (Yp.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Discriminator', + '$visitor' + ])), + (Xp.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Encoding', + '$visitor' + ])), + (Qp.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'ExternalDocumentation', + '$visitor' + ])), + (th.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Header', + '$visitor' + ])), + (rh.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Info', + '$visitor' + ])), + (uh.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'License', + '$visitor' + ])), + (dh.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Link', + '$visitor' + ])), + (fh.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'MediaType', + '$visitor' + ])), + (vh.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'OAuthFlow', + '$visitor' + ])), + (_h.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'OAuthFlows', + '$visitor' + ])), + (wh.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'OpenApi', + 'fixedFields', + 'openapi' + ])), + (Oh.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'OpenApi', + '$visitor' + ])), + (jh.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Operation', + '$visitor' + ])), + (Ih.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Parameter', + '$visitor' + ])), + (Ph.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'PathItem', + '$visitor' + ])), + (Rh.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Paths', + '$visitor' + ])), + (Dh.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Reference', + '$visitor' + ])), + (Lh.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'RequestBody', + '$visitor' + ])), + (Fh.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Response', + '$visitor' + ])), + (Kh.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Responses', + '$visitor' + ])), + (Wf.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Schema', + '$visitor' + ])), + (Hf.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'SecurityRequirement', + '$visitor' + ])), + (Jf.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'SecurityScheme', + '$visitor' + ])), + (Gf.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Server', + '$visitor' + ])), + (Xf.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'ServerVariable', + '$visitor' + ])), + (Qf.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Tag', + '$visitor' + ])), + (em.refract = es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'XML', + '$visitor' + ]))); + const Wv = class Callback_Callback extends Hp {}; + const Kv = class Components_Components extends Jp { + get pathItems() { + return this.get('pathItems'); + } + set pathItems(s) { + this.set('pathItems', s); + } + }; + const Hv = class Contact_Contact extends Gp {}; + const Jv = class Discriminator_Discriminator extends Yp {}; + const Gv = class Encoding_Encoding extends Xp {}; + const Yv = class Example_Example extends Zp {}; + const Xv = class ExternalDocumentation_ExternalDocumentation extends Qp {}; + const Zv = class Header_Header extends th { + get schema() { + return this.get('schema'); + } + set schema(s) { + this.set('schema', s); + } + }; + const Qv = class Info_Info extends rh { + get license() { + return this.get('license'); + } + set license(s) { + this.set('license', s); + } + get summary() { + return this.get('summary'); + } + set summary(s) { + this.set('summary', s); + } + }; + class JsonSchemaDialect extends Cu.Om { + static default = new JsonSchemaDialect('https://spec.openapis.org/oas/3.1/dialect/base'); + constructor(s, o, i) { + (super(s, o, i), (this.element = 'jsonSchemaDialect')); + } + } + const eb = JsonSchemaDialect; + const tb = class License_License extends uh { + get identifier() { + return this.get('identifier'); + } + set identifier(s) { + this.set('identifier', s); + } + }; + const nb = class Link_Link extends dh {}; + const pb = class MediaType_MediaType extends fh { + get schema() { + return this.get('schema'); + } + set schema(s) { + this.set('schema', s); + } + }; + const mb = class OAuthFlow_OAuthFlow extends vh {}; + const yb = class OAuthFlows_OAuthFlows extends _h {}; + const _b = class Openapi_Openapi extends wh {}; + class OpenApi3_1 extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'openApi3_1'), this.classes.push('api')); + } + get openapi() { + return this.get('openapi'); + } + set openapi(s) { + this.set('openapi', s); + } + get info() { + return this.get('info'); + } + set info(s) { + this.set('info', s); + } + get jsonSchemaDialect() { + return this.get('jsonSchemaDialect'); + } + set jsonSchemaDialect(s) { + this.set('jsonSchemaDialect', s); + } + get servers() { + return this.get('servers'); + } + set servers(s) { + this.set('servers', s); + } + get paths() { + return this.get('paths'); + } + set paths(s) { + this.set('paths', s); + } + get components() { + return this.get('components'); + } + set components(s) { + this.set('components', s); + } + get security() { + return this.get('security'); + } + set security(s) { + this.set('security', s); + } + get tags() { + return this.get('tags'); + } + set tags(s) { + this.set('tags', s); + } + get externalDocs() { + return this.get('externalDocs'); + } + set externalDocs(s) { + this.set('externalDocs', s); + } + get webhooks() { + return this.get('webhooks'); + } + set webhooks(s) { + this.set('webhooks', s); + } + } + const wb = OpenApi3_1; + const Sb = class Operation_Operation extends jh { + get requestBody() { + return this.get('requestBody'); + } + set requestBody(s) { + this.set('requestBody', s); + } + }; + const Ob = class Parameter_Parameter extends Ih { + get schema() { + return this.get('schema'); + } + set schema(s) { + this.set('schema', s); + } + }; + const Ab = class PathItem_PathItem extends Ph { + get GET() { + return this.get('get'); + } + set GET(s) { + this.set('GET', s); + } + get PUT() { + return this.get('put'); + } + set PUT(s) { + this.set('PUT', s); + } + get POST() { + return this.get('post'); + } + set POST(s) { + this.set('POST', s); + } + get DELETE() { + return this.get('delete'); + } + set DELETE(s) { + this.set('DELETE', s); + } + get OPTIONS() { + return this.get('options'); + } + set OPTIONS(s) { + this.set('OPTIONS', s); + } + get HEAD() { + return this.get('head'); + } + set HEAD(s) { + this.set('HEAD', s); + } + get PATCH() { + return this.get('patch'); + } + set PATCH(s) { + this.set('PATCH', s); + } + get TRACE() { + return this.get('trace'); + } + set TRACE(s) { + this.set('TRACE', s); + } + }; + const Ib = class Paths_Paths extends Rh {}; + class Reference_Reference extends Dh {} + (Object.defineProperty(Reference_Reference.prototype, 'description', { + get() { + return this.get('description'); + }, + set(s) { + this.set('description', s); + }, + enumerable: !0 + }), + Object.defineProperty(Reference_Reference.prototype, 'summary', { + get() { + return this.get('summary'); + }, + set(s) { + this.set('summary', s); + }, + enumerable: !0 + })); + const Pb = Reference_Reference; + const Mb = class RequestBody_RequestBody extends Lh {}; + const Rb = class elements_Response_Response extends Fh {}; + const Lb = class Responses_Responses extends Kh {}; + class elements_Schema_Schema extends Cu.Sh { + constructor(s, o, i) { + (super(s, o, i), (this.element = 'schema')); + } + get $schema() { + return this.get('$schema'); + } + set $schema(s) { + this.set('$schema', s); + } + get $vocabulary() { + return this.get('$vocabulary'); + } + set $vocabulary(s) { + this.set('$vocabulary', s); + } + get $id() { + return this.get('$id'); + } + set $id(s) { + this.set('$id', s); + } + get $anchor() { + return this.get('$anchor'); + } + set $anchor(s) { + this.set('$anchor', s); + } + get $dynamicAnchor() { + return this.get('$dynamicAnchor'); + } + set $dynamicAnchor(s) { + this.set('$dynamicAnchor', s); + } + get $dynamicRef() { + return this.get('$dynamicRef'); + } + set $dynamicRef(s) { + this.set('$dynamicRef', s); + } + get $ref() { + return this.get('$ref'); + } + set $ref(s) { + this.set('$ref', s); + } + get $defs() { + return this.get('$defs'); + } + set $defs(s) { + this.set('$defs', s); + } + get $comment() { + return this.get('$comment'); + } + set $comment(s) { + this.set('$comment', s); + } + get allOf() { + return this.get('allOf'); + } + set allOf(s) { + this.set('allOf', s); + } + get anyOf() { + return this.get('anyOf'); + } + set anyOf(s) { + this.set('anyOf', s); + } + get oneOf() { + return this.get('oneOf'); + } + set oneOf(s) { + this.set('oneOf', s); + } + get not() { + return this.get('not'); + } + set not(s) { + this.set('not', s); + } + get if() { + return this.get('if'); + } + set if(s) { + this.set('if', s); + } + get then() { + return this.get('then'); + } + set then(s) { + this.set('then', s); + } + get else() { + return this.get('else'); + } + set else(s) { + this.set('else', s); + } + get dependentSchemas() { + return this.get('dependentSchemas'); + } + set dependentSchemas(s) { + this.set('dependentSchemas', s); + } + get prefixItems() { + return this.get('prefixItems'); + } + set prefixItems(s) { + this.set('prefixItems', s); + } + get items() { + return this.get('items'); + } + set items(s) { + this.set('items', s); + } + get containsProp() { + return this.get('contains'); + } + set containsProp(s) { + this.set('contains', s); + } + get properties() { + return this.get('properties'); + } + set properties(s) { + this.set('properties', s); + } + get patternProperties() { + return this.get('patternProperties'); + } + set patternProperties(s) { + this.set('patternProperties', s); + } + get additionalProperties() { + return this.get('additionalProperties'); + } + set additionalProperties(s) { + this.set('additionalProperties', s); + } + get propertyNames() { + return this.get('propertyNames'); + } + set propertyNames(s) { + this.set('propertyNames', s); + } + get unevaluatedItems() { + return this.get('unevaluatedItems'); + } + set unevaluatedItems(s) { + this.set('unevaluatedItems', s); + } + get unevaluatedProperties() { + return this.get('unevaluatedProperties'); + } + set unevaluatedProperties(s) { + this.set('unevaluatedProperties', s); + } + get type() { + return this.get('type'); + } + set type(s) { + this.set('type', s); + } + get enum() { + return this.get('enum'); + } + set enum(s) { + this.set('enum', s); + } + get const() { + return this.get('const'); + } + set const(s) { + this.set('const', s); + } + get multipleOf() { + return this.get('multipleOf'); + } + set multipleOf(s) { + this.set('multipleOf', s); + } + get maximum() { + return this.get('maximum'); + } + set maximum(s) { + this.set('maximum', s); + } + get exclusiveMaximum() { + return this.get('exclusiveMaximum'); + } + set exclusiveMaximum(s) { + this.set('exclusiveMaximum', s); + } + get minimum() { + return this.get('minimum'); + } + set minimum(s) { + this.set('minimum', s); + } + get exclusiveMinimum() { + return this.get('exclusiveMinimum'); + } + set exclusiveMinimum(s) { + this.set('exclusiveMinimum', s); + } + get maxLength() { + return this.get('maxLength'); + } + set maxLength(s) { + this.set('maxLength', s); + } + get minLength() { + return this.get('minLength'); + } + set minLength(s) { + this.set('minLength', s); + } + get pattern() { + return this.get('pattern'); + } + set pattern(s) { + this.set('pattern', s); + } + get maxItems() { + return this.get('maxItems'); + } + set maxItems(s) { + this.set('maxItems', s); + } + get minItems() { + return this.get('minItems'); + } + set minItems(s) { + this.set('minItems', s); + } + get uniqueItems() { + return this.get('uniqueItems'); + } + set uniqueItems(s) { + this.set('uniqueItems', s); + } + get maxContains() { + return this.get('maxContains'); + } + set maxContains(s) { + this.set('maxContains', s); + } + get minContains() { + return this.get('minContains'); + } + set minContains(s) { + this.set('minContains', s); + } + get maxProperties() { + return this.get('maxProperties'); + } + set maxProperties(s) { + this.set('maxProperties', s); + } + get minProperties() { + return this.get('minProperties'); + } + set minProperties(s) { + this.set('minProperties', s); + } + get required() { + return this.get('required'); + } + set required(s) { + this.set('required', s); + } + get dependentRequired() { + return this.get('dependentRequired'); + } + set dependentRequired(s) { + this.set('dependentRequired', s); + } + get title() { + return this.get('title'); + } + set title(s) { + this.set('title', s); + } + get description() { + return this.get('description'); + } + set description(s) { + this.set('description', s); + } + get default() { + return this.get('default'); + } + set default(s) { + this.set('default', s); + } + get deprecated() { + return this.get('deprecated'); + } + set deprecated(s) { + this.set('deprecated', s); + } + get readOnly() { + return this.get('readOnly'); + } + set readOnly(s) { + this.set('readOnly', s); + } + get writeOnly() { + return this.get('writeOnly'); + } + set writeOnly(s) { + this.set('writeOnly', s); + } + get examples() { + return this.get('examples'); + } + set examples(s) { + this.set('examples', s); + } + get format() { + return this.get('format'); + } + set format(s) { + this.set('format', s); + } + get contentEncoding() { + return this.get('contentEncoding'); + } + set contentEncoding(s) { + this.set('contentEncoding', s); + } + get contentMediaType() { + return this.get('contentMediaType'); + } + set contentMediaType(s) { + this.set('contentMediaType', s); + } + get contentSchema() { + return this.get('contentSchema'); + } + set contentSchema(s) { + this.set('contentSchema', s); + } + get discriminator() { + return this.get('discriminator'); + } + set discriminator(s) { + this.set('discriminator', s); + } + get xml() { + return this.get('xml'); + } + set xml(s) { + this.set('xml', s); + } + get externalDocs() { + return this.get('externalDocs'); + } + set externalDocs(s) { + this.set('externalDocs', s); + } + get example() { + return this.get('example'); + } + set example(s) { + this.set('example', s); + } + } + const qb = elements_Schema_Schema; + const zb = class SecurityRequirement_SecurityRequirement extends Hf {}; + const Qb = class SecurityScheme_SecurityScheme extends Jf {}; + const e_ = class Server_Server extends Gf {}; + const t_ = class ServerVariable_ServerVariable extends Xf {}; + const r_ = class Tag_Tag extends Qf {}; + const n_ = class Xml_Xml extends em {}; + class OpenApi3_1Visitor extends Mixin(im, rm) { + constructor(s) { + (super(s), + (this.element = new wb()), + (this.specPath = Tl(['document', 'objects', 'OpenApi'])), + (this.canSupportSpecificationExtensions = !0), + (this.openApiSemanticElement = this.element)); + } + ObjectElement(s) { + return ((this.openApiGenericElement = s), im.prototype.ObjectElement.call(this, s)); + } + } + const s_ = OpenApi3_1Visitor, + { + visitors: { + document: { + objects: { + Info: { $visitor: o_ } + } + } + } + } = $v; + const i_ = class info_InfoVisitor extends o_ { + constructor(s) { + (super(s), (this.element = new Qv())); + } + }, + { + visitors: { + document: { + objects: { + Contact: { $visitor: a_ } + } + } + } + } = $v; + const l_ = class contact_ContactVisitor extends a_ { + constructor(s) { + (super(s), (this.element = new Hv())); + } + }, + { + visitors: { + document: { + objects: { + License: { $visitor: c_ } + } + } + } + } = $v; + const u_ = class license_LicenseVisitor extends c_ { + constructor(s) { + (super(s), (this.element = new tb())); + } + }, + { + visitors: { + document: { + objects: { + Link: { $visitor: p_ } + } + } + } + } = $v; + const h_ = class link_LinkVisitor extends p_ { + constructor(s) { + (super(s), (this.element = new nb())); + } + }; + class JsonSchemaDialectVisitor extends Mixin(nm, rm) { + StringElement(s) { + const o = new eb(serializers_value(s)); + return (this.copyMetaAndAttributes(s, o), (this.element = o), Ju); + } + } + const d_ = JsonSchemaDialectVisitor, + { + visitors: { + document: { + objects: { + Server: { $visitor: f_ } + } + } + } + } = $v; + const m_ = class server_ServerVisitor extends f_ { + constructor(s) { + (super(s), (this.element = new e_())); + } + }, + { + visitors: { + document: { + objects: { + ServerVariable: { $visitor: g_ } + } + } + } + } = $v; + const y_ = class server_variable_ServerVariableVisitor extends g_ { + constructor(s) { + (super(s), (this.element = new t_())); + } + }, + { + visitors: { + document: { + objects: { + MediaType: { $visitor: v_ } + } + } + } + } = $v; + const b_ = class media_type_MediaTypeVisitor extends v_ { + constructor(s) { + (super(s), (this.element = new pb())); + } + }, + { + visitors: { + document: { + objects: { + SecurityRequirement: { $visitor: E_ } + } + } + } + } = $v; + const w_ = class security_requirement_SecurityRequirementVisitor extends E_ { + constructor(s) { + (super(s), (this.element = new zb())); + } + }, + { + visitors: { + document: { + objects: { + Components: { $visitor: S_ } + } + } + } + } = $v; + const x_ = class components_ComponentsVisitor extends S_ { + constructor(s) { + (super(s), (this.element = new Kv())); + } + }, + { + visitors: { + document: { + objects: { + Tag: { $visitor: k_ } + } + } + } + } = $v; + const C_ = class tag_TagVisitor extends k_ { + constructor(s) { + (super(s), (this.element = new r_())); + } + }, + { + visitors: { + document: { + objects: { + Reference: { $visitor: O_ } + } + } + } + } = $v; + const A_ = class reference_ReferenceVisitor extends O_ { + constructor(s) { + (super(s), (this.element = new Pb())); + } + }, + { + visitors: { + document: { + objects: { + Parameter: { $visitor: j_ } + } + } + } + } = $v; + const I_ = class parameter_ParameterVisitor extends j_ { + constructor(s) { + (super(s), (this.element = new Ob())); + } + }, + { + visitors: { + document: { + objects: { + Header: { $visitor: P_ } + } + } + } + } = $v; + const M_ = class header_HeaderVisitor extends P_ { + constructor(s) { + (super(s), (this.element = new Zv())); + } + }, + T_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Wv || (s(u) && o('callback', u) && i('object', u)) + ), + N_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Kv || (s(u) && o('components', u) && i('object', u)) + ), + R_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Hv || (s(u) && o('contact', u) && i('object', u)) + ), + D_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Yv || (s(u) && o('example', u) && i('object', u)) + ), + L_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Xv || (s(u) && o('externalDocumentation', u) && i('object', u)) + ), + B_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Zv || (s(u) && o('header', u) && i('object', u)) + ), + F_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Qv || (s(u) && o('info', u) && i('object', u)) + ), + q_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof eb || (s(u) && o('jsonSchemaDialect', u) && i('string', u)) + ), + $_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof tb || (s(u) && o('license', u) && i('object', u)) + ), + V_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof nb || (s(u) && o('link', u) && i('object', u)) + ), + U_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof _b || (s(u) && o('openapi', u) && i('string', u)) + ), + z_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i, hasClass: u }) => + (_) => + _ instanceof wb || (s(_) && o('openApi3_1', _) && i('object', _) && u('api', _)) + ), + W_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Sb || (s(u) && o('operation', u) && i('object', u)) + ), + K_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Ob || (s(u) && o('parameter', u) && i('object', u)) + ), + H_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Ab || (s(u) && o('pathItem', u) && i('object', u)) + ), + isPathItemElementExternal = (s) => { + if (!H_(s)) return !1; + if (!Ru(s.$ref)) return !1; + const o = serializers_value(s.$ref); + return 'string' == typeof o && o.length > 0 && !o.startsWith('#'); + }, + J_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Ib || (s(u) && o('paths', u) && i('object', u)) + ), + G_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Pb || (s(u) && o('reference', u) && i('object', u)) + ), + isReferenceElementExternal = (s) => { + if (!G_(s)) return !1; + if (!Ru(s.$ref)) return !1; + const o = serializers_value(s.$ref); + return 'string' == typeof o && o.length > 0 && !o.startsWith('#'); + }, + Y_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Mb || (s(u) && o('requestBody', u) && i('object', u)) + ), + X_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Rb || (s(u) && o('response', u) && i('object', u)) + ), + Z_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Lb || (s(u) && o('responses', u) && i('object', u)) + ), + Q_ = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof qb || (s(u) && o('schema', u) && i('object', u)) + ), + predicates_isBooleanJsonSchemaElement = (s) => + Bu(s) && s.classes.includes('boolean-json-schema'), + eE = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof zb || (s(u) && o('securityRequirement', u) && i('object', u)) + ), + tE = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof Qb || (s(u) && o('securityScheme', u) && i('object', u)) + ), + rE = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof e_ || (s(u) && o('server', u) && i('object', u)) + ), + nE = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof t_ || (s(u) && o('serverVariable', u) && i('object', u)) + ), + sE = helpers( + ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => + (u) => + u instanceof pb || (s(u) && o('mediaType', u) && i('object', u)) + ); + const oE = class ParentSchemaAwareVisitor_ParentSchemaAwareVisitor { + parent; + constructor({ parent: s }) { + this.parent = s; + } + }; + class open_api_3_1_schema_SchemaVisitor extends Mixin(im, oE, rm) { + constructor(s) { + (super(s), + (this.element = new qb()), + (this.specPath = Tl(['document', 'objects', 'Schema'])), + (this.canSupportSpecificationExtensions = !0), + (this.jsonSchemaDefaultDialect = eb.default), + this.passingOptionsNames.push('parent')); + } + ObjectElement(s) { + (this.handle$schema(s), this.handle$id(s), (this.parent = this.element)); + const o = im.prototype.ObjectElement.call(this, s); + return ( + Ru(this.element.$ref) && + (this.element.classes.push('reference-element'), + this.element.setMetaProperty('referenced-element', 'schema')), + o + ); + } + BooleanElement(s) { + const o = super.enter(s); + return (this.element.classes.push('boolean-json-schema'), o); + } + getJsonSchemaDialect() { + let s; + return ( + (s = + void 0 !== this.openApiSemanticElement && + q_(this.openApiSemanticElement.jsonSchemaDialect) + ? serializers_value(this.openApiSemanticElement.jsonSchemaDialect) + : void 0 !== this.openApiGenericElement && + Ru(this.openApiGenericElement.get('jsonSchemaDialect')) + ? serializers_value(this.openApiGenericElement.get('jsonSchemaDialect')) + : serializers_value(this.jsonSchemaDefaultDialect)), + s + ); + } + handle$schema(s) { + if (Rl(this.parent) && !Ru(s.get('$schema'))) + this.element.setMetaProperty('inherited$schema', this.getJsonSchemaDialect()); + else if (Q_(this.parent) && !Ru(s.get('$schema'))) { + const s = Na( + serializers_value(this.parent.meta.get('inherited$schema')), + serializers_value(this.parent.$schema) + ); + this.element.setMetaProperty('inherited$schema', s); + } + } + handle$id(s) { + const o = + void 0 !== this.parent + ? cloneDeep(this.parent.getMetaProperty('inherited$id', [])) + : new Cu.wE(), + i = serializers_value(s.get('$id')); + (Vd(i) && o.push(i), this.element.setMetaProperty('inherited$id', o)); + } + } + const iE = open_api_3_1_schema_SchemaVisitor; + const aE = class $vocabularyVisitor extends rm { + ObjectElement(s) { + const o = super.enter(s); + return (this.element.classes.push('json-schema-$vocabulary'), o); + } + }; + const lE = class $refVisitor extends rm { + StringElement(s) { + const o = super.enter(s); + return (this.element.classes.push('reference-value'), o); + } + }; + class $defsVisitor extends Mixin(vm, oE, rm) { + constructor(s) { + (super(s), + (this.element = new Cu.Sh()), + this.element.classes.push('json-schema-$defs'), + (this.specPath = Tl(['document', 'objects', 'Schema'])), + this.passingOptionsNames.push('parent')); + } + } + const cE = $defsVisitor; + class schema_AllOfVisitor_AllOfVisitor extends Mixin(nm, oE, rm) { + constructor(s) { + (super(s), + (this.element = new Cu.wE()), + this.element.classes.push('json-schema-allOf'), + this.passingOptionsNames.push('parent')); + } + ArrayElement(s) { + return ( + s.forEach((s) => { + if (Fu(s)) { + const o = this.toRefractedElement(['document', 'objects', 'Schema'], s); + this.element.push(o); + } else { + const o = cloneDeep(s); + this.element.push(o); + } + }), + this.copyMetaAndAttributes(s, this.element), + Ju + ); + } + } + const uE = schema_AllOfVisitor_AllOfVisitor; + class schema_AnyOfVisitor_AnyOfVisitor extends Mixin(nm, oE, rm) { + constructor(s) { + (super(s), + (this.element = new Cu.wE()), + this.element.classes.push('json-schema-anyOf'), + this.passingOptionsNames.push('parent')); + } + ArrayElement(s) { + return ( + s.forEach((s) => { + if (Fu(s)) { + const o = this.toRefractedElement(['document', 'objects', 'Schema'], s); + this.element.push(o); + } else { + const o = cloneDeep(s); + this.element.push(o); + } + }), + this.copyMetaAndAttributes(s, this.element), + Ju + ); + } + } + const pE = schema_AnyOfVisitor_AnyOfVisitor; + class schema_OneOfVisitor_OneOfVisitor extends Mixin(nm, oE, rm) { + constructor(s) { + (super(s), + (this.element = new Cu.wE()), + this.element.classes.push('json-schema-oneOf'), + this.passingOptionsNames.push('parent')); + } + ArrayElement(s) { + return ( + s.forEach((s) => { + if (Fu(s)) { + const o = this.toRefractedElement(['document', 'objects', 'Schema'], s); + this.element.push(o); + } else { + const o = cloneDeep(s); + this.element.push(o); + } + }), + this.copyMetaAndAttributes(s, this.element), + Ju + ); + } + } + const hE = schema_OneOfVisitor_OneOfVisitor; + class DependentSchemasVisitor extends Mixin(vm, oE, rm) { + constructor(s) { + (super(s), + (this.element = new Cu.Sh()), + this.element.classes.push('json-schema-dependentSchemas'), + (this.specPath = Tl(['document', 'objects', 'Schema'])), + this.passingOptionsNames.push('parent')); + } + } + const dE = DependentSchemasVisitor; + class PrefixItemsVisitor extends Mixin(nm, oE, rm) { + constructor(s) { + (super(s), + (this.element = new Cu.wE()), + this.element.classes.push('json-schema-prefixItems'), + this.passingOptionsNames.push('parent')); + } + ArrayElement(s) { + return ( + s.forEach((s) => { + if (Fu(s)) { + const o = this.toRefractedElement(['document', 'objects', 'Schema'], s); + this.element.push(o); + } else { + const o = cloneDeep(s); + this.element.push(o); + } + }), + this.copyMetaAndAttributes(s, this.element), + Ju + ); + } + } + const fE = PrefixItemsVisitor; + class schema_PropertiesVisitor_PropertiesVisitor extends Mixin(vm, oE, rm) { + constructor(s) { + (super(s), + (this.element = new Cu.Sh()), + this.element.classes.push('json-schema-properties'), + (this.specPath = Tl(['document', 'objects', 'Schema'])), + this.passingOptionsNames.push('parent')); + } + } + const mE = schema_PropertiesVisitor_PropertiesVisitor; + class PatternPropertiesVisitor_PatternPropertiesVisitor extends Mixin(vm, oE, rm) { + constructor(s) { + (super(s), + (this.element = new Cu.Sh()), + this.element.classes.push('json-schema-patternProperties'), + (this.specPath = Tl(['document', 'objects', 'Schema'])), + this.passingOptionsNames.push('parent')); + } + } + const gE = PatternPropertiesVisitor_PatternPropertiesVisitor; + const yE = class schema_TypeVisitor_TypeVisitor extends rm { + StringElement(s) { + const o = super.enter(s); + return (this.element.classes.push('json-schema-type'), o); + } + ArrayElement(s) { + const o = super.enter(s); + return (this.element.classes.push('json-schema-type'), o); + } + }; + const vE = class EnumVisitor_EnumVisitor extends rm { + ArrayElement(s) { + const o = super.enter(s); + return (this.element.classes.push('json-schema-enum'), o); + } + }; + const bE = class DependentRequiredVisitor extends rm { + ObjectElement(s) { + const o = super.enter(s); + return (this.element.classes.push('json-schema-dependentRequired'), o); + } + }; + const _E = class schema_ExamplesVisitor_ExamplesVisitor extends rm { + ArrayElement(s) { + const o = super.enter(s); + return (this.element.classes.push('json-schema-examples'), o); + } + }, + { + visitors: { + document: { + objects: { + Discriminator: { $visitor: EE } + } + } + } + } = $v; + const wE = class distriminator_DiscriminatorVisitor extends EE { + constructor(s) { + (super(s), (this.element = new Jv()), (this.canSupportSpecificationExtensions = !0)); + } + }, + { + visitors: { + document: { + objects: { + XML: { $visitor: SE } + } + } + } + } = $v; + const xE = class xml_XmlVisitor extends SE { + constructor(s) { + (super(s), (this.element = new n_())); + } + }; + class SchemasVisitor_SchemasVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new xy()), + (this.specPath = Tl(['document', 'objects', 'Schema']))); + } + } + const kE = SchemasVisitor_SchemasVisitor; + class ComponentsPathItems extends Cu.Sh { + static primaryClass = 'components-path-items'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(ComponentsPathItems.primaryClass)); + } + } + const CE = ComponentsPathItems; + class PathItemsVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new CE()), + (this.specPath = (s) => + isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'PathItem'])); + } + ObjectElement(s) { + const o = vm.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(G_).forEach((s) => { + s.setMetaProperty('referenced-element', 'pathItem'); + }), + o + ); + } + } + const OE = PathItemsVisitor, + { + visitors: { + document: { + objects: { + Example: { $visitor: AE } + } + } + } + } = $v; + const jE = class example_ExampleVisitor extends AE { + constructor(s) { + (super(s), (this.element = new Yv())); + } + }, + { + visitors: { + document: { + objects: { + ExternalDocumentation: { $visitor: IE } + } + } + } + } = $v; + const PE = class external_documentation_ExternalDocumentationVisitor extends IE { + constructor(s) { + (super(s), (this.element = new Xv())); + } + }, + { + visitors: { + document: { + objects: { + Encoding: { $visitor: ME } + } + } + } + } = $v; + const TE = class open_api_3_1_encoding_EncodingVisitor extends ME { + constructor(s) { + (super(s), (this.element = new Gv())); + } + }, + { + visitors: { + document: { + objects: { + Paths: { $visitor: NE } + } + } + } + } = $v; + const RE = class paths_PathsVisitor extends NE { + constructor(s) { + (super(s), (this.element = new Ib())); + } + }, + { + visitors: { + document: { + objects: { + RequestBody: { $visitor: DE } + } + } + } + } = $v; + const LE = class request_body_RequestBodyVisitor extends DE { + constructor(s) { + (super(s), (this.element = new Mb())); + } + }, + { + visitors: { + document: { + objects: { + Callback: { $visitor: BE } + } + } + } + } = $v; + const FE = class callback_CallbackVisitor extends BE { + constructor(s) { + (super(s), + (this.element = new Wv()), + (this.specPath = (s) => + isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'PathItem'])); + } + ObjectElement(s) { + const o = BE.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(G_).forEach((s) => { + s.setMetaProperty('referenced-element', 'pathItem'); + }), + o + ); + } + }, + { + visitors: { + document: { + objects: { + Response: { $visitor: qE } + } + } + } + } = $v; + const $E = class response_ResponseVisitor extends qE { + constructor(s) { + (super(s), (this.element = new Rb())); + } + }, + { + visitors: { + document: { + objects: { + Responses: { $visitor: VE } + } + } + } + } = $v; + const UE = class open_api_3_1_responses_ResponsesVisitor extends VE { + constructor(s) { + (super(s), (this.element = new Lb())); + } + }, + { + visitors: { + document: { + objects: { + Operation: { $visitor: zE } + } + } + } + } = $v; + const WE = class operation_OperationVisitor extends zE { + constructor(s) { + (super(s), (this.element = new Sb())); + } + }, + { + visitors: { + document: { + objects: { + PathItem: { $visitor: KE } + } + } + } + } = $v; + const HE = class path_item_PathItemVisitor extends KE { + constructor(s) { + (super(s), (this.element = new Ab())); + } + }, + { + visitors: { + document: { + objects: { + SecurityScheme: { $visitor: JE } + } + } + } + } = $v; + const GE = class security_scheme_SecuritySchemeVisitor extends JE { + constructor(s) { + (super(s), (this.element = new Qb())); + } + }, + { + visitors: { + document: { + objects: { + OAuthFlows: { $visitor: YE } + } + } + } + } = $v; + const XE = class oauth_flows_OAuthFlowsVisitor extends YE { + constructor(s) { + (super(s), (this.element = new yb())); + } + }, + { + visitors: { + document: { + objects: { + OAuthFlow: { $visitor: ZE } + } + } + } + } = $v; + const QE = class oauth_flow_OAuthFlowVisitor extends ZE { + constructor(s) { + (super(s), (this.element = new mb())); + } + }; + class Webhooks extends Cu.Sh { + static primaryClass = 'webhooks'; + constructor(s, o, i) { + (super(s, o, i), this.classes.push(Webhooks.primaryClass)); + } + } + const ew = Webhooks; + class WebhooksVisitor extends Mixin(vm, rm) { + constructor(s) { + (super(s), + (this.element = new ew()), + (this.specPath = (s) => + isReferenceLikeElement(s) + ? ['document', 'objects', 'Reference'] + : ['document', 'objects', 'PathItem'])); + } + ObjectElement(s) { + const o = vm.prototype.ObjectElement.call(this, s); + return ( + this.element.filter(G_).forEach((s) => { + s.setMetaProperty('referenced-element', 'pathItem'); + }), + this.element.filter(H_).forEach((s, o) => { + s.setMetaProperty('webhook-name', serializers_value(o)); + }), + o + ); + } + } + const tw = WebhooksVisitor, + rw = { + visitors: { + value: $v.visitors.value, + document: { + objects: { + OpenApi: { + $visitor: s_, + fixedFields: { + openapi: $v.visitors.document.objects.OpenApi.fixedFields.openapi, + info: { $ref: '#/visitors/document/objects/Info' }, + jsonSchemaDialect: d_, + servers: $v.visitors.document.objects.OpenApi.fixedFields.servers, + paths: { $ref: '#/visitors/document/objects/Paths' }, + webhooks: tw, + components: { $ref: '#/visitors/document/objects/Components' }, + security: $v.visitors.document.objects.OpenApi.fixedFields.security, + tags: $v.visitors.document.objects.OpenApi.fixedFields.tags, + externalDocs: { $ref: '#/visitors/document/objects/ExternalDocumentation' } + } + }, + Info: { + $visitor: i_, + fixedFields: { + title: $v.visitors.document.objects.Info.fixedFields.title, + description: $v.visitors.document.objects.Info.fixedFields.description, + summary: { $ref: '#/visitors/value' }, + termsOfService: $v.visitors.document.objects.Info.fixedFields.termsOfService, + contact: { $ref: '#/visitors/document/objects/Contact' }, + license: { $ref: '#/visitors/document/objects/License' }, + version: $v.visitors.document.objects.Info.fixedFields.version + } + }, + Contact: { + $visitor: l_, + fixedFields: { + name: $v.visitors.document.objects.Contact.fixedFields.name, + url: $v.visitors.document.objects.Contact.fixedFields.url, + email: $v.visitors.document.objects.Contact.fixedFields.email + } + }, + License: { + $visitor: u_, + fixedFields: { + name: $v.visitors.document.objects.License.fixedFields.name, + identifier: { $ref: '#/visitors/value' }, + url: $v.visitors.document.objects.License.fixedFields.url + } + }, + Server: { + $visitor: m_, + fixedFields: { + url: $v.visitors.document.objects.Server.fixedFields.url, + description: $v.visitors.document.objects.Server.fixedFields.description, + variables: $v.visitors.document.objects.Server.fixedFields.variables + } + }, + ServerVariable: { + $visitor: y_, + fixedFields: { + enum: $v.visitors.document.objects.ServerVariable.fixedFields.enum, + default: $v.visitors.document.objects.ServerVariable.fixedFields.default, + description: + $v.visitors.document.objects.ServerVariable.fixedFields.description + } + }, + Components: { + $visitor: x_, + fixedFields: { + schemas: kE, + responses: $v.visitors.document.objects.Components.fixedFields.responses, + parameters: $v.visitors.document.objects.Components.fixedFields.parameters, + examples: $v.visitors.document.objects.Components.fixedFields.examples, + requestBodies: + $v.visitors.document.objects.Components.fixedFields.requestBodies, + headers: $v.visitors.document.objects.Components.fixedFields.headers, + securitySchemes: + $v.visitors.document.objects.Components.fixedFields.securitySchemes, + links: $v.visitors.document.objects.Components.fixedFields.links, + callbacks: $v.visitors.document.objects.Components.fixedFields.callbacks, + pathItems: OE + } + }, + Paths: { $visitor: RE }, + PathItem: { + $visitor: HE, + fixedFields: { + $ref: $v.visitors.document.objects.PathItem.fixedFields.$ref, + summary: $v.visitors.document.objects.PathItem.fixedFields.summary, + description: $v.visitors.document.objects.PathItem.fixedFields.description, + get: { $ref: '#/visitors/document/objects/Operation' }, + put: { $ref: '#/visitors/document/objects/Operation' }, + post: { $ref: '#/visitors/document/objects/Operation' }, + delete: { $ref: '#/visitors/document/objects/Operation' }, + options: { $ref: '#/visitors/document/objects/Operation' }, + head: { $ref: '#/visitors/document/objects/Operation' }, + patch: { $ref: '#/visitors/document/objects/Operation' }, + trace: { $ref: '#/visitors/document/objects/Operation' }, + servers: $v.visitors.document.objects.PathItem.fixedFields.servers, + parameters: $v.visitors.document.objects.PathItem.fixedFields.parameters + } + }, + Operation: { + $visitor: WE, + fixedFields: { + tags: $v.visitors.document.objects.Operation.fixedFields.tags, + summary: $v.visitors.document.objects.Operation.fixedFields.summary, + description: $v.visitors.document.objects.Operation.fixedFields.description, + externalDocs: { $ref: '#/visitors/document/objects/ExternalDocumentation' }, + operationId: $v.visitors.document.objects.Operation.fixedFields.operationId, + parameters: $v.visitors.document.objects.Operation.fixedFields.parameters, + requestBody: $v.visitors.document.objects.Operation.fixedFields.requestBody, + responses: { $ref: '#/visitors/document/objects/Responses' }, + callbacks: $v.visitors.document.objects.Operation.fixedFields.callbacks, + deprecated: $v.visitors.document.objects.Operation.fixedFields.deprecated, + security: $v.visitors.document.objects.Operation.fixedFields.security, + servers: $v.visitors.document.objects.Operation.fixedFields.servers + } + }, + ExternalDocumentation: { + $visitor: PE, + fixedFields: { + description: + $v.visitors.document.objects.ExternalDocumentation.fixedFields.description, + url: $v.visitors.document.objects.ExternalDocumentation.fixedFields.url + } + }, + Parameter: { + $visitor: I_, + fixedFields: { + name: $v.visitors.document.objects.Parameter.fixedFields.name, + in: $v.visitors.document.objects.Parameter.fixedFields.in, + description: $v.visitors.document.objects.Parameter.fixedFields.description, + required: $v.visitors.document.objects.Parameter.fixedFields.required, + deprecated: $v.visitors.document.objects.Parameter.fixedFields.deprecated, + allowEmptyValue: + $v.visitors.document.objects.Parameter.fixedFields.allowEmptyValue, + style: $v.visitors.document.objects.Parameter.fixedFields.style, + explode: $v.visitors.document.objects.Parameter.fixedFields.explode, + allowReserved: + $v.visitors.document.objects.Parameter.fixedFields.allowReserved, + schema: { $ref: '#/visitors/document/objects/Schema' }, + example: $v.visitors.document.objects.Parameter.fixedFields.example, + examples: $v.visitors.document.objects.Parameter.fixedFields.examples, + content: $v.visitors.document.objects.Parameter.fixedFields.content + } + }, + RequestBody: { + $visitor: LE, + fixedFields: { + description: $v.visitors.document.objects.RequestBody.fixedFields.description, + content: $v.visitors.document.objects.RequestBody.fixedFields.content, + required: $v.visitors.document.objects.RequestBody.fixedFields.required + } + }, + MediaType: { + $visitor: b_, + fixedFields: { + schema: { $ref: '#/visitors/document/objects/Schema' }, + example: $v.visitors.document.objects.MediaType.fixedFields.example, + examples: $v.visitors.document.objects.MediaType.fixedFields.examples, + encoding: $v.visitors.document.objects.MediaType.fixedFields.encoding + } + }, + Encoding: { + $visitor: TE, + fixedFields: { + contentType: $v.visitors.document.objects.Encoding.fixedFields.contentType, + headers: $v.visitors.document.objects.Encoding.fixedFields.headers, + style: $v.visitors.document.objects.Encoding.fixedFields.style, + explode: $v.visitors.document.objects.Encoding.fixedFields.explode, + allowReserved: $v.visitors.document.objects.Encoding.fixedFields.allowReserved + } + }, + Responses: { + $visitor: UE, + fixedFields: { + default: $v.visitors.document.objects.Responses.fixedFields.default + } + }, + Response: { + $visitor: $E, + fixedFields: { + description: $v.visitors.document.objects.Response.fixedFields.description, + headers: $v.visitors.document.objects.Response.fixedFields.headers, + content: $v.visitors.document.objects.Response.fixedFields.content, + links: $v.visitors.document.objects.Response.fixedFields.links + } + }, + Callback: { $visitor: FE }, + Example: { + $visitor: jE, + fixedFields: { + summary: $v.visitors.document.objects.Example.fixedFields.summary, + description: $v.visitors.document.objects.Example.fixedFields.description, + value: $v.visitors.document.objects.Example.fixedFields.value, + externalValue: $v.visitors.document.objects.Example.fixedFields.externalValue + } + }, + Link: { + $visitor: h_, + fixedFields: { + operationRef: $v.visitors.document.objects.Link.fixedFields.operationRef, + operationId: $v.visitors.document.objects.Link.fixedFields.operationId, + parameters: $v.visitors.document.objects.Link.fixedFields.parameters, + requestBody: $v.visitors.document.objects.Link.fixedFields.requestBody, + description: $v.visitors.document.objects.Link.fixedFields.description, + server: { $ref: '#/visitors/document/objects/Server' } + } + }, + Header: { + $visitor: M_, + fixedFields: { + description: $v.visitors.document.objects.Header.fixedFields.description, + required: $v.visitors.document.objects.Header.fixedFields.required, + deprecated: $v.visitors.document.objects.Header.fixedFields.deprecated, + allowEmptyValue: + $v.visitors.document.objects.Header.fixedFields.allowEmptyValue, + style: $v.visitors.document.objects.Header.fixedFields.style, + explode: $v.visitors.document.objects.Header.fixedFields.explode, + allowReserved: $v.visitors.document.objects.Header.fixedFields.allowReserved, + schema: { $ref: '#/visitors/document/objects/Schema' }, + example: $v.visitors.document.objects.Header.fixedFields.example, + examples: $v.visitors.document.objects.Header.fixedFields.examples, + content: $v.visitors.document.objects.Header.fixedFields.content + } + }, + Tag: { + $visitor: C_, + fixedFields: { + name: $v.visitors.document.objects.Tag.fixedFields.name, + description: $v.visitors.document.objects.Tag.fixedFields.description, + externalDocs: { $ref: '#/visitors/document/objects/ExternalDocumentation' } + } + }, + Reference: { + $visitor: A_, + fixedFields: { + $ref: $v.visitors.document.objects.Reference.fixedFields.$ref, + summary: { $ref: '#/visitors/value' }, + description: { $ref: '#/visitors/value' } + } + }, + Schema: { + $visitor: iE, + fixedFields: { + $schema: { $ref: '#/visitors/value' }, + $vocabulary: aE, + $id: { $ref: '#/visitors/value' }, + $anchor: { $ref: '#/visitors/value' }, + $dynamicAnchor: { $ref: '#/visitors/value' }, + $dynamicRef: { $ref: '#/visitors/value' }, + $ref: lE, + $defs: cE, + $comment: { $ref: '#/visitors/value' }, + allOf: uE, + anyOf: pE, + oneOf: hE, + not: { $ref: '#/visitors/document/objects/Schema' }, + if: { $ref: '#/visitors/document/objects/Schema' }, + then: { $ref: '#/visitors/document/objects/Schema' }, + else: { $ref: '#/visitors/document/objects/Schema' }, + dependentSchemas: dE, + prefixItems: fE, + items: { $ref: '#/visitors/document/objects/Schema' }, + contains: { $ref: '#/visitors/document/objects/Schema' }, + properties: mE, + patternProperties: gE, + additionalProperties: { $ref: '#/visitors/document/objects/Schema' }, + propertyNames: { $ref: '#/visitors/document/objects/Schema' }, + unevaluatedItems: { $ref: '#/visitors/document/objects/Schema' }, + unevaluatedProperties: { $ref: '#/visitors/document/objects/Schema' }, + type: yE, + enum: vE, + const: { $ref: '#/visitors/value' }, + multipleOf: { $ref: '#/visitors/value' }, + maximum: { $ref: '#/visitors/value' }, + exclusiveMaximum: { $ref: '#/visitors/value' }, + minimum: { $ref: '#/visitors/value' }, + exclusiveMinimum: { $ref: '#/visitors/value' }, + maxLength: { $ref: '#/visitors/value' }, + minLength: { $ref: '#/visitors/value' }, + pattern: { $ref: '#/visitors/value' }, + maxItems: { $ref: '#/visitors/value' }, + minItems: { $ref: '#/visitors/value' }, + uniqueItems: { $ref: '#/visitors/value' }, + maxContains: { $ref: '#/visitors/value' }, + minContains: { $ref: '#/visitors/value' }, + maxProperties: { $ref: '#/visitors/value' }, + minProperties: { $ref: '#/visitors/value' }, + required: { $ref: '#/visitors/value' }, + dependentRequired: bE, + title: { $ref: '#/visitors/value' }, + description: { $ref: '#/visitors/value' }, + default: { $ref: '#/visitors/value' }, + deprecated: { $ref: '#/visitors/value' }, + readOnly: { $ref: '#/visitors/value' }, + writeOnly: { $ref: '#/visitors/value' }, + examples: _E, + format: { $ref: '#/visitors/value' }, + contentEncoding: { $ref: '#/visitors/value' }, + contentMediaType: { $ref: '#/visitors/value' }, + contentSchema: { $ref: '#/visitors/document/objects/Schema' }, + discriminator: { $ref: '#/visitors/document/objects/Discriminator' }, + xml: { $ref: '#/visitors/document/objects/XML' }, + externalDocs: { $ref: '#/visitors/document/objects/ExternalDocumentation' }, + example: { $ref: '#/visitors/value' } + } + }, + Discriminator: { + $visitor: wE, + fixedFields: { + propertyName: + $v.visitors.document.objects.Discriminator.fixedFields.propertyName, + mapping: $v.visitors.document.objects.Discriminator.fixedFields.mapping + } + }, + XML: { + $visitor: xE, + fixedFields: { + name: $v.visitors.document.objects.XML.fixedFields.name, + namespace: $v.visitors.document.objects.XML.fixedFields.namespace, + prefix: $v.visitors.document.objects.XML.fixedFields.prefix, + attribute: $v.visitors.document.objects.XML.fixedFields.attribute, + wrapped: $v.visitors.document.objects.XML.fixedFields.wrapped + } + }, + SecurityScheme: { + $visitor: GE, + fixedFields: { + type: $v.visitors.document.objects.SecurityScheme.fixedFields.type, + description: + $v.visitors.document.objects.SecurityScheme.fixedFields.description, + name: $v.visitors.document.objects.SecurityScheme.fixedFields.name, + in: $v.visitors.document.objects.SecurityScheme.fixedFields.in, + scheme: $v.visitors.document.objects.SecurityScheme.fixedFields.scheme, + bearerFormat: + $v.visitors.document.objects.SecurityScheme.fixedFields.bearerFormat, + flows: { $ref: '#/visitors/document/objects/OAuthFlows' }, + openIdConnectUrl: + $v.visitors.document.objects.SecurityScheme.fixedFields.openIdConnectUrl + } + }, + OAuthFlows: { + $visitor: XE, + fixedFields: { + implicit: { $ref: '#/visitors/document/objects/OAuthFlow' }, + password: { $ref: '#/visitors/document/objects/OAuthFlow' }, + clientCredentials: { $ref: '#/visitors/document/objects/OAuthFlow' }, + authorizationCode: { $ref: '#/visitors/document/objects/OAuthFlow' } + } + }, + OAuthFlow: { + $visitor: QE, + fixedFields: { + authorizationUrl: + $v.visitors.document.objects.OAuthFlow.fixedFields.authorizationUrl, + tokenUrl: $v.visitors.document.objects.OAuthFlow.fixedFields.tokenUrl, + refreshUrl: $v.visitors.document.objects.OAuthFlow.fixedFields.refreshUrl, + scopes: $v.visitors.document.objects.OAuthFlow.fixedFields.scopes + } + }, + SecurityRequirement: { $visitor: w_ } + }, + extension: { $visitor: $v.visitors.document.extension.$visitor } + } + } + }, + apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType = (s) => { + if (Nu(s)) return `${s.element.charAt(0).toUpperCase() + s.element.slice(1)}Element`; + }, + nw = { + CallbackElement: ['content'], + ComponentsElement: ['content'], + ContactElement: ['content'], + DiscriminatorElement: ['content'], + Encoding: ['content'], + Example: ['content'], + ExternalDocumentationElement: ['content'], + HeaderElement: ['content'], + InfoElement: ['content'], + LicenseElement: ['content'], + MediaTypeElement: ['content'], + OAuthFlowElement: ['content'], + OAuthFlowsElement: ['content'], + OpenApi3_1Element: ['content'], + OperationElement: ['content'], + ParameterElement: ['content'], + PathItemElement: ['content'], + PathsElement: ['content'], + ReferenceElement: ['content'], + RequestBodyElement: ['content'], + ResponseElement: ['content'], + ResponsesElement: ['content'], + SchemaElement: ['content'], + SecurityRequirementElement: ['content'], + SecuritySchemeElement: ['content'], + ServerElement: ['content'], + ServerVariableElement: ['content'], + TagElement: ['content'], + ...Qu + }, + sw = { + namespace: (s) => { + const { base: o } = s; + return ( + o.register('callback', Wv), + o.register('components', Kv), + o.register('contact', Hv), + o.register('discriminator', Jv), + o.register('encoding', Gv), + o.register('example', Yv), + o.register('externalDocumentation', Xv), + o.register('header', Zv), + o.register('info', Qv), + o.register('jsonSchemaDialect', eb), + o.register('license', tb), + o.register('link', nb), + o.register('mediaType', pb), + o.register('oAuthFlow', mb), + o.register('oAuthFlows', yb), + o.register('openapi', _b), + o.register('openApi3_1', wb), + o.register('operation', Sb), + o.register('parameter', Ob), + o.register('pathItem', Ab), + o.register('paths', Ib), + o.register('reference', Pb), + o.register('requestBody', Mb), + o.register('response', Rb), + o.register('responses', Lb), + o.register('schema', qb), + o.register('securityRequirement', zb), + o.register('securityScheme', Qb), + o.register('server', e_), + o.register('serverVariable', t_), + o.register('tag', r_), + o.register('xml', n_), + o + ); + } + }, + ow = sw, + ancestorLineageToJSONPointer = (s) => { + const o = s.reduce((o, i, u) => { + if ($u(i)) { + const s = String(serializers_value(i.key)); + o.push(s); + } else if (qu(s[u - 2])) { + const _ = String(s[u - 2].content.indexOf(i)); + o.push(_); + } + return o; + }, []); + return es_compile(o); + }, + apidom_ns_openapi_3_1_es_refractor_toolbox = () => { + const s = createNamespace(ow); + return { + predicates: { + ...de, + isElement: Nu, + isStringElement: Ru, + isArrayElement: qu, + isObjectElement: Fu, + isMemberElement: $u, + isServersElement: rg, + includesClasses, + hasElementSourceMap + }, + ancestorLineageToJSONPointer, + namespace: s + }; + }, + apidom_ns_openapi_3_1_es_refractor_refract = ( + s, + { + specPath: o = ['visitors', 'document', 'objects', 'OpenApi', '$visitor'], + plugins: i = [] + } = {} + ) => { + const u = (0, Cu.e)(s), + _ = dereference(rw), + w = new (cp(o, _))({ specObj: _ }); + return ( + visitor_visit(u, w), + dispatchPluginsSync(w.element, i, { + toolboxCreator: apidom_ns_openapi_3_1_es_refractor_toolbox, + visitorOptions: { + keyMap: nw, + nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType + } + }) + ); + }, + apidom_ns_openapi_3_1_es_refractor_createRefractor = + (s) => + (o, i = {}) => + apidom_ns_openapi_3_1_es_refractor_refract(o, { specPath: s, ...i }); + ((Wv.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Callback', + '$visitor' + ])), + (Kv.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Components', + '$visitor' + ])), + (Hv.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Contact', + '$visitor' + ])), + (Yv.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Example', + '$visitor' + ])), + (Jv.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Discriminator', + '$visitor' + ])), + (Gv.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Encoding', + '$visitor' + ])), + (Xv.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'ExternalDocumentation', + '$visitor' + ])), + (Zv.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Header', + '$visitor' + ])), + (Qv.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Info', + '$visitor' + ])), + (eb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'OpenApi', + 'fixedFields', + 'jsonSchemaDialect' + ])), + (tb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'License', + '$visitor' + ])), + (nb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Link', + '$visitor' + ])), + (pb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'MediaType', + '$visitor' + ])), + (mb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'OAuthFlow', + '$visitor' + ])), + (yb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'OAuthFlows', + '$visitor' + ])), + (_b.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'OpenApi', + 'fixedFields', + 'openapi' + ])), + (wb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'OpenApi', + '$visitor' + ])), + (Sb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Operation', + '$visitor' + ])), + (Ob.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Parameter', + '$visitor' + ])), + (Ab.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'PathItem', + '$visitor' + ])), + (Ib.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Paths', + '$visitor' + ])), + (Pb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Reference', + '$visitor' + ])), + (Mb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'RequestBody', + '$visitor' + ])), + (Rb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Response', + '$visitor' + ])), + (Lb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Responses', + '$visitor' + ])), + (qb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Schema', + '$visitor' + ])), + (zb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'SecurityRequirement', + '$visitor' + ])), + (Qb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'SecurityScheme', + '$visitor' + ])), + (e_.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Server', + '$visitor' + ])), + (t_.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'ServerVariable', + '$visitor' + ])), + (r_.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'Tag', + '$visitor' + ])), + (n_.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ + 'visitors', + 'document', + 'objects', + 'XML', + '$visitor' + ]))); + const iw = class NotImplementedError extends Hh {}; + const aw = class MediaTypes extends Array { + unknownMediaType = 'application/octet-stream'; + filterByFormat() { + throw new iw('filterByFormat method in MediaTypes class is not yet implemented.'); + } + findBy() { + throw new iw('findBy method in MediaTypes class is not yet implemented.'); + } + latest() { + throw new iw('latest method in MediaTypes class is not yet implemented.'); + } + }; + class OpenAPIMediaTypes extends aw { + filterByFormat(s = 'generic') { + const o = 'generic' === s ? 'openapi;version' : s; + return this.filter((s) => s.includes(o)); + } + findBy(s = '3.1.0', o = 'generic') { + const i = + 'generic' === o + ? `vnd.oai.openapi;version=${s}` + : `vnd.oai.openapi+${o};version=${s}`; + return this.find((s) => s.includes(i)) || this.unknownMediaType; + } + latest(s = 'generic') { + return Fa(this.filterByFormat(s)); + } + } + const lw = new OpenAPIMediaTypes( + 'application/vnd.oai.openapi;version=3.1.0', + 'application/vnd.oai.openapi+json;version=3.1.0', + 'application/vnd.oai.openapi+yaml;version=3.1.0' + ); + const cw = class es_Reference_Reference { + uri; + depth; + value; + refSet; + errors; + constructor({ uri: s, depth: o = 0, refSet: i, value: u }) { + ((this.uri = s), + (this.value = u), + (this.depth = o), + (this.refSet = i), + (this.errors = [])); + } + }; + const uw = class ReferenceSet { + rootRef; + refs; + circular; + constructor({ refs: s = [], circular: o = !1 } = {}) { + ((this.refs = []), (this.circular = o), s.forEach(this.add.bind(this))); + } + get size() { + return this.refs.length; + } + add(s) { + return ( + this.has(s) || + (this.refs.push(s), + (this.rootRef = void 0 === this.rootRef ? s : this.rootRef), + (s.refSet = this)), + this + ); + } + merge(s) { + for (const o of s.values()) this.add(o); + return this; + } + has(s) { + const o = Yl(s) ? s : s.uri; + return Dl(this.find((s) => s.uri === o)); + } + find(s) { + return this.refs.find(s); + } + *values() { + yield* this.refs; + } + clean() { + (this.refs.forEach((s) => { + s.refSet = void 0; + }), + (this.rootRef = void 0), + (this.refs.length = 0)); + } + }, + pw = { + parse: { mediaType: 'text/plain', parsers: [], parserOpts: {} }, + resolve: { + baseURI: '', + resolvers: [], + resolverOpts: {}, + strategies: [], + strategyOpts: {}, + internal: !0, + external: !0, + maxDepth: 1 / 0 + }, + dereference: { + strategies: [], + strategyOpts: {}, + refSet: null, + maxDepth: 1 / 0, + circular: 'ignore', + circularReplacer: Ip, + immutable: !0 + }, + bundle: { strategies: [], refSet: null, maxDepth: 1 / 0 } + }; + const hw = _curry2(function lens(s, o) { + return function (i) { + return function (u) { + return kl( + function (s) { + return o(s, u); + }, + i(s(u)) + ); + }; + }; + }); + var dw = _curry3(function assocPath(s, o, i) { + if (0 === s.length) return o; + var u = s[0]; + if (s.length > 1) { + var _ = !ld(i) && _has(u, i) && 'object' == typeof i[u] ? i[u] : Yo(s[1]) ? [] : {}; + o = assocPath(Array.prototype.slice.call(s, 1), o, _); + } + return (function _assoc(s, o, i) { + if (Yo(s) && aa(i)) { + var u = [].concat(i); + return ((u[s] = o), u); + } + var _ = {}; + for (var w in i) _[w] = i[w]; + return ((_[s] = o), _); + })(u, o, i); + }); + const fw = dw; + var Identity = function (s) { + return { + value: s, + map: function (o) { + return Identity(o(s)); + } + }; + }, + mw = _curry3(function over(s, o, i) { + return s(function (s) { + return Identity(o(s)); + })(i).value; + }); + const gw = mw, + yw = hw(cp(['resolve', 'baseURI']), fw(['resolve', 'baseURI'])), + baseURIDefault = (s) => (qp(s) ? url_cwd() : s), + util_merge = (s, o) => { + const i = lp(s, o); + return gw(yw, baseURIDefault, i); + }; + const vw = class File_File { + uri; + mediaType; + data; + parseResult; + constructor({ uri: s, mediaType: o = 'text/plain', data: i, parseResult: u }) { + ((this.uri = s), (this.mediaType = o), (this.data = i), (this.parseResult = u)); + } + get extension() { + return Yl(this.uri) + ? ((s) => { + const o = s.lastIndexOf('.'); + return o >= 0 ? s.substring(o).toLowerCase() : ''; + })(this.uri) + : ''; + } + toString() { + if ('string' == typeof this.data) return this.data; + if ( + this.data instanceof ArrayBuffer || + ['ArrayBuffer'].includes(ea(this.data)) || + ArrayBuffer.isView(this.data) + ) { + return new TextDecoder('utf-8').decode(this.data); + } + return String(this.data); + } + }; + const bw = class PluginError extends Ho { + plugin; + constructor(s, o) { + (super(s, { cause: o.cause }), (this.plugin = o.plugin)); + } + }, + plugins_filter = async (s, o, i) => { + const u = await Promise.all(i.map(_p([s], o))); + return i.filter((s, o) => u[o]); + }, + run = async (s, o, i) => { + let u; + for (const _ of i) + try { + const i = await _[s].call(_, ...o); + return { plugin: _, result: i }; + } catch (s) { + u = new bw('Error while running plugin', { cause: s, plugin: _ }); + } + return Promise.reject(u); + }; + const _w = class DereferenceError extends Ho {}; + const Ew = class UnmatchedDereferenceStrategyError extends _w {}, + dereferenceApiDOM = async (s, o) => { + let i = s, + u = !1; + if (!Ku(s)) { + const o = cloneShallow(s); + (o.classes.push('result'), (i = new Mu([o])), (u = !0)); + } + const _ = new vw({ + uri: o.resolve.baseURI, + parseResult: i, + mediaType: o.parse.mediaType + }), + w = await plugins_filter('canDereference', [_, o], o.dereference.strategies); + if (gp(w)) throw new Ew(_.uri); + try { + const { result: s } = await run('dereference', [_, o], w); + return u ? s.get(0) : s; + } catch (s) { + throw new _w(`Error while dereferencing file "${_.uri}"`, { cause: s }); + } + }; + const ww = class ParseError extends Ho {}; + const Sw = class ParserError extends ww {}; + const xw = class Parser { + name; + allowEmpty; + sourceMap; + fileExtensions; + mediaTypes; + constructor({ + name: s, + allowEmpty: o = !0, + sourceMap: i = !1, + fileExtensions: u = [], + mediaTypes: _ = [] + }) { + ((this.name = s), + (this.allowEmpty = o), + (this.sourceMap = i), + (this.fileExtensions = u), + (this.mediaTypes = _)); + } + }; + const kw = class BinaryParser extends xw { + constructor(s) { + super({ ...(null != s ? s : {}), name: 'binary' }); + } + canParse(s) { + return 0 === this.fileExtensions.length || this.fileExtensions.includes(s.extension); + } + parse(s) { + try { + const o = unescape(encodeURIComponent(s.toString())), + i = btoa(o), + u = new Mu(); + if (0 !== i.length) { + const s = new Cu.Om(i); + (s.classes.push('result'), u.push(s)); + } + return u; + } catch (o) { + throw new Sw(`Error parsing "${s.uri}"`, { cause: o }); + } + } + }; + const Cw = class ResolveStrategy { + name; + constructor({ name: s }) { + this.name = s; + } + }; + const Ow = class OpenAPI3_1ResolveStrategy extends Cw { + constructor(s) { + super({ ...(null != s ? s : {}), name: 'openapi-3-1' }); + } + canResolve(s, o) { + const i = o.dereference.strategies.find((s) => 'openapi-3-1' === s.name); + return void 0 !== i && i.canDereference(s, o); + } + async resolve(s, o) { + const i = o.dereference.strategies.find((s) => 'openapi-3-1' === s.name); + if (void 0 === i) throw new Ew('"openapi-3-1" dereference strategy is not available.'); + const u = new uw(), + _ = util_merge(o, { resolve: { internal: !1 }, dereference: { refSet: u } }); + return (await i.dereference(s, _), u); + } + }; + const Aw = class Resolver { + name; + constructor({ name: s }) { + this.name = s; + } + }; + const jw = class HTTPResolver extends Aw { + timeout; + redirects; + withCredentials; + constructor(s) { + const { + name: o = 'http-resolver', + timeout: i = 5e3, + redirects: u = 5, + withCredentials: _ = !1 + } = null != s ? s : {}; + (super({ name: o }), + (this.timeout = i), + (this.redirects = u), + (this.withCredentials = _)); + } + canRead(s) { + return isHttpUrl(s.uri); + } + }; + const Iw = class ResolveError extends Ho {}; + const Pw = class ResolverError extends Iw {}, + { AbortController: Mw, AbortSignal: Tw } = globalThis; + (void 0 === globalThis.AbortController && (globalThis.AbortController = Mw), + void 0 === globalThis.AbortSignal && (globalThis.AbortSignal = Tw)); + const Nw = class HTTPResolverSwaggerClient extends jw { + swaggerHTTPClient = http_http; + swaggerHTTPClientConfig; + constructor({ + swaggerHTTPClient: s = http_http, + swaggerHTTPClientConfig: o = {}, + ...i + } = {}) { + (super({ ...i, name: 'http-swagger-client' }), + (this.swaggerHTTPClient = s), + (this.swaggerHTTPClientConfig = o)); + } + getHttpClient() { + return this.swaggerHTTPClient; + } + async read(s) { + const o = this.getHttpClient(), + i = new AbortController(), + { signal: u } = i, + _ = setTimeout(() => { + i.abort(); + }, this.timeout), + w = + this.getHttpClient().withCredentials || this.withCredentials + ? 'include' + : 'same-origin', + x = 0 === this.redirects ? 'error' : 'follow', + C = this.redirects > 0 ? this.redirects : void 0; + try { + return ( + await o({ + url: s.uri, + signal: u, + userFetch: async (s, o) => { + let i = await fetch(s, o); + try { + i.headers.delete('Content-Type'); + } catch { + ((i = new Response(i.body, { ...i, headers: new Headers(i.headers) })), + i.headers.delete('Content-Type')); + } + return i; + }, + credentials: w, + redirect: x, + follow: C, + ...this.swaggerHTTPClientConfig + }) + ).text.arrayBuffer(); + } catch (o) { + throw new Pw(`Error downloading "${s.uri}"`, { cause: o }); + } finally { + clearTimeout(_); + } + } + }, + from = (s, o = wp) => { + if (Yl(s)) + try { + return o.fromRefract(JSON.parse(s)); + } catch {} + return ku(s) && md('element', s) ? o.fromRefract(s) : o.toElement(s); + }; + const Rw = class JSONParser extends xw { + constructor(s = {}) { + super({ name: 'json-swagger-client', mediaTypes: ['application/json'], ...s }); + } + async canParse(s) { + const o = 0 === this.fileExtensions.length || this.fileExtensions.includes(s.extension), + i = this.mediaTypes.includes(s.mediaType); + if (!o) return !1; + if (i) return !0; + if (!i) + try { + return (JSON.parse(s.toString()), !0); + } catch (s) { + return !1; + } + return !1; + } + async parse(s) { + if (this.sourceMap) + throw new Sw("json-swagger-client parser plugin doesn't support sourceMaps option"); + const o = new Mu(), + i = s.toString(); + if (this.allowEmpty && '' === i.trim()) return o; + try { + const s = from(JSON.parse(i)); + return (s.classes.push('result'), o.push(s), o); + } catch (o) { + throw new Sw(`Error parsing "${s.uri}"`, { cause: o }); + } + } + }; + const Dw = class YAMLParser extends xw { + constructor(s = {}) { + super({ + name: 'yaml-1-2-swagger-client', + mediaTypes: ['text/yaml', 'application/yaml'], + ...s + }); + } + async canParse(s) { + const o = 0 === this.fileExtensions.length || this.fileExtensions.includes(s.extension), + i = this.mediaTypes.includes(s.mediaType); + if (!o) return !1; + if (i) return !0; + if (!i) + try { + return (mn.load(s.toString(), { schema: nn }), !0); + } catch (s) { + return !1; + } + return !1; + } + async parse(s) { + if (this.sourceMap) + throw new Sw( + "yaml-1-2-swagger-client parser plugin doesn't support sourceMaps option" + ); + const o = new Mu(), + i = s.toString(); + try { + const s = mn.load(i, { schema: nn }); + if (this.allowEmpty && void 0 === s) return o; + const u = from(s); + return (u.classes.push('result'), o.push(u), o); + } catch (o) { + throw new Sw(`Error parsing "${s.uri}"`, { cause: o }); + } + } + }; + const Lw = class OpenAPIJSON3_1Parser extends xw { + detectionRegExp = /"openapi"\s*:\s*"(?3\.1\.(?:[1-9]\d*|0))"/; + constructor(s = {}) { + super({ + name: 'openapi-json-3-1-swagger-client', + mediaTypes: new OpenAPIMediaTypes( + ...lw.filterByFormat('generic'), + ...lw.filterByFormat('json') + ), + ...s + }); + } + async canParse(s) { + const o = 0 === this.fileExtensions.length || this.fileExtensions.includes(s.extension), + i = this.mediaTypes.includes(s.mediaType); + if (!o) return !1; + if (i) return !0; + if (!i) + try { + const o = s.toString(); + return (JSON.parse(o), this.detectionRegExp.test(o)); + } catch (s) { + return !1; + } + return !1; + } + async parse(s) { + if (this.sourceMap) + throw new Sw( + "openapi-json-3-1-swagger-client parser plugin doesn't support sourceMaps option" + ); + const o = new Mu(), + i = s.toString(); + if (this.allowEmpty && '' === i.trim()) return o; + try { + const s = JSON.parse(i), + u = wb.refract(s, this.refractorOpts); + return (u.classes.push('result'), o.push(u), o); + } catch (o) { + throw new Sw(`Error parsing "${s.uri}"`, { cause: o }); + } + } + }; + const Bw = class OpenAPIYAML31Parser extends xw { + detectionRegExp = + /(?^(["']?)openapi\2\s*:\s*(["']?)(?3\.1\.(?:[1-9]\d*|0))\3(?:\s+|$))|(?"openapi"\s*:\s*"(?3\.1\.(?:[1-9]\d*|0))")/m; + constructor(s = {}) { + super({ + name: 'openapi-yaml-3-1-swagger-client', + mediaTypes: new OpenAPIMediaTypes( + ...lw.filterByFormat('generic'), + ...lw.filterByFormat('yaml') + ), + ...s + }); + } + async canParse(s) { + const o = 0 === this.fileExtensions.length || this.fileExtensions.includes(s.extension), + i = this.mediaTypes.includes(s.mediaType); + if (!o) return !1; + if (i) return !0; + if (!i) + try { + const o = s.toString(); + return (mn.load(o), this.detectionRegExp.test(o)); + } catch (s) { + return !1; + } + return !1; + } + async parse(s) { + if (this.sourceMap) + throw new Sw( + "openapi-yaml-3-1-swagger-client parser plugin doesn't support sourceMaps option" + ); + const o = new Mu(), + i = s.toString(); + try { + const s = mn.load(i, { schema: nn }); + if (this.allowEmpty && void 0 === s) return o; + const u = wb.refract(s, this.refractorOpts); + return (u.classes.push('result'), o.push(u), o); + } catch (o) { + throw new Sw(`Error parsing "${s.uri}"`, { cause: o }); + } + } + }; + const Fw = _curry3(function propEq(s, o, i) { + return ra(s, Da(o, i)); + }); + const qw = class DereferenceStrategy { + name; + constructor({ name: s }) { + this.name = s; + } + }; + var $w = _curry2(function none(s, o) { + return ju(_complement(s), o); + }); + const Vw = $w; + var Uw = __webpack_require__(8068); + const zw = class ElementIdentityError extends Jo { + value; + constructor(s, o) { + (super(s, o), void 0 !== o && (this.value = o.value)); + } + }; + class IdentityManager { + uuid; + identityMap; + constructor({ length: s = 6 } = {}) { + ((this.uuid = new Uw({ length: s })), (this.identityMap = new WeakMap())); + } + identify(s) { + if (!Nu(s)) + throw new zw( + 'Cannot not identify the element. `element` is neither structurally compatible nor a subclass of an Element class.', + { value: s } + ); + if (s.meta.hasKey('id') && Ru(s.meta.get('id')) && !s.meta.get('id').equals('')) + return s.id; + if (this.identityMap.has(s)) return this.identityMap.get(s); + const o = new Cu.Om(this.generateId()); + return (this.identityMap.set(s, o), o); + } + forget(s) { + return !!this.identityMap.has(s) && (this.identityMap.delete(s), !0); + } + generateId() { + return this.uuid.randomUUID(); + } + } + new IdentityManager(); + const Ww = _curry3(function pathOr(s, o, i) { + return Na(s, _path(o, i)); + }), + traversal_find = (s, o) => { + const i = new PredicateVisitor({ predicate: s, returnOnTrue: Ju }); + return (visitor_visit(o, i), Ww(void 0, [0], i.result)); + }; + const Kw = class JsonSchema$anchorError extends Ho {}; + const Hw = class EvaluationJsonSchema$anchorError extends Kw {}; + const Jw = class InvalidJsonSchema$anchorError extends Kw { + constructor(s) { + super(`Invalid JSON Schema $anchor "${s}".`); + } + }, + isAnchor = (s) => /^[A-Za-z_][A-Za-z_0-9.-]*$/.test(s), + uriToAnchor = (s) => { + const o = getHash(s); + return Up('#', o); + }, + $anchor_evaluate = (s, o) => { + const i = ((s) => { + if (!isAnchor(s)) throw new Jw(s); + return s; + })(s), + u = traversal_find((s) => Q_(s) && serializers_value(s.$anchor) === i, o); + if (Rl(u)) throw new Hw(`Evaluation failed on token: "${i}"`); + return u; + }, + traversal_filter = (s, o) => { + const i = new PredicateVisitor({ predicate: s }); + return (visitor_visit(o, i), new Cu.G6(i.result)); + }; + const Gw = class JsonSchemaUriError extends Ho {}; + const Yw = class EvaluationJsonSchemaUriError extends Gw {}, + resolveSchema$refField = (s, o) => { + if (void 0 === o.$ref) return; + const i = getHash(serializers_value(o.$ref)), + u = serializers_value(o.meta.get('inherited$id')), + _ = Ca((s, o) => resolve(s, sanitize(stripHash(o))), s, [ + ...u, + serializers_value(o.$ref) + ]); + return `${_}${'#' === i ? '' : i}`; + }, + refractToSchemaElement = (s) => { + if (refractToSchemaElement.cache.has(s)) return refractToSchemaElement.cache.get(s); + const o = qb.refract(s); + return (refractToSchemaElement.cache.set(s, o), o); + }; + refractToSchemaElement.cache = new WeakMap(); + const maybeRefractToSchemaElement = (s) => + isPrimitiveElement(s) ? refractToSchemaElement(s) : s, + uri_evaluate = (s, o) => { + const { cache: i } = uri_evaluate, + u = stripHash(s), + isSchemaElementWith$id = (s) => Q_(s) && void 0 !== s.$id; + if (!i.has(o)) { + const s = traversal_filter(isSchemaElementWith$id, o); + i.set(o, Array.from(s)); + } + const _ = i.get(o).find((s) => { + const o = ((s, o) => { + if (void 0 === o.$id) return; + const i = serializers_value(o.meta.get('inherited$id')); + return Ca((s, o) => resolve(s, sanitize(stripHash(o))), s, [ + ...i, + serializers_value(o.$id) + ]); + })(u, s); + return o === u; + }); + if (Rl(_)) throw new Yw(`Evaluation failed on URI: "${s}"`); + let w, x; + return ( + isAnchor(uriToAnchor(s)) + ? ((w = $anchor_evaluate), (x = uriToAnchor(s))) + : ((w = es_evaluate), (x = uriToPointer(s))), + w(x, _) + ); + }; + uri_evaluate.cache = new WeakMap(); + const Xw = class MaximumDereferenceDepthError extends _w {}; + const Zw = class MaximumResolveDepthError extends Iw {}; + const Qw = class UnmatchedResolverError extends Pw {}, + _swagger_api_apidom_reference_es_parse = async (s, o) => { + const i = new vw({ uri: sanitize(stripHash(s)), mediaType: o.parse.mediaType }), + u = await (async (s, o) => { + const i = o.resolve.resolvers.map((s) => { + const i = Object.create(s); + return Object.assign(i, o.resolve.resolverOpts); + }), + u = await plugins_filter('canRead', [s, o], i); + if (gp(u)) throw new Qw(s.uri); + try { + const { result: o } = await run('read', [s], u); + return o; + } catch (o) { + throw new Iw(`Error while reading file "${s.uri}"`, { cause: o }); + } + })(i, o); + return (async (s, o) => { + const i = o.parse.parsers.map((s) => { + const i = Object.create(s); + return Object.assign(i, o.parse.parserOpts); + }), + u = await plugins_filter('canParse', [s, o], i); + if (gp(u)) throw new Qw(s.uri); + try { + const { plugin: i, result: _ } = await run('parse', [s, o], u); + return !i.allowEmpty && _.isEmpty + ? Promise.reject(new ww(`Error while parsing file "${s.uri}". File is empty.`)) + : _; + } catch (o) { + throw new ww(`Error while parsing file "${s.uri}"`, { cause: o }); + } + })(new vw({ ...i, data: u }), o); + }; + class AncestorLineage extends Array { + includesCycle(s) { + return this.filter((o) => o.has(s)).length > 1; + } + includes(s, o) { + return s instanceof Set ? super.includes(s, o) : this.some((o) => o.has(s)); + } + findItem(s) { + for (const o of this) for (const i of o) if (Nu(i) && s(i)) return i; + } + } + const eS = visitor_visit[Symbol.for('nodejs.util.promisify.custom')], + tS = new IdentityManager(), + mutationReplacer = (s, o, i, u) => { + $u(u) ? (u.value = s) : Array.isArray(u) && (u[i] = s); + }; + class OpenAPI3_1DereferenceVisitor { + indirections; + namespace; + reference; + options; + ancestors; + refractCache; + constructor({ + reference: s, + namespace: o, + options: i, + indirections: u = [], + ancestors: _ = new AncestorLineage(), + refractCache: w = new Map() + }) { + ((this.indirections = u), + (this.namespace = o), + (this.reference = s), + (this.options = i), + (this.ancestors = new AncestorLineage(..._)), + (this.refractCache = w)); + } + toBaseURI(s) { + return resolve(this.reference.uri, sanitize(stripHash(s))); + } + async toReference(s) { + if (this.reference.depth >= this.options.resolve.maxDepth) + throw new Zw( + `Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"` + ); + const o = this.toBaseURI(s), + { refSet: i } = this.reference; + if (i.has(o)) return i.find(Fw(o, 'uri')); + const u = await _swagger_api_apidom_reference_es_parse(unsanitize(o), { + ...this.options, + parse: { ...this.options.parse, mediaType: 'text/plain' } + }), + _ = new cw({ uri: o, value: cloneDeep(u), depth: this.reference.depth + 1 }); + if ((i.add(_), this.options.dereference.immutable)) { + const s = new cw({ + uri: `immutable://${o}`, + value: u, + depth: this.reference.depth + 1 + }); + i.add(s); + } + return _; + } + toAncestorLineage(s) { + const o = new Set(s.filter(Nu)); + return [new AncestorLineage(...this.ancestors, o), o]; + } + async ReferenceElement(s, o, i, u, _, w) { + if (this.indirections.includes(s)) return !1; + const [x, C] = this.toAncestorLineage([..._, i]), + j = this.toBaseURI(serializers_value(s.$ref)), + L = stripHash(this.reference.uri) === j, + B = !L; + if (!this.options.resolve.internal && L) return !1; + if (!this.options.resolve.external && B) return !1; + const $ = await this.toReference(serializers_value(s.$ref)), + V = resolve(j, serializers_value(s.$ref)); + this.indirections.push(s); + const U = uriToPointer(V); + let z = es_evaluate(U, $.value.result); + if (((z.id = tS.identify(z)), isPrimitiveElement(z))) { + const o = serializers_value(s.meta.get('referenced-element')), + i = `${o}-${serializers_value(tS.identify(z))}`; + if (this.refractCache.has(i)) z = this.refractCache.get(i); + else if (isReferenceLikeElement(z)) + ((z = Pb.refract(z)), + z.setMetaProperty('referenced-element', o), + this.refractCache.set(i, z)); + else { + ((z = this.namespace.getElementClass(o).refract(z)), this.refractCache.set(i, z)); + } + } + if (s === z) throw new Ho('Recursive Reference Object detected'); + if (this.indirections.length > this.options.dereference.maxDepth) + throw new Xw( + `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"` + ); + if (x.includes(z)) { + if ((($.refSet.circular = !0), 'error' === this.options.dereference.circular)) + throw new Ho('Circular reference detected'); + if ('replace' === this.options.dereference.circular) { + var Y, Z; + const o = new Cu.sI(z.id, { + type: 'reference', + uri: $.uri, + $ref: serializers_value(s.$ref) + }), + u = ( + null !== + (Y = + null === (Z = this.options.dereference.strategyOpts['openapi-3-1']) || + void 0 === Z + ? void 0 + : Z.circularReplacer) && void 0 !== Y + ? Y + : this.options.dereference.circularReplacer + )(o); + return (w.replaceWith(u, mutationReplacer), !i && u); + } + } + const ee = stripHash($.refSet.rootRef.uri) !== $.uri, + ie = ['error', 'replace'].includes(this.options.dereference.circular); + if ((B || ee || G_(z) || ie) && !x.includesCycle(z)) { + C.add(s); + const o = new OpenAPI3_1DereferenceVisitor({ + reference: $, + namespace: this.namespace, + indirections: [...this.indirections], + options: this.options, + refractCache: this.refractCache, + ancestors: x + }); + ((z = await eS(z, o, { + keyMap: nw, + nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType + })), + C.delete(s)); + } + this.indirections.pop(); + const ae = cloneShallow(z); + return ( + ae.setMetaProperty('id', tS.generateId()), + ae.setMetaProperty('ref-fields', { + $ref: serializers_value(s.$ref), + description: serializers_value(s.description), + summary: serializers_value(s.summary) + }), + ae.setMetaProperty('ref-origin', $.uri), + ae.setMetaProperty('ref-referencing-element-id', cloneDeep(tS.identify(s))), + Fu(z) && + Fu(ae) && + (s.hasKey('description') && + 'description' in z && + (ae.remove('description'), ae.set('description', s.get('description'))), + s.hasKey('summary') && + 'summary' in z && + (ae.remove('summary'), ae.set('summary', s.get('summary')))), + w.replaceWith(ae, mutationReplacer), + !i && ae + ); + } + async PathItemElement(s, o, i, u, _, w) { + if (!Ru(s.$ref)) return; + if (this.indirections.includes(s)) return !1; + const [x, C] = this.toAncestorLineage([..._, i]), + j = this.toBaseURI(serializers_value(s.$ref)), + L = stripHash(this.reference.uri) === j, + B = !L; + if (!this.options.resolve.internal && L) return; + if (!this.options.resolve.external && B) return; + const $ = await this.toReference(serializers_value(s.$ref)), + V = resolve(j, serializers_value(s.$ref)); + this.indirections.push(s); + const U = uriToPointer(V); + let z = es_evaluate(U, $.value.result); + if (((z.id = tS.identify(z)), isPrimitiveElement(z))) { + const s = `path-item-${serializers_value(tS.identify(z))}`; + this.refractCache.has(s) + ? (z = this.refractCache.get(s)) + : ((z = Ab.refract(z)), this.refractCache.set(s, z)); + } + if (s === z) throw new Ho('Recursive Path Item Object reference detected'); + if (this.indirections.length > this.options.dereference.maxDepth) + throw new Xw( + `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"` + ); + if (x.includes(z)) { + if ((($.refSet.circular = !0), 'error' === this.options.dereference.circular)) + throw new Ho('Circular reference detected'); + if ('replace' === this.options.dereference.circular) { + var Y, Z; + const o = new Cu.sI(z.id, { + type: 'path-item', + uri: $.uri, + $ref: serializers_value(s.$ref) + }), + u = ( + null !== + (Y = + null === (Z = this.options.dereference.strategyOpts['openapi-3-1']) || + void 0 === Z + ? void 0 + : Z.circularReplacer) && void 0 !== Y + ? Y + : this.options.dereference.circularReplacer + )(o); + return (w.replaceWith(u, mutationReplacer), !i && u); + } + } + const ee = stripHash($.refSet.rootRef.uri) !== $.uri, + ie = ['error', 'replace'].includes(this.options.dereference.circular); + if ((B || ee || (H_(z) && Ru(z.$ref)) || ie) && !x.includesCycle(z)) { + C.add(s); + const o = new OpenAPI3_1DereferenceVisitor({ + reference: $, + namespace: this.namespace, + indirections: [...this.indirections], + options: this.options, + refractCache: this.refractCache, + ancestors: x + }); + ((z = await eS(z, o, { + keyMap: nw, + nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType + })), + C.delete(s)); + } + if ((this.indirections.pop(), H_(z))) { + const o = new Ab([...z.content], cloneDeep(z.meta), cloneDeep(z.attributes)); + (o.setMetaProperty('id', tS.generateId()), + s.forEach((s, i, u) => { + (o.remove(serializers_value(i)), o.content.push(u)); + }), + o.remove('$ref'), + o.setMetaProperty('ref-fields', { $ref: serializers_value(s.$ref) }), + o.setMetaProperty('ref-origin', $.uri), + o.setMetaProperty('ref-referencing-element-id', cloneDeep(tS.identify(s))), + (z = o)); + } + return (w.replaceWith(z, mutationReplacer), i ? void 0 : z); + } + async LinkElement(s, o, i, u, _, w) { + if (!Ru(s.operationRef) && !Ru(s.operationId)) return; + if (Ru(s.operationRef) && Ru(s.operationId)) + throw new Ho( + 'LinkElement operationRef and operationId fields are mutually exclusive.' + ); + let x; + if (Ru(s.operationRef)) { + var C; + const o = uriToPointer(serializers_value(s.operationRef)), + u = this.toBaseURI(serializers_value(s.operationRef)), + _ = stripHash(this.reference.uri) === u, + j = !_; + if (!this.options.resolve.internal && _) return; + if (!this.options.resolve.external && j) return; + const L = await this.toReference(serializers_value(s.operationRef)); + if (((x = es_evaluate(o, L.value.result)), isPrimitiveElement(x))) { + const s = `operation-${serializers_value(tS.identify(x))}`; + this.refractCache.has(s) + ? (x = this.refractCache.get(s)) + : ((x = Sb.refract(x)), this.refractCache.set(s, x)); + } + ((x = cloneShallow(x)), x.setMetaProperty('ref-origin', L.uri)); + const B = cloneShallow(s); + return ( + null === (C = B.operationRef) || void 0 === C || C.meta.set('operation', x), + w.replaceWith(B, mutationReplacer), + i ? void 0 : B + ); + } + if (Ru(s.operationId)) { + var j; + const o = serializers_value(s.operationId), + u = await this.toReference(unsanitize(this.reference.uri)); + if ( + ((x = traversal_find( + (s) => W_(s) && Nu(s.operationId) && s.operationId.equals(o), + u.value.result + )), + Rl(x)) + ) + throw new Ho(`OperationElement(operationId=${o}) not found.`); + const _ = cloneShallow(s); + return ( + null === (j = _.operationId) || void 0 === j || j.meta.set('operation', x), + w.replaceWith(_, mutationReplacer), + i ? void 0 : _ + ); + } + } + async ExampleElement(s, o, i, u, _, w) { + if (!Ru(s.externalValue)) return; + if (s.hasKey('value') && Ru(s.externalValue)) + throw new Ho('ExampleElement value and externalValue fields are mutually exclusive.'); + const x = this.toBaseURI(serializers_value(s.externalValue)), + C = stripHash(this.reference.uri) === x, + j = !C; + if (!this.options.resolve.internal && C) return; + if (!this.options.resolve.external && j) return; + const L = await this.toReference(serializers_value(s.externalValue)), + B = cloneShallow(L.value.result); + B.setMetaProperty('ref-origin', L.uri); + const $ = cloneShallow(s); + return (($.value = B), w.replaceWith($, mutationReplacer), i ? void 0 : $); + } + async SchemaElement(s, o, i, u, _, w) { + if (!Ru(s.$ref)) return; + if (this.indirections.includes(s)) return !1; + const [x, C] = this.toAncestorLineage([..._, i]); + let j = await this.toReference(unsanitize(this.reference.uri)), + { uri: L } = j; + const B = resolveSchema$refField(L, s), + $ = stripHash(B), + V = new vw({ uri: $ }), + U = Vw((s) => s.canRead(V), this.options.resolve.resolvers), + z = !U; + let Y, + Z = stripHash(this.reference.uri) === B, + ee = !Z; + this.indirections.push(s); + try { + if (U || z) { + L = this.toBaseURI(B); + const s = B, + o = maybeRefractToSchemaElement(j.value.result); + if ( + ((Y = uri_evaluate(s, o)), + (Y = maybeRefractToSchemaElement(Y)), + (Y.id = tS.identify(Y)), + !this.options.resolve.internal && Z) + ) + return; + if (!this.options.resolve.external && ee) return; + } else { + if ( + ((L = this.toBaseURI(B)), + (Z = stripHash(this.reference.uri) === L), + (ee = !Z), + !this.options.resolve.internal && Z) + ) + return; + if (!this.options.resolve.external && ee) return; + j = await this.toReference(unsanitize(B)); + const s = uriToPointer(B), + o = maybeRefractToSchemaElement(j.value.result); + ((Y = es_evaluate(s, o)), + (Y = maybeRefractToSchemaElement(Y)), + (Y.id = tS.identify(Y))); + } + } catch (s) { + if (!(z && s instanceof Yw)) throw s; + if (isAnchor(uriToAnchor(B))) { + if ( + ((Z = stripHash(this.reference.uri) === L), + (ee = !Z), + !this.options.resolve.internal && Z) + ) + return; + if (!this.options.resolve.external && ee) return; + j = await this.toReference(unsanitize(B)); + const s = uriToAnchor(B), + o = maybeRefractToSchemaElement(j.value.result); + ((Y = $anchor_evaluate(s, o)), + (Y = maybeRefractToSchemaElement(Y)), + (Y.id = tS.identify(Y))); + } else { + if ( + ((L = this.toBaseURI(B)), + (Z = stripHash(this.reference.uri) === L), + (ee = !Z), + !this.options.resolve.internal && Z) + ) + return; + if (!this.options.resolve.external && ee) return; + j = await this.toReference(unsanitize(B)); + const s = uriToPointer(B), + o = maybeRefractToSchemaElement(j.value.result); + ((Y = es_evaluate(s, o)), + (Y = maybeRefractToSchemaElement(Y)), + (Y.id = tS.identify(Y))); + } + } + if (s === Y) throw new Ho('Recursive Schema Object reference detected'); + if (this.indirections.length > this.options.dereference.maxDepth) + throw new Xw( + `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"` + ); + if (x.includes(Y)) { + if (((j.refSet.circular = !0), 'error' === this.options.dereference.circular)) + throw new Ho('Circular reference detected'); + if ('replace' === this.options.dereference.circular) { + var ie, ae; + const o = new Cu.sI(Y.id, { + type: 'json-schema', + uri: j.uri, + $ref: serializers_value(s.$ref) + }), + u = ( + null !== + (ie = + null === (ae = this.options.dereference.strategyOpts['openapi-3-1']) || + void 0 === ae + ? void 0 + : ae.circularReplacer) && void 0 !== ie + ? ie + : this.options.dereference.circularReplacer + )(o); + return (w.replaceWith(u, mutationReplacer), !i && u); + } + } + const le = stripHash(j.refSet.rootRef.uri) !== j.uri, + ce = ['error', 'replace'].includes(this.options.dereference.circular); + if ((ee || le || (Q_(Y) && Ru(Y.$ref)) || ce) && !x.includesCycle(Y)) { + C.add(s); + const o = new OpenAPI3_1DereferenceVisitor({ + reference: j, + namespace: this.namespace, + indirections: [...this.indirections], + options: this.options, + refractCache: this.refractCache, + ancestors: x + }); + ((Y = await eS(Y, o, { + keyMap: nw, + nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType + })), + C.delete(s)); + } + if ((this.indirections.pop(), predicates_isBooleanJsonSchemaElement(Y))) { + const o = cloneDeep(Y); + return ( + o.setMetaProperty('id', tS.generateId()), + o.setMetaProperty('ref-fields', { $ref: serializers_value(s.$ref) }), + o.setMetaProperty('ref-origin', j.uri), + o.setMetaProperty('ref-referencing-element-id', cloneDeep(tS.identify(s))), + w.replaceWith(o, mutationReplacer), + !i && o + ); + } + if (Q_(Y)) { + const o = new qb([...Y.content], cloneDeep(Y.meta), cloneDeep(Y.attributes)); + (o.setMetaProperty('id', tS.generateId()), + s.forEach((s, i, u) => { + (o.remove(serializers_value(i)), o.content.push(u)); + }), + o.remove('$ref'), + o.setMetaProperty('ref-fields', { $ref: serializers_value(s.$ref) }), + o.setMetaProperty('ref-origin', j.uri), + o.setMetaProperty('ref-referencing-element-id', cloneDeep(tS.identify(s))), + (Y = o)); + } + return (w.replaceWith(Y, mutationReplacer), i ? void 0 : Y); + } + } + const rS = OpenAPI3_1DereferenceVisitor, + nS = visitor_visit[Symbol.for('nodejs.util.promisify.custom')]; + const sS = class OpenAPI3_1DereferenceStrategy extends qw { + constructor(s) { + super({ ...(null != s ? s : {}), name: 'openapi-3-1' }); + } + canDereference(s) { + var o; + return 'text/plain' !== s.mediaType + ? lw.includes(s.mediaType) + : z_(null === (o = s.parseResult) || void 0 === o ? void 0 : o.result); + } + async dereference(s, o) { + var i; + const u = createNamespace(ow), + _ = null !== (i = o.dereference.refSet) && void 0 !== i ? i : new uw(), + w = new uw(); + let x, + C = _; + (_.has(s.uri) + ? (x = _.find(Fw(s.uri, 'uri'))) + : ((x = new cw({ uri: s.uri, value: s.parseResult })), _.add(x)), + o.dereference.immutable && + (_.refs + .map((s) => new cw({ ...s, value: cloneDeep(s.value) })) + .forEach((s) => w.add(s)), + (x = w.find((o) => o.uri === s.uri)), + (C = w))); + const j = new rS({ reference: x, namespace: u, options: o }), + L = await nS(C.rootRef.value, j, { + keyMap: nw, + nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType + }); + return ( + o.dereference.immutable && + w.refs + .filter((s) => s.uri.startsWith('immutable://')) + .map((s) => new cw({ ...s, uri: s.uri.replace(/^immutable:\/\//, '') })) + .forEach((s) => _.add(s)), + null === o.dereference.refSet && _.clean(), + w.clean(), + L + ); + } + }, + to_path = (s) => { + const o = ((s) => s.slice(2))(s); + return o.reduce((s, i, u) => { + if ($u(i)) { + const o = String(serializers_value(i.key)); + s.push(o); + } else if (qu(o[u - 2])) { + const _ = o[u - 2].content.indexOf(i); + s.push(_); + } + return s; + }, []); + }; + const oS = class ModelPropertyMacroVisitor { + modelPropertyMacro; + options; + SchemaElement = { + leave: (s, o, i, u, _) => { + void 0 !== s.properties && + Fu(s.properties) && + s.properties.forEach((o) => { + if (Fu(o)) + try { + const s = this.modelPropertyMacro(serializers_value(o)); + o.set('default', s); + } catch (o) { + var u, w; + const x = new Error(o, { cause: o }); + ((x.fullPath = [...to_path([..._, i, s]), 'properties']), + null === (u = this.options.dereference.dereferenceOpts) || + void 0 === u || + null === (u = u.errors) || + void 0 === u || + null === (w = u.push) || + void 0 === w || + w.call(u, x)); + } + }); + } + }; + constructor({ modelPropertyMacro: s, options: o }) { + ((this.modelPropertyMacro = s), (this.options = o)); + } + }; + const iS = class all_of_AllOfVisitor { + options; + SchemaElement = { + leave(s, o, i, u, _) { + if (void 0 === s.allOf) return; + if (!qu(s.allOf)) { + var w, x; + const o = new TypeError('allOf must be an array'); + return ( + (o.fullPath = [...to_path([..._, i, s]), 'allOf']), + void ( + null === (w = this.options.dereference.dereferenceOpts) || + void 0 === w || + null === (w = w.errors) || + void 0 === w || + null === (x = w.push) || + void 0 === x || + x.call(w, o) + ) + ); + } + if (s.allOf.isEmpty) return void s.remove('allOf'); + if (!s.allOf.content.every(Q_)) { + var C, j; + const o = new TypeError('Elements in allOf must be objects'); + return ( + (o.fullPath = [...to_path([..._, i, s]), 'allOf']), + void ( + null === (C = this.options.dereference.dereferenceOpts) || + void 0 === C || + null === (C = C.errors) || + void 0 === C || + null === (j = C.push) || + void 0 === j || + j.call(C, o) + ) + ); + } + for (; s.hasKey('allOf'); ) { + const { allOf: o } = s; + s.remove('allOf'); + const i = deepmerge.all([...o.content, s]); + if ((s.hasKey('$$ref') || i.remove('$$ref'), s.hasKey('example'))) { + const o = i.getMember('example'); + o && (o.value = s.get('example')); + } + if (s.hasKey('examples')) { + const o = i.getMember('examples'); + o && (o.value = s.get('examples')); + } + s.content = i.content; + } + } + }; + constructor({ options: s }) { + this.options = s; + } + }; + const aS = class ParameterMacroVisitor { + parameterMacro; + options; + #e; + OperationElement = { + enter: (s) => { + this.#e = s; + }, + leave: () => { + this.#e = void 0; + } + }; + ParameterElement = { + leave: (s, o, i, u, _) => { + const w = this.#e ? serializers_value(this.#e) : null, + x = serializers_value(s); + try { + const o = this.parameterMacro(w, x); + s.set('default', o); + } catch (s) { + var C, j; + const o = new Error(s, { cause: s }); + ((o.fullPath = to_path([..._, i])), + null === (C = this.options.dereference.dereferenceOpts) || + void 0 === C || + null === (C = C.errors) || + void 0 === C || + null === (j = C.push) || + void 0 === j || + j.call(C, o)); + } + } + }; + constructor({ parameterMacro: s, options: o }) { + ((this.parameterMacro = s), (this.options = o)); + } + }, + get_root_cause = (s) => { + if (null == s.cause) return s; + let { cause: o } = s; + for (; null != o.cause; ) o = o.cause; + return o; + }; + const lS = class SchemaRefError extends Jo {}, + { wrapError: cS } = ru, + uS = visitor_visit[Symbol.for('nodejs.util.promisify.custom')], + pS = new IdentityManager(), + dereference_mutationReplacer = (s, o, i, u) => { + $u(u) ? (u.value = s) : Array.isArray(u) && (u[i] = s); + }; + class OpenAPI3_1SwaggerClientDereferenceVisitor extends rS { + useCircularStructures; + allowMetaPatches; + basePath; + constructor({ + allowMetaPatches: s = !0, + useCircularStructures: o = !1, + basePath: i = null, + ...u + }) { + (super(u), + (this.allowMetaPatches = s), + (this.useCircularStructures = o), + (this.basePath = i)); + } + async ReferenceElement(s, o, i, u, _, w) { + try { + if (this.indirections.includes(s)) return !1; + const [o, u] = this.toAncestorLineage([..._, i]), + L = this.toBaseURI(serializers_value(s.$ref)), + B = stripHash(this.reference.uri) === L, + $ = !B; + if (!this.options.resolve.internal && B) return !1; + if (!this.options.resolve.external && $) return !1; + const V = await this.toReference(serializers_value(s.$ref)), + U = resolve(L, serializers_value(s.$ref)); + this.indirections.push(s); + const z = uriToPointer(U); + let Y = es_evaluate(z, V.value.result); + if (((Y.id = pS.identify(Y)), isPrimitiveElement(Y))) { + const o = serializers_value(s.meta.get('referenced-element')), + i = `${o}-${serializers_value(pS.identify(Y))}`; + if (this.refractCache.has(i)) Y = this.refractCache.get(i); + else if (isReferenceLikeElement(Y)) + ((Y = Pb.refract(Y)), + Y.setMetaProperty('referenced-element', o), + this.refractCache.set(i, Y)); + else { + ((Y = this.namespace.getElementClass(o).refract(Y)), this.refractCache.set(i, Y)); + } + } + if (s === Y) throw new Ho('Recursive Reference Object detected'); + if (this.indirections.length > this.options.dereference.maxDepth) + throw new Xw( + `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"` + ); + if (o.includes(Y)) { + if (((V.refSet.circular = !0), 'error' === this.options.dereference.circular)) + throw new Ho('Circular reference detected'); + if ('replace' === this.options.dereference.circular) { + var x, C; + const o = new Cu.sI(Y.id, { + type: 'reference', + uri: V.uri, + $ref: serializers_value(s.$ref), + baseURI: U, + referencingElement: s + }), + u = ( + null !== + (x = + null === (C = this.options.dereference.strategyOpts['openapi-3-1']) || + void 0 === C + ? void 0 + : C.circularReplacer) && void 0 !== x + ? x + : this.options.dereference.circularReplacer + )(o); + return (w.replaceWith(o, dereference_mutationReplacer), !i && u); + } + } + const Z = stripHash(V.refSet.rootRef.uri) !== V.uri, + ee = ['error', 'replace'].includes(this.options.dereference.circular); + if (($ || Z || G_(Y) || ee) && !o.includesCycle(Y)) { + var j; + u.add(s); + const w = new OpenAPI3_1SwaggerClientDereferenceVisitor({ + reference: V, + namespace: this.namespace, + indirections: [...this.indirections], + options: this.options, + refractCache: this.refractCache, + ancestors: o, + allowMetaPatches: this.allowMetaPatches, + useCircularStructures: this.useCircularStructures, + basePath: + null !== (j = this.basePath) && void 0 !== j + ? j + : [...to_path([..._, i, s]), '$ref'] + }); + ((Y = await uS(Y, w, { + keyMap: nw, + nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType + })), + u.delete(s)); + } + this.indirections.pop(); + const ie = cloneShallow(Y); + if ( + (ie.setMetaProperty('ref-fields', { + $ref: serializers_value(s.$ref), + description: serializers_value(s.description), + summary: serializers_value(s.summary) + }), + ie.setMetaProperty('ref-origin', V.uri), + ie.setMetaProperty('ref-referencing-element-id', cloneDeep(pS.identify(s))), + Fu(Y) && + (s.hasKey('description') && + 'description' in Y && + (ie.remove('description'), ie.set('description', s.get('description'))), + s.hasKey('summary') && + 'summary' in Y && + (ie.remove('summary'), ie.set('summary', s.get('summary')))), + this.allowMetaPatches && Fu(ie) && !ie.hasKey('$$ref')) + ) { + const s = resolve(L, U); + ie.set('$$ref', s); + } + return (w.replaceWith(ie, dereference_mutationReplacer), !i && ie); + } catch (o) { + var L, B, $; + const u = get_root_cause(o), + w = cS(u, { + baseDoc: this.reference.uri, + $ref: serializers_value(s.$ref), + pointer: uriToPointer(serializers_value(s.$ref)), + fullPath: + null !== (L = this.basePath) && void 0 !== L + ? L + : [...to_path([..._, i, s]), '$ref'] + }); + return void ( + null === (B = this.options.dereference.dereferenceOpts) || + void 0 === B || + null === (B = B.errors) || + void 0 === B || + null === ($ = B.push) || + void 0 === $ || + $.call(B, w) + ); + } + } + async PathItemElement(s, o, i, u, _, w) { + try { + if (!Ru(s.$ref)) return; + if (this.indirections.includes(s)) return !1; + if (includesClasses(['cycle'], s.$ref)) return !1; + const [o, u] = this.toAncestorLineage([..._, i]), + L = this.toBaseURI(serializers_value(s.$ref)), + B = stripHash(this.reference.uri) === L, + $ = !B; + if (!this.options.resolve.internal && B) return; + if (!this.options.resolve.external && $) return; + const V = await this.toReference(serializers_value(s.$ref)), + U = resolve(L, serializers_value(s.$ref)); + this.indirections.push(s); + const z = uriToPointer(U); + let Y = es_evaluate(z, V.value.result); + if (((Y.id = pS.identify(Y)), isPrimitiveElement(Y))) { + const s = `path-item-${serializers_value(pS.identify(Y))}`; + this.refractCache.has(s) + ? (Y = this.refractCache.get(s)) + : ((Y = Ab.refract(Y)), this.refractCache.set(s, Y)); + } + if (s === Y) throw new Ho('Recursive Path Item Object reference detected'); + if (this.indirections.length > this.options.dereference.maxDepth) + throw new Xw( + `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"` + ); + if (o.includes(Y)) { + if (((V.refSet.circular = !0), 'error' === this.options.dereference.circular)) + throw new Ho('Circular reference detected'); + if ('replace' === this.options.dereference.circular) { + var x, C; + const o = new Cu.sI(Y.id, { + type: 'path-item', + uri: V.uri, + $ref: serializers_value(s.$ref), + baseURI: U, + referencingElement: s + }), + u = ( + null !== + (x = + null === (C = this.options.dereference.strategyOpts['openapi-3-1']) || + void 0 === C + ? void 0 + : C.circularReplacer) && void 0 !== x + ? x + : this.options.dereference.circularReplacer + )(o); + return (w.replaceWith(o, dereference_mutationReplacer), !i && u); + } + } + const Z = stripHash(V.refSet.rootRef.uri) !== V.uri, + ee = ['error', 'replace'].includes(this.options.dereference.circular); + if (($ || Z || (H_(Y) && Ru(Y.$ref)) || ee) && !o.includesCycle(Y)) { + var j; + u.add(s); + const w = new OpenAPI3_1SwaggerClientDereferenceVisitor({ + reference: V, + namespace: this.namespace, + indirections: [...this.indirections], + options: this.options, + ancestors: o, + allowMetaPatches: this.allowMetaPatches, + useCircularStructures: this.useCircularStructures, + basePath: + null !== (j = this.basePath) && void 0 !== j + ? j + : [...to_path([..._, i, s]), '$ref'] + }); + ((Y = await uS(Y, w, { + keyMap: nw, + nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType + })), + u.delete(s)); + } + if ((this.indirections.pop(), H_(Y))) { + const o = new Ab([...Y.content], cloneDeep(Y.meta), cloneDeep(Y.attributes)); + if ( + (s.forEach((s, i, u) => { + (o.remove(serializers_value(i)), o.content.push(u)); + }), + o.remove('$ref'), + o.setMetaProperty('ref-fields', { $ref: serializers_value(s.$ref) }), + o.setMetaProperty('ref-origin', V.uri), + o.setMetaProperty('ref-referencing-element-id', cloneDeep(pS.identify(s))), + this.allowMetaPatches && void 0 === o.get('$$ref')) + ) { + const s = resolve(L, U); + o.set('$$ref', s); + } + Y = o; + } + return (w.replaceWith(Y, dereference_mutationReplacer), i ? void 0 : Y); + } catch (o) { + var L, B, $; + const u = get_root_cause(o), + w = cS(u, { + baseDoc: this.reference.uri, + $ref: serializers_value(s.$ref), + pointer: uriToPointer(serializers_value(s.$ref)), + fullPath: + null !== (L = this.basePath) && void 0 !== L + ? L + : [...to_path([..._, i, s]), '$ref'] + }); + return void ( + null === (B = this.options.dereference.dereferenceOpts) || + void 0 === B || + null === (B = B.errors) || + void 0 === B || + null === ($ = B.push) || + void 0 === $ || + $.call(B, w) + ); + } + } + async SchemaElement(s, o, i, u, _, w) { + try { + if (!Ru(s.$ref)) return; + if (this.indirections.includes(s)) return !1; + const [o, u] = this.toAncestorLineage([..._, i]); + let L = await this.toReference(unsanitize(this.reference.uri)), + { uri: B } = L; + const $ = resolveSchema$refField(B, s), + V = stripHash($), + U = new vw({ uri: V }), + z = !this.options.resolve.resolvers.some((s) => s.canRead(U)), + Y = !z; + let Z, + ee = stripHash(this.reference.uri) === $, + ie = !ee; + this.indirections.push(s); + try { + if (z || Y) { + B = this.toBaseURI($); + const s = $, + o = maybeRefractToSchemaElement(L.value.result); + if ( + ((Z = uri_evaluate(s, o)), + (Z = maybeRefractToSchemaElement(Z)), + (Z.id = pS.identify(Z)), + !this.options.resolve.internal && ee) + ) + return; + if (!this.options.resolve.external && ie) return; + } else { + if ( + ((B = this.toBaseURI($)), + (ee = stripHash(this.reference.uri) === B), + (ie = !ee), + !this.options.resolve.internal && ee) + ) + return; + if (!this.options.resolve.external && ie) return; + L = await this.toReference(unsanitize($)); + const s = uriToPointer($), + o = maybeRefractToSchemaElement(L.value.result); + ((Z = es_evaluate(s, o)), + (Z = maybeRefractToSchemaElement(Z)), + (Z.id = pS.identify(Z))); + } + } catch (s) { + if (!(Y && s instanceof Yw)) throw s; + if (isAnchor(uriToAnchor($))) { + if ( + ((ee = stripHash(this.reference.uri) === B), + (ie = !ee), + !this.options.resolve.internal && ee) + ) + return; + if (!this.options.resolve.external && ie) return; + L = await this.toReference(unsanitize($)); + const s = uriToAnchor($), + o = maybeRefractToSchemaElement(L.value.result); + ((Z = $anchor_evaluate(s, o)), + (Z = maybeRefractToSchemaElement(Z)), + (Z.id = pS.identify(Z))); + } else { + if ( + ((B = this.toBaseURI(serializers_value($))), + (ee = stripHash(this.reference.uri) === B), + (ie = !ee), + !this.options.resolve.internal && ee) + ) + return; + if (!this.options.resolve.external && ie) return; + L = await this.toReference(unsanitize($)); + const s = uriToPointer($), + o = maybeRefractToSchemaElement(L.value.result); + ((Z = es_evaluate(s, o)), + (Z = maybeRefractToSchemaElement(Z)), + (Z.id = pS.identify(Z))); + } + } + if (s === Z) throw new Ho('Recursive Schema Object reference detected'); + if (this.indirections.length > this.options.dereference.maxDepth) + throw new Xw( + `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"` + ); + if (o.includes(Z)) { + if (((L.refSet.circular = !0), 'error' === this.options.dereference.circular)) + throw new Ho('Circular reference detected'); + if ('replace' === this.options.dereference.circular) { + var x, C; + const o = new Cu.sI(Z.id, { + type: 'json-schema', + uri: L.uri, + $ref: serializers_value(s.$ref), + baseURI: resolve(B, $), + referencingElement: s + }), + u = ( + null !== + (x = + null === (C = this.options.dereference.strategyOpts['openapi-3-1']) || + void 0 === C + ? void 0 + : C.circularReplacer) && void 0 !== x + ? x + : this.options.dereference.circularReplacer + )(o); + return (w.replaceWith(u, dereference_mutationReplacer), !i && u); + } + } + const ae = stripHash(L.refSet.rootRef.uri) !== L.uri, + le = ['error', 'replace'].includes(this.options.dereference.circular); + if ((ie || ae || (Q_(Z) && Ru(Z.$ref)) || le) && !o.includesCycle(Z)) { + var j; + u.add(s); + const w = new OpenAPI3_1SwaggerClientDereferenceVisitor({ + reference: L, + namespace: this.namespace, + indirections: [...this.indirections], + options: this.options, + useCircularStructures: this.useCircularStructures, + allowMetaPatches: this.allowMetaPatches, + ancestors: o, + basePath: + null !== (j = this.basePath) && void 0 !== j + ? j + : [...to_path([..._, i, s]), '$ref'] + }); + ((Z = await uS(Z, w, { + keyMap: nw, + nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType + })), + u.delete(s)); + } + if ((this.indirections.pop(), predicates_isBooleanJsonSchemaElement(Z))) { + const o = cloneDeep(Z); + return ( + o.setMetaProperty('ref-fields', { $ref: serializers_value(s.$ref) }), + o.setMetaProperty('ref-origin', L.uri), + o.setMetaProperty('ref-referencing-element-id', cloneDeep(pS.identify(s))), + w.replaceWith(o, dereference_mutationReplacer), + !i && o + ); + } + if (Q_(Z)) { + const o = new qb([...Z.content], cloneDeep(Z.meta), cloneDeep(Z.attributes)); + if ( + (s.forEach((s, i, u) => { + (o.remove(serializers_value(i)), o.content.push(u)); + }), + o.remove('$ref'), + o.setMetaProperty('ref-fields', { $ref: serializers_value(s.$ref) }), + o.setMetaProperty('ref-origin', L.uri), + o.setMetaProperty('ref-referencing-element-id', cloneDeep(pS.identify(s))), + this.allowMetaPatches && void 0 === o.get('$$ref')) + ) { + const s = resolve(B, $); + o.set('$$ref', s); + } + Z = o; + } + return (w.replaceWith(Z, dereference_mutationReplacer), i ? void 0 : Z); + } catch (o) { + var L, B, $; + const u = get_root_cause(o), + w = new lS(`Could not resolve reference: ${u.message}`, { + baseDoc: this.reference.uri, + $ref: serializers_value(s.$ref), + fullPath: + null !== (L = this.basePath) && void 0 !== L + ? L + : [...to_path([..._, i, s]), '$ref'], + cause: u + }); + return void ( + null === (B = this.options.dereference.dereferenceOpts) || + void 0 === B || + null === (B = B.errors) || + void 0 === B || + null === ($ = B.push) || + void 0 === $ || + $.call(B, w) + ); + } + } + async LinkElement() {} + async ExampleElement(s, o, i, u, _, w) { + try { + return await super.ExampleElement(s, o, i, u, _, w); + } catch (o) { + var x, C, j; + const u = get_root_cause(o), + w = cS(u, { + baseDoc: this.reference.uri, + externalValue: serializers_value(s.externalValue), + fullPath: + null !== (x = this.basePath) && void 0 !== x + ? x + : [...to_path([..._, i, s]), 'externalValue'] + }); + return void ( + null === (C = this.options.dereference.dereferenceOpts) || + void 0 === C || + null === (C = C.errors) || + void 0 === C || + null === (j = C.push) || + void 0 === j || + j.call(C, w) + ); + } + } + } + const hS = OpenAPI3_1SwaggerClientDereferenceVisitor, + dS = mergeAll[Symbol.for('nodejs.util.promisify.custom')]; + const fS = class RootVisitor { + constructor({ parameterMacro: s, modelPropertyMacro: o, mode: i, options: u, ..._ }) { + const w = []; + (w.push(new hS({ ..._, options: u })), + 'function' == typeof o && w.push(new oS({ modelPropertyMacro: o, options: u })), + 'strict' !== i && w.push(new iS({ options: u })), + 'function' == typeof s && w.push(new aS({ parameterMacro: s, options: u }))); + const x = dS(w, { + nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType + }); + Object.assign(this, x); + } + }, + mS = visitor_visit[Symbol.for('nodejs.util.promisify.custom')]; + const gS = class OpenAPI3_1SwaggerClientDereferenceStrategy extends sS { + allowMetaPatches; + parameterMacro; + modelPropertyMacro; + mode; + ancestors; + constructor({ + allowMetaPatches: s = !1, + parameterMacro: o = null, + modelPropertyMacro: i = null, + mode: u = 'non-strict', + ancestors: _ = [], + ...w + } = {}) { + (super({ ...w }), + (this.name = 'openapi-3-1-swagger-client'), + (this.allowMetaPatches = s), + (this.parameterMacro = o), + (this.modelPropertyMacro = i), + (this.mode = u), + (this.ancestors = [..._])); + } + async dereference(s, o) { + var i; + const u = createNamespace(ow), + _ = null !== (i = o.dereference.refSet) && void 0 !== i ? i : new uw(), + w = new uw(); + let x, + C = _; + (_.has(s.uri) + ? (x = _.find((o) => o.uri === s.uri)) + : ((x = new cw({ uri: s.uri, value: s.parseResult })), _.add(x)), + o.dereference.immutable && + (_.refs + .map((s) => new cw({ ...s, value: cloneDeep(s.value) })) + .forEach((s) => w.add(s)), + (x = w.find((o) => o.uri === s.uri)), + (C = w))); + const j = new fS({ + reference: x, + namespace: u, + options: o, + allowMetaPatches: this.allowMetaPatches, + ancestors: this.ancestors, + modelPropertyMacro: this.modelPropertyMacro, + mode: this.mode, + parameterMacro: this.parameterMacro + }), + L = await mS(C.rootRef.value, j, { + keyMap: nw, + nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType + }); + return ( + o.dereference.immutable && + w.refs + .filter((s) => s.uri.startsWith('immutable://')) + .map((s) => new cw({ ...s, uri: s.uri.replace(/^immutable:\/\//, '') })) + .forEach((s) => _.add(s)), + null === o.dereference.refSet && _.clean(), + w.clean(), + L + ); + } + }, + circularReplacer = (s) => { + const o = serializers_value(s.meta.get('baseURI')), + i = s.meta.get('referencingElement'); + return new Cu.Sh({ $ref: o }, cloneDeep(i.meta), cloneDeep(i.attributes)); + }, + resolveOpenAPI31Strategy = async (s) => { + const { + spec: o, + timeout: i, + redirects: u, + requestInterceptor: _, + responseInterceptor: w, + pathDiscriminator: x = [], + allowMetaPatches: C = !1, + useCircularStructures: j = !1, + skipNormalization: L = !1, + parameterMacro: B = null, + modelPropertyMacro: $ = null, + mode: V = 'non-strict', + strategies: U + } = s; + try { + const { cache: z } = resolveOpenAPI31Strategy, + Y = U.find((s) => s.match(o)), + Z = isHttpUrl(url_cwd()) ? url_cwd() : Nc, + ee = options_retrievalURI(s), + ie = resolve(Z, ee); + let ae; + z.has(o) + ? (ae = z.get(o)) + : ((ae = wb.refract(o)), ae.classes.push('result'), z.set(o, ae)); + const le = new Mu([ae]), + ce = es_compile(x), + pe = '' === ce ? '' : `#${ce}`, + de = es_evaluate(ce, ae), + fe = new cw({ uri: ie, value: le }), + ye = new uw({ refs: [fe] }); + '' !== ce && (ye.rootRef = void 0); + const be = [new Set([de])], + _e = [], + we = await (async (s, o = {}) => { + const i = util_merge(pw, o); + return dereferenceApiDOM(s, i); + })(de, { + resolve: { + baseURI: `${ie}${pe}`, + resolvers: [new Nw({ timeout: i || 1e4, redirects: u || 10 })], + resolverOpts: { + swaggerHTTPClientConfig: { requestInterceptor: _, responseInterceptor: w } + }, + strategies: [new Ow()] + }, + parse: { + mediaType: lw.latest(), + parsers: [ + new Lw({ allowEmpty: !1, sourceMap: !1 }), + new Bw({ allowEmpty: !1, sourceMap: !1 }), + new Rw({ allowEmpty: !1, sourceMap: !1 }), + new Dw({ allowEmpty: !1, sourceMap: !1 }), + new kw({ allowEmpty: !1, sourceMap: !1 }) + ] + }, + dereference: { + maxDepth: 100, + strategies: [ + new gS({ + allowMetaPatches: C, + useCircularStructures: j, + parameterMacro: B, + modelPropertyMacro: $, + mode: V, + ancestors: be + }) + ], + refSet: ye, + dereferenceOpts: { errors: _e }, + immutable: !1, + circular: j ? 'ignore' : 'replace', + circularReplacer: j ? pw.dereference.circularReplacer : circularReplacer + } + }), + Se = ((s, o, i) => new xp({ element: i }).transclude(s, o))(de, we, ae), + xe = L ? Se : Y.normalize(Se); + return { spec: serializers_value(xe), errors: _e }; + } catch (s) { + if (s instanceof Wp || s instanceof Kp) return { spec: null, errors: [] }; + throw s; + } + }; + resolveOpenAPI31Strategy.cache = new WeakMap(); + const yS = resolveOpenAPI31Strategy; + function _clone(s, o, i) { + if ( + (i || (i = new vS()), + (function _isPrimitive(s) { + var o = typeof s; + return null == s || ('object' != o && 'function' != o); + })(s)) + ) + return s; + var u = function copy(u) { + var _ = i.get(s); + if (_) return _; + for (var w in (i.set(s, u), s)) + Object.prototype.hasOwnProperty.call(s, w) && (u[w] = o ? _clone(s[w], !0, i) : s[w]); + return u; + }; + switch (ea(s)) { + case 'Object': + return u(Object.create(Object.getPrototypeOf(s))); + case 'Array': + return u(Array(s.length)); + case 'Date': + return new Date(s.valueOf()); + case 'RegExp': + return _cloneRegExp(s); + case 'Int8Array': + case 'Uint8Array': + case 'Uint8ClampedArray': + case 'Int16Array': + case 'Uint16Array': + case 'Int32Array': + case 'Uint32Array': + case 'Float32Array': + case 'Float64Array': + case 'BigInt64Array': + case 'BigUint64Array': + return s.slice(); + default: + return s; + } + } + var vS = (function () { + function _ObjectMap() { + ((this.map = {}), (this.length = 0)); + } + return ( + (_ObjectMap.prototype.set = function (s, o) { + var i = this.hash(s), + u = this.map[i]; + (u || (this.map[i] = u = []), u.push([s, o]), (this.length += 1)); + }), + (_ObjectMap.prototype.hash = function (s) { + var o = []; + for (var i in s) o.push(Object.prototype.toString.call(s[i])); + return o.join(); + }), + (_ObjectMap.prototype.get = function (s) { + if (this.length <= 180) + for (var o in this.map) + for (var i = this.map[o], u = 0; u < i.length; u += 1) { + if ((w = i[u])[0] === s) return w[1]; + } + else { + var _ = this.hash(s); + if ((i = this.map[_])) + for (u = 0; u < i.length; u += 1) { + var w; + if ((w = i[u])[0] === s) return w[1]; + } + } + }), + _ObjectMap + ); + })(), + bS = (function () { + function XReduceBy(s, o, i, u) { + ((this.valueFn = s), + (this.valueAcc = o), + (this.keyFn = i), + (this.xf = u), + (this.inputs = {})); + } + return ( + (XReduceBy.prototype['@@transducer/init'] = _xfBase_init), + (XReduceBy.prototype['@@transducer/result'] = function (s) { + var o; + for (o in this.inputs) + if ( + _has(o, this.inputs) && + (s = this.xf['@@transducer/step'](s, this.inputs[o]))['@@transducer/reduced'] + ) { + s = s['@@transducer/value']; + break; + } + return ((this.inputs = null), this.xf['@@transducer/result'](s)); + }), + (XReduceBy.prototype['@@transducer/step'] = function (s, o) { + var i = this.keyFn(o); + return ( + (this.inputs[i] = this.inputs[i] || [i, _clone(this.valueAcc, !1)]), + (this.inputs[i][1] = this.valueFn(this.inputs[i][1], o)), + s + ); + }), + XReduceBy + ); + })(); + function _xreduceBy(s, o, i) { + return function (u) { + return new bS(s, o, i, u); + }; + } + var _S = _curryN( + 4, + [], + _dispatchable([], _xreduceBy, function reduceBy(s, o, i, u) { + var _ = _xwrap(function (u, _) { + var w = i(_), + x = s(_has(w, u) ? u[w] : _clone(o, !1), _); + return x && x['@@transducer/reduced'] ? _reduced(u) : ((u[w] = x), u); + }); + return wa(_, {}, u); + }) + ); + const ES = _curry2( + _checkForMethod( + 'groupBy', + _S(function (s, o) { + return (s.push(o), s); + }, []) + ) + ); + const wS = class NormalizeStorage { + internalStore; + constructor(s, o, i) { + ((this.storageElement = s), (this.storageField = o), (this.storageSubField = i)); + } + get store() { + if (!this.internalStore) { + let s = this.storageElement.get(this.storageField); + Fu(s) || ((s = new Cu.Sh()), this.storageElement.set(this.storageField, s)); + let o = s.get(this.storageSubField); + (qu(o) || ((o = new Cu.wE()), s.set(this.storageSubField, o)), + (this.internalStore = o)); + } + return this.internalStore; + } + append(s) { + this.includes(s) || this.store.push(s); + } + includes(s) { + return this.store.includes(s); + } + }, + removeSpaces = (s) => s.replace(/\s/g, ''), + normalize_operation_ids_replaceSpecialCharsWithUnderscore = (s) => s.replace(/\W/gi, '_'), + normalizeOperationId = (s, o, i) => { + const u = removeSpaces(s); + return u.length > 0 + ? normalize_operation_ids_replaceSpecialCharsWithUnderscore(u) + : ((s, o) => + `${normalize_operation_ids_replaceSpecialCharsWithUnderscore(removeSpaces(o.toLowerCase()))}${normalize_operation_ids_replaceSpecialCharsWithUnderscore(removeSpaces(s))}`)( + o, + i + ); + }, + normalize_operation_ids = + ({ + storageField: s = 'x-normalized', + operationIdNormalizer: o = normalizeOperationId + } = {}) => + (i) => { + const { predicates: u, ancestorLineageToJSONPointer: _, namespace: w } = i, + x = [], + C = [], + j = []; + let L; + return { + visitor: { + OpenApi3_1Element: { + enter(o) { + L = new wS(o, s, 'operation-ids'); + }, + leave() { + const s = ES((s) => serializers_value(s.operationId), C); + (Object.entries(s).forEach(([s, o]) => { + Array.isArray(o) && + (o.length <= 1 || + o.forEach((o, i) => { + const u = `${s}${i + 1}`; + o.operationId = new w.elements.String(u); + })); + }), + j.forEach((s) => { + if (void 0 === s.operationId) return; + const o = String(serializers_value(s.operationId)), + i = C.find( + (s) => serializers_value(s.meta.get('originalOperationId')) === o + ); + void 0 !== i && + ((s.operationId = cloneDeep.safe(i.operationId)), + s.meta.set('originalOperationId', o), + s.set('__originalOperationId', o)); + }), + (C.length = 0), + (j.length = 0), + (L = void 0)); + } + }, + PathItemElement: { + enter(s) { + const o = Na('path', serializers_value(s.meta.get('path'))); + x.push(o); + }, + leave() { + x.pop(); + } + }, + OperationElement: { + enter(s, i, u, j, B) { + if (void 0 === s.operationId) return; + const $ = _([...B, u, s]); + if (L.includes($)) return; + const V = String(serializers_value(s.operationId)), + U = Fa(x), + z = Na('method', serializers_value(s.meta.get('http-method'))), + Y = o(V, U, z); + V !== Y && + ((s.operationId = new w.elements.String(Y)), + s.set('__originalOperationId', V), + s.meta.set('originalOperationId', V), + C.push(s), + L.append($)); + } + }, + LinkElement: { + leave(s) { + u.isLinkElement(s) && void 0 !== s.operationId && j.push(s); + } + } + } + }; + }; + var SS = (function () { + function XUniqWith(s, o) { + ((this.xf = o), (this.pred = s), (this.items = [])); + } + return ( + (XUniqWith.prototype['@@transducer/init'] = _xfBase_init), + (XUniqWith.prototype['@@transducer/result'] = _xfBase_result), + (XUniqWith.prototype['@@transducer/step'] = function (s, o) { + return _includesWith(this.pred, o, this.items) + ? s + : (this.items.push(o), this.xf['@@transducer/step'](s, o)); + }), + XUniqWith + ); + })(); + function _xuniqWith(s) { + return function (o) { + return new SS(s, o); + }; + } + var xS = _curry2( + _dispatchable([], _xuniqWith, function (s, o) { + for (var i, u = 0, _ = o.length, w = []; u < _; ) + (_includesWith(s, (i = o[u]), w) || (w[w.length] = i), (u += 1)); + return w; + }) + ); + const kS = xS, + normalize_parameters = + ({ storageField: s = 'x-normalized' } = {}) => + (o) => { + const { predicates: i, ancestorLineageToJSONPointer: u } = o, + parameterEquals = (s, o) => + !!i.isParameterElement(s) && + !!i.isParameterElement(o) && + !!i.isStringElement(s.name) && + !!i.isStringElement(s.in) && + !!i.isStringElement(o.name) && + !!i.isStringElement(o.in) && + serializers_value(s.name) === serializers_value(o.name) && + serializers_value(s.in) === serializers_value(o.in), + _ = []; + let w; + return { + visitor: { + OpenApi3_1Element: { + enter(o) { + w = new wS(o, s, 'parameters'); + }, + leave() { + w = void 0; + } + }, + PathItemElement: { + enter(s, o, u, w, x) { + if (x.some(i.isComponentsElement)) return; + const { parameters: C } = s; + i.isArrayElement(C) ? _.push([...C.content]) : _.push([]); + }, + leave() { + _.pop(); + } + }, + OperationElement: { + leave(s, o, i, x, C) { + const j = Fa(_); + if (!Array.isArray(j) || 0 === j.length) return; + const L = u([...C, i, s]); + if (w.includes(L)) return; + const B = Ww([], ['parameters', 'content'], s), + $ = kS(parameterEquals, [...B, ...j]); + ((s.parameters = new yv($)), w.append(L)); + } + } + } + }; + }, + normalize_security_requirements = + ({ storageField: s = 'x-normalized' } = {}) => + (o) => { + const { predicates: i, ancestorLineageToJSONPointer: u } = o; + let _, w; + return { + visitor: { + OpenApi3_1Element: { + enter(o) { + ((w = new wS(o, s, 'security-requirements')), + i.isArrayElement(o.security) && (_ = o.security)); + }, + leave() { + ((w = void 0), (_ = void 0)); + } + }, + OperationElement: { + leave(s, o, x, C, j) { + if (j.some(i.isComponentsElement)) return; + const L = u([...j, x, s]); + if (w.includes(L)) return; + var B; + void 0 === s.security && + void 0 !== _ && + ((s.security = new Sv( + null === (B = _) || void 0 === B ? void 0 : B.content + )), + w.append(L)); + } + } + } + }; + }, + normalize_parameter_examples = + ({ storageField: s = 'x-normalized' } = {}) => + (o) => { + const { predicates: i, ancestorLineageToJSONPointer: u } = o; + let _; + return { + visitor: { + OpenApi3_1Element: { + enter(o) { + _ = new wS(o, s, 'parameter-examples'); + }, + leave() { + _ = void 0; + } + }, + ParameterElement: { + leave(s, o, w, x, C) { + var j, L; + if (C.some(i.isComponentsElement)) return; + if (void 0 === s.schema || !i.isSchemaElement(s.schema)) return; + if ( + void 0 === (null === (j = s.schema) || void 0 === j ? void 0 : j.example) && + void 0 === (null === (L = s.schema) || void 0 === L ? void 0 : L.examples) + ) + return; + const B = u([...C, w, s]); + if (!_.includes(B)) { + if (void 0 !== s.examples && i.isObjectElement(s.examples)) { + const o = s.examples.map((s) => cloneDeep.safe(s.value)); + return ( + void 0 !== s.schema.examples && + (s.schema.set('examples', o), _.append(B)), + void ( + void 0 !== s.schema.example && + (s.schema.set('example', o[0]), _.append(B)) + ) + ); + } + void 0 !== s.example && + (void 0 !== s.schema.examples && + (s.schema.set('examples', [cloneDeep(s.example)]), _.append(B)), + void 0 !== s.schema.example && + (s.schema.set('example', cloneDeep(s.example)), _.append(B))); + } + } + } + } + }; + }, + normalize_header_examples = + ({ storageField: s = 'x-normalized' } = {}) => + (o) => { + const { predicates: i, ancestorLineageToJSONPointer: u } = o; + let _; + return { + visitor: { + OpenApi3_1Element: { + enter(o) { + _ = new wS(o, s, 'header-examples'); + }, + leave() { + _ = void 0; + } + }, + HeaderElement: { + leave(s, o, w, x, C) { + var j, L; + if (C.some(i.isComponentsElement)) return; + if (void 0 === s.schema || !i.isSchemaElement(s.schema)) return; + if ( + void 0 === (null === (j = s.schema) || void 0 === j ? void 0 : j.example) && + void 0 === (null === (L = s.schema) || void 0 === L ? void 0 : L.examples) + ) + return; + const B = u([...C, w, s]); + if (!_.includes(B)) { + if (void 0 !== s.examples && i.isObjectElement(s.examples)) { + const o = s.examples.map((s) => cloneDeep.safe(s.value)); + return ( + void 0 !== s.schema.examples && + (s.schema.set('examples', o), _.append(B)), + void ( + void 0 !== s.schema.example && + (s.schema.set('example', o[0]), _.append(B)) + ) + ); + } + void 0 !== s.example && + (void 0 !== s.schema.examples && + (s.schema.set('examples', [cloneDeep(s.example)]), _.append(B)), + void 0 !== s.schema.example && + (s.schema.set('example', cloneDeep(s.example)), _.append(B))); + } + } + } + } + }; + }, + openapi_3_1_apidom_normalize = (s) => { + if (!Fu(s)) return s; + const o = [ + normalize_operation_ids({ + operationIdNormalizer: (s, o, i) => + opId({ operationId: s }, o, i, { v2OperationIdCompatibilityMode: !1 }) + }), + normalize_parameters(), + normalize_security_requirements(), + normalize_parameter_examples(), + normalize_header_examples() + ]; + return dispatchPluginsSync(s, o, { + toolboxCreator: apidom_ns_openapi_3_1_es_refractor_toolbox, + visitorOptions: { + keyMap: nw, + nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType + } + }); + }, + CS = { + name: 'openapi-3-1-apidom', + match: (s) => isOpenAPI31(s), + normalize(s) { + if (!Nu(s) && ku(s) && !s.$$normalized) { + const i = ((o = openapi_3_1_apidom_normalize), + (s) => { + const i = wb.refract(s); + i.classes.push('result'); + const u = o(i), + _ = serializers_value(u); + return (yS.cache.set(_, u), serializers_value(u)); + })(s); + return ((i.$$normalized = !0), i); + } + var o; + return Nu(s) ? openapi_3_1_apidom_normalize(s) : s; + }, + resolve: async (s) => yS(s) + }, + OS = CS, + makeResolve = (s) => async (o) => + (async (s) => { + const { spec: o, requestInterceptor: i, responseInterceptor: u } = s, + _ = options_retrievalURI(s), + w = options_httpClient(s), + x = + o || + (await makeFetchJSON(w, { requestInterceptor: i, responseInterceptor: u })(_)), + C = { ...s, spec: x }; + return s.strategies.find((s) => s.match(x)).resolve(C); + })({ ...s, ...o }), + AS = makeResolve({ strategies: [fu, hu, uu] }); + var jS = __webpack_require__(69883); + const IS = function fnparser() { + const s = TS, + o = MS, + i = this, + u = 'parser.js: Parser(): '; + ((i.ast = void 0), (i.stats = void 0), (i.trace = void 0), (i.callbacks = [])); + let _, + w, + x, + C, + j, + L, + B, + $ = 0, + V = 0, + U = 0, + z = 0, + Y = 0, + Z = new (function systemData() { + ((this.state = s.ACTIVE), + (this.phraseLength = 0), + (this.refresh = () => { + ((this.state = s.ACTIVE), (this.phraseLength = 0)); + })); + })(); + i.parse = (ee, ie, ae, le) => { + const ce = `${u}parse(): `; + (($ = 0), + (V = 0), + (U = 0), + (z = 0), + (Y = 0), + (_ = void 0), + (w = void 0), + (x = void 0), + (C = void 0), + Z.refresh(), + (j = void 0), + (L = void 0), + (B = void 0), + (C = o.stringToChars(ae)), + (_ = ee.rules), + (w = ee.udts)); + const pe = ie.toLowerCase(); + let de; + for (const s in _) + if (_.hasOwnProperty(s) && pe === _[s].lower) { + de = _[s].index; + break; + } + if (void 0 === de) + throw new Error(`${ce}start rule name '${startRule}' not recognized`); + ((() => { + const s = `${u}initializeCallbacks(): `; + let o, x; + for (j = [], L = [], o = 0; o < _.length; o += 1) j[o] = void 0; + for (o = 0; o < w.length; o += 1) L[o] = void 0; + const C = []; + for (o = 0; o < _.length; o += 1) C.push(_[o].lower); + for (o = 0; o < w.length; o += 1) C.push(w[o].lower); + for (const u in i.callbacks) + if (i.callbacks.hasOwnProperty(u)) { + if (((o = C.indexOf(u.toLowerCase())), o < 0)) + throw new Error(`${s}syntax callback '${u}' not a rule or udt name`); + if ( + ((x = i.callbacks[u] ? i.callbacks[u] : void 0), + 'function' != typeof x && void 0 !== x) + ) + throw new Error( + `${s}syntax callback[${u}] must be function reference or falsy)` + ); + o < _.length ? (j[o] = x) : (L[o - _.length] = x); + } + })(), + i.trace && i.trace.init(_, w, C), + i.stats && i.stats.init(_, w), + i.ast && i.ast.init(_, w, C), + (B = le), + (x = [{ type: s.RNM, index: de }]), + opExecute(0, 0), + (x = void 0)); + let fe = !1; + switch (Z.state) { + case s.ACTIVE: + throw new Error(`${ce}final state should never be 'ACTIVE'`); + case s.NOMATCH: + fe = !1; + break; + case s.EMPTY: + case s.MATCH: + fe = Z.phraseLength === C.length; + break; + default: + throw new Error('unrecognized state'); + } + return { + success: fe, + state: Z.state, + stateName: s.idName(Z.state), + length: C.length, + matched: Z.phraseLength, + maxMatched: Y, + maxTreeDepth: U, + nodeHits: z + }; + }; + const validateRnmCallbackResult = (o, i, _, w) => { + if (i.phraseLength > _) { + let s = `${u}opRNM(${o.name}): callback function error: `; + throw ( + (s += `sysData.phraseLength: ${i.phraseLength}`), + (s += ` must be <= remaining chars: ${_}`), + new Error(s) + ); + } + switch (i.state) { + case s.ACTIVE: + if (!w) + throw new Error( + `${u}opRNM(${o.name}): callback function return error. ACTIVE state not allowed.` + ); + break; + case s.EMPTY: + i.phraseLength = 0; + break; + case s.MATCH: + 0 === i.phraseLength && (i.state = s.EMPTY); + break; + case s.NOMATCH: + i.phraseLength = 0; + break; + default: + throw new Error( + `${u}opRNM(${o.name}): callback function return error. Unrecognized return state: ${i.state}` + ); + } + }, + opUDT = (o, j) => { + let V, U, z; + const Y = x[o], + ee = w[Y.index]; + ((Z.UdtIndex = ee.index), + $ || + ((z = i.ast && i.ast.udtDefined(Y.index)), + z && + ((U = _.length + Y.index), (V = i.ast.getLength()), i.ast.down(U, ee.name)))); + const ie = C.length - j; + (L[Y.index](Z, C, j, B), + ((o, i, _) => { + if (i.phraseLength > _) { + let s = `${u}opUDT(${o.name}): callback function error: `; + throw ( + (s += `sysData.phraseLength: ${i.phraseLength}`), + (s += ` must be <= remaining chars: ${_}`), + new Error(s) + ); + } + switch (i.state) { + case s.ACTIVE: + throw new Error(`${u}opUDT(${o.name}) ACTIVE state return not allowed.`); + case s.EMPTY: + if (!o.empty) throw new Error(`${u}opUDT(${o.name}) may not return EMPTY.`); + i.phraseLength = 0; + break; + case s.MATCH: + if (0 === i.phraseLength) { + if (!o.empty) + throw new Error(`${u}opUDT(${o.name}) may not return EMPTY.`); + i.state = s.EMPTY; + } + break; + case s.NOMATCH: + i.phraseLength = 0; + break; + default: + throw new Error( + `${u}opUDT(${o.name}): callback function return error. Unrecognized return state: ${i.state}` + ); + } + })(ee, Z, ie), + $ || + (z && + (Z.state === s.NOMATCH + ? i.ast.setLength(V) + : i.ast.up(U, ee.name, j, Z.phraseLength)))); + }, + opExecute = (o, w) => { + const L = `${u}opExecute(): `, + ee = x[o]; + switch ( + ((z += 1), + V > U && (U = V), + (V += 1), + Z.refresh(), + i.trace && i.trace.down(ee, w), + ee.type) + ) { + case s.ALT: + ((o, i) => { + const u = x[o]; + for ( + let o = 0; + o < u.children.length && + (opExecute(u.children[o], i), Z.state === s.NOMATCH); + o += 1 + ); + })(o, w); + break; + case s.CAT: + ((o, u) => { + let _, w, C, j; + const L = x[o]; + (i.ast && (w = i.ast.getLength()), (_ = !0), (C = u), (j = 0)); + for (let o = 0; o < L.children.length; o += 1) { + if ((opExecute(L.children[o], C), Z.state === s.NOMATCH)) { + _ = !1; + break; + } + ((C += Z.phraseLength), (j += Z.phraseLength)); + } + _ + ? ((Z.state = 0 === j ? s.EMPTY : s.MATCH), (Z.phraseLength = j)) + : ((Z.state = s.NOMATCH), + (Z.phraseLength = 0), + i.ast && i.ast.setLength(w)); + })(o, w); + break; + case s.REP: + ((o, u) => { + let _, w, j, L; + const B = x[o]; + if (0 === B.max) return ((Z.state = s.EMPTY), void (Z.phraseLength = 0)); + for ( + w = u, j = 0, L = 0, i.ast && (_ = i.ast.getLength()); + !(w >= C.length) && + (opExecute(o + 1, w), Z.state !== s.NOMATCH) && + Z.state !== s.EMPTY && + ((L += 1), (j += Z.phraseLength), (w += Z.phraseLength), L !== B.max); + ); + Z.state === s.EMPTY || L >= B.min + ? ((Z.state = 0 === j ? s.EMPTY : s.MATCH), (Z.phraseLength = j)) + : ((Z.state = s.NOMATCH), + (Z.phraseLength = 0), + i.ast && i.ast.setLength(_)); + })(o, w); + break; + case s.RNM: + ((o, u) => { + let w, L, V; + const U = x[o], + z = _[U.index], + Y = j[z.index]; + if ( + ($ || + ((L = i.ast && i.ast.ruleDefined(U.index)), + L && ((w = i.ast.getLength()), i.ast.down(U.index, _[U.index].name))), + Y) + ) { + const o = C.length - u; + (Y(Z, C, u, B), + validateRnmCallbackResult(z, Z, o, !0), + Z.state === s.ACTIVE && + ((V = x), + (x = z.opcodes), + opExecute(0, u), + (x = V), + Y(Z, C, u, B), + validateRnmCallbackResult(z, Z, o, !1))); + } else ((V = x), (x = z.opcodes), opExecute(0, u, Z), (x = V)); + $ || + (L && + (Z.state === s.NOMATCH + ? i.ast.setLength(w) + : i.ast.up(U.index, z.name, u, Z.phraseLength))); + })(o, w); + break; + case s.TRG: + ((o, i) => { + const u = x[o]; + ((Z.state = s.NOMATCH), + i < C.length && + u.min <= C[i] && + C[i] <= u.max && + ((Z.state = s.MATCH), (Z.phraseLength = 1))); + })(o, w); + break; + case s.TBS: + ((o, i) => { + const u = x[o], + _ = u.string.length; + if (((Z.state = s.NOMATCH), i + _ <= C.length)) { + for (let s = 0; s < _; s += 1) if (C[i + s] !== u.string[s]) return; + ((Z.state = s.MATCH), (Z.phraseLength = _)); + } + })(o, w); + break; + case s.TLS: + ((o, i) => { + let u; + const _ = x[o]; + Z.state = s.NOMATCH; + const w = _.string.length; + if (0 !== w) { + if (i + w <= C.length) { + for (let s = 0; s < w; s += 1) + if ( + ((u = C[i + s]), u >= 65 && u <= 90 && (u += 32), u !== _.string[s]) + ) + return; + ((Z.state = s.MATCH), (Z.phraseLength = w)); + } + } else Z.state = s.EMPTY; + })(o, w); + break; + case s.UDT: + opUDT(o, w); + break; + case s.AND: + ((o, i) => { + switch ( + (($ += 1), opExecute(o + 1, i), ($ -= 1), (Z.phraseLength = 0), Z.state) + ) { + case s.EMPTY: + case s.MATCH: + Z.state = s.EMPTY; + break; + case s.NOMATCH: + Z.state = s.NOMATCH; + break; + default: + throw new Error(`opAND: invalid state ${Z.state}`); + } + })(o, w); + break; + case s.NOT: + ((o, i) => { + switch ( + (($ += 1), opExecute(o + 1, i), ($ -= 1), (Z.phraseLength = 0), Z.state) + ) { + case s.EMPTY: + case s.MATCH: + Z.state = s.NOMATCH; + break; + case s.NOMATCH: + Z.state = s.EMPTY; + break; + default: + throw new Error(`opNOT: invalid state ${Z.state}`); + } + })(o, w); + break; + default: + throw new Error(`${L}unrecognized operator`); + } + ($ || (w + Z.phraseLength > Y && (Y = w + Z.phraseLength)), + i.stats && i.stats.collect(ee, Z), + i.trace && i.trace.up(ee, Z.state, w, Z.phraseLength), + (V -= 1)); + }; + }, + PS = function fnast() { + const s = TS, + o = MS, + i = this; + let u, + _, + w, + x = 0; + const C = [], + j = [], + L = []; + function indent(s) { + let o = ''; + for (; s-- > 0; ) o += ' '; + return o; + } + ((i.callbacks = []), + (i.init = (s, o, B) => { + let $; + ((j.length = 0), (L.length = 0), (x = 0), (u = s), (_ = o), (w = B)); + const V = []; + for ($ = 0; $ < u.length; $ += 1) V.push(u[$].lower); + for ($ = 0; $ < _.length; $ += 1) V.push(_[$].lower); + for (x = u.length + _.length, $ = 0; $ < x; $ += 1) C[$] = void 0; + for (const s in i.callbacks) + if (i.callbacks.hasOwnProperty(s)) { + const o = s.toLowerCase(); + if ((($ = V.indexOf(o)), $ < 0)) + throw new Error( + `parser.js: Ast()): init: node '${s}' not a rule or udt name` + ); + C[$] = i.callbacks[s]; + } + }), + (i.ruleDefined = (s) => !!C[s]), + (i.udtDefined = (s) => !!C[u.length + s]), + (i.down = (o, i) => { + const u = L.length; + return ( + j.push(u), + L.push({ + name: i, + thisIndex: u, + thatIndex: void 0, + state: s.SEM_PRE, + callbackIndex: o, + phraseIndex: void 0, + phraseLength: void 0, + stack: j.length + }), + u + ); + }), + (i.up = (o, i, u, _) => { + const w = L.length, + x = j.pop(); + return ( + L.push({ + name: i, + thisIndex: w, + thatIndex: x, + state: s.SEM_POST, + callbackIndex: o, + phraseIndex: u, + phraseLength: _, + stack: j.length + }), + (L[x].thatIndex = w), + (L[x].phraseIndex = u), + (L[x].phraseLength = _), + w + ); + }), + (i.translate = (o) => { + let i, u; + for (let _ = 0; _ < L.length; _ += 1) + ((u = L[_]), + (i = C[u.callbackIndex]), + i && + (u.state === s.SEM_PRE + ? i(s.SEM_PRE, w, u.phraseIndex, u.phraseLength, o) + : i && i(s.SEM_POST, w, u.phraseIndex, u.phraseLength, o))); + }), + (i.setLength = (s) => { + ((L.length = s), (j.length = s > 0 ? L[s - 1].stack : 0)); + }), + (i.getLength = () => L.length), + (i.toXml = () => { + let i = '', + u = 0; + return ( + (i += '\n'), + (i += `\n`), + (i += '\x3c!-- input string --\x3e\n'), + (i += indent(u + 2)), + (i += o.charsToString(w)), + (i += '\n'), + L.forEach((_) => { + _.state === s.SEM_PRE + ? ((u += 1), + (i += indent(u)), + (i += `\n`), + (i += indent(u + 2)), + (i += o.charsToString(w, _.phraseIndex, _.phraseLength)), + (i += '\n')) + : ((i += indent(u)), + (i += `\x3c!-- name="${_.name}" --\x3e\n`), + (u -= 1)); + }), + (i += '\n'), + i + ); + })); + }, + MS = { + stringToChars: (s) => [...s].map((s) => s.codePointAt(0)), + charsToString: (s, o, i) => { + let u = s; + for (; !(void 0 === o || o < 0); ) { + if (void 0 === i) { + u = s.slice(o); + break; + } + if (i <= 0) return ''; + u = s.slice(o, o + i); + break; + } + return String.fromCodePoint(...u); + } + }, + TS = { + ALT: 1, + CAT: 2, + REP: 3, + RNM: 4, + TRG: 5, + TBS: 6, + TLS: 7, + UDT: 11, + AND: 12, + NOT: 13, + ACTIVE: 100, + MATCH: 101, + EMPTY: 102, + NOMATCH: 103, + SEM_PRE: 200, + SEM_POST: 201, + SEM_OK: 300, + idName: (s) => { + switch (s) { + case TS.ALT: + return 'ALT'; + case TS.CAT: + return 'CAT'; + case TS.REP: + return 'REP'; + case TS.RNM: + return 'RNM'; + case TS.TRG: + return 'TRG'; + case TS.TBS: + return 'TBS'; + case TS.TLS: + return 'TLS'; + case TS.UDT: + return 'UDT'; + case TS.AND: + return 'AND'; + case TS.NOT: + return 'NOT'; + case TS.ACTIVE: + return 'ACTIVE'; + case TS.EMPTY: + return 'EMPTY'; + case TS.MATCH: + return 'MATCH'; + case TS.NOMATCH: + return 'NOMATCH'; + case TS.SEM_PRE: + return 'SEM_PRE'; + case TS.SEM_POST: + return 'SEM_POST'; + case TS.SEM_OK: + return 'SEM_OK'; + default: + return 'UNRECOGNIZED STATE'; + } + } + }; + const server_url_template = (s, o, i, u, _) => { + if (s === TS.SEM_PRE) { + if (!1 === Array.isArray(_)) throw new Error("parser's user data must be an array"); + _.push(['server-url-template', MS.charsToString(o, i, u)]); + } + return TS.SEM_OK; + }, + callbacks_server_variable = (s, o, i, u, _) => { + if (s === TS.SEM_PRE) { + if (!1 === Array.isArray(_)) throw new Error("parser's user data must be an array"); + _.push(['server-variable', MS.charsToString(o, i, u)]); + } + return TS.SEM_OK; + }, + server_variable_name = (s, o, i, u, _) => { + if (s === TS.SEM_PRE) { + if (!1 === Array.isArray(_)) throw new Error("parser's user data must be an array"); + _.push(['server-variable-name', MS.charsToString(o, i, u)]); + } + return TS.SEM_OK; + }, + callbacks_literals = (s, o, i, u, _) => { + if (s === TS.SEM_PRE) { + if (!1 === Array.isArray(_)) throw new Error("parser's user data must be an array"); + _.push(['literals', MS.charsToString(o, i, u)]); + } + return TS.SEM_OK; + }, + NS = new (function grammar() { + ((this.grammarObject = 'grammarObject'), + (this.rules = []), + (this.rules[0] = { + name: 'server-url-template', + lower: 'server-url-template', + index: 0, + isBkr: !1 + }), + (this.rules[1] = { + name: 'server-variable', + lower: 'server-variable', + index: 1, + isBkr: !1 + }), + (this.rules[2] = { + name: 'server-variable-name', + lower: 'server-variable-name', + index: 2, + isBkr: !1 + }), + (this.rules[3] = { name: 'literals', lower: 'literals', index: 3, isBkr: !1 }), + (this.rules[4] = { name: 'ALPHA', lower: 'alpha', index: 4, isBkr: !1 }), + (this.rules[5] = { name: 'DIGIT', lower: 'digit', index: 5, isBkr: !1 }), + (this.rules[6] = { name: 'HEXDIG', lower: 'hexdig', index: 6, isBkr: !1 }), + (this.rules[7] = { name: 'pct-encoded', lower: 'pct-encoded', index: 7, isBkr: !1 }), + (this.rules[8] = { name: 'unreserved', lower: 'unreserved', index: 8, isBkr: !1 }), + (this.rules[9] = { name: 'sub-delims', lower: 'sub-delims', index: 9, isBkr: !1 }), + (this.rules[10] = { name: 'ucschar', lower: 'ucschar', index: 10, isBkr: !1 }), + (this.rules[11] = { name: 'iprivate', lower: 'iprivate', index: 11, isBkr: !1 }), + (this.udts = []), + (this.rules[0].opcodes = []), + (this.rules[0].opcodes[0] = { type: 3, min: 1, max: 1 / 0 }), + (this.rules[0].opcodes[1] = { type: 1, children: [2, 3] }), + (this.rules[0].opcodes[2] = { type: 4, index: 3 }), + (this.rules[0].opcodes[3] = { type: 4, index: 1 }), + (this.rules[1].opcodes = []), + (this.rules[1].opcodes[0] = { type: 2, children: [1, 2, 3] }), + (this.rules[1].opcodes[1] = { type: 7, string: [123] }), + (this.rules[1].opcodes[2] = { type: 4, index: 2 }), + (this.rules[1].opcodes[3] = { type: 7, string: [125] }), + (this.rules[2].opcodes = []), + (this.rules[2].opcodes[0] = { type: 3, min: 1, max: 1 / 0 }), + (this.rules[2].opcodes[1] = { type: 1, children: [2, 3, 4, 5, 6] }), + (this.rules[2].opcodes[2] = { type: 4, index: 8 }), + (this.rules[2].opcodes[3] = { type: 4, index: 7 }), + (this.rules[2].opcodes[4] = { type: 4, index: 9 }), + (this.rules[2].opcodes[5] = { type: 7, string: [58] }), + (this.rules[2].opcodes[6] = { type: 7, string: [64] }), + (this.rules[3].opcodes = []), + (this.rules[3].opcodes[0] = { type: 3, min: 1, max: 1 / 0 }), + (this.rules[3].opcodes[1] = { + type: 1, + children: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] + }), + (this.rules[3].opcodes[2] = { type: 6, string: [33] }), + (this.rules[3].opcodes[3] = { type: 5, min: 35, max: 36 }), + (this.rules[3].opcodes[4] = { type: 6, string: [38] }), + (this.rules[3].opcodes[5] = { type: 5, min: 40, max: 59 }), + (this.rules[3].opcodes[6] = { type: 6, string: [61] }), + (this.rules[3].opcodes[7] = { type: 5, min: 63, max: 91 }), + (this.rules[3].opcodes[8] = { type: 6, string: [93] }), + (this.rules[3].opcodes[9] = { type: 6, string: [95] }), + (this.rules[3].opcodes[10] = { type: 5, min: 97, max: 122 }), + (this.rules[3].opcodes[11] = { type: 6, string: [126] }), + (this.rules[3].opcodes[12] = { type: 4, index: 10 }), + (this.rules[3].opcodes[13] = { type: 4, index: 11 }), + (this.rules[3].opcodes[14] = { type: 4, index: 7 }), + (this.rules[4].opcodes = []), + (this.rules[4].opcodes[0] = { type: 1, children: [1, 2] }), + (this.rules[4].opcodes[1] = { type: 5, min: 65, max: 90 }), + (this.rules[4].opcodes[2] = { type: 5, min: 97, max: 122 }), + (this.rules[5].opcodes = []), + (this.rules[5].opcodes[0] = { type: 5, min: 48, max: 57 }), + (this.rules[6].opcodes = []), + (this.rules[6].opcodes[0] = { type: 1, children: [1, 2, 3, 4, 5, 6, 7] }), + (this.rules[6].opcodes[1] = { type: 4, index: 5 }), + (this.rules[6].opcodes[2] = { type: 7, string: [97] }), + (this.rules[6].opcodes[3] = { type: 7, string: [98] }), + (this.rules[6].opcodes[4] = { type: 7, string: [99] }), + (this.rules[6].opcodes[5] = { type: 7, string: [100] }), + (this.rules[6].opcodes[6] = { type: 7, string: [101] }), + (this.rules[6].opcodes[7] = { type: 7, string: [102] }), + (this.rules[7].opcodes = []), + (this.rules[7].opcodes[0] = { type: 2, children: [1, 2, 3] }), + (this.rules[7].opcodes[1] = { type: 7, string: [37] }), + (this.rules[7].opcodes[2] = { type: 4, index: 6 }), + (this.rules[7].opcodes[3] = { type: 4, index: 6 }), + (this.rules[8].opcodes = []), + (this.rules[8].opcodes[0] = { type: 1, children: [1, 2, 3, 4, 5, 6] }), + (this.rules[8].opcodes[1] = { type: 4, index: 4 }), + (this.rules[8].opcodes[2] = { type: 4, index: 5 }), + (this.rules[8].opcodes[3] = { type: 7, string: [45] }), + (this.rules[8].opcodes[4] = { type: 7, string: [46] }), + (this.rules[8].opcodes[5] = { type: 7, string: [95] }), + (this.rules[8].opcodes[6] = { type: 7, string: [126] }), + (this.rules[9].opcodes = []), + (this.rules[9].opcodes[0] = { + type: 1, + children: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + }), + (this.rules[9].opcodes[1] = { type: 7, string: [33] }), + (this.rules[9].opcodes[2] = { type: 7, string: [36] }), + (this.rules[9].opcodes[3] = { type: 7, string: [38] }), + (this.rules[9].opcodes[4] = { type: 7, string: [39] }), + (this.rules[9].opcodes[5] = { type: 7, string: [40] }), + (this.rules[9].opcodes[6] = { type: 7, string: [41] }), + (this.rules[9].opcodes[7] = { type: 7, string: [42] }), + (this.rules[9].opcodes[8] = { type: 7, string: [43] }), + (this.rules[9].opcodes[9] = { type: 7, string: [44] }), + (this.rules[9].opcodes[10] = { type: 7, string: [59] }), + (this.rules[9].opcodes[11] = { type: 7, string: [61] }), + (this.rules[10].opcodes = []), + (this.rules[10].opcodes[0] = { + type: 1, + children: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17] + }), + (this.rules[10].opcodes[1] = { type: 5, min: 160, max: 55295 }), + (this.rules[10].opcodes[2] = { type: 5, min: 63744, max: 64975 }), + (this.rules[10].opcodes[3] = { type: 5, min: 65008, max: 65519 }), + (this.rules[10].opcodes[4] = { type: 5, min: 65536, max: 131069 }), + (this.rules[10].opcodes[5] = { type: 5, min: 131072, max: 196605 }), + (this.rules[10].opcodes[6] = { type: 5, min: 196608, max: 262141 }), + (this.rules[10].opcodes[7] = { type: 5, min: 262144, max: 327677 }), + (this.rules[10].opcodes[8] = { type: 5, min: 327680, max: 393213 }), + (this.rules[10].opcodes[9] = { type: 5, min: 393216, max: 458749 }), + (this.rules[10].opcodes[10] = { type: 5, min: 458752, max: 524285 }), + (this.rules[10].opcodes[11] = { type: 5, min: 524288, max: 589821 }), + (this.rules[10].opcodes[12] = { type: 5, min: 589824, max: 655357 }), + (this.rules[10].opcodes[13] = { type: 5, min: 655360, max: 720893 }), + (this.rules[10].opcodes[14] = { type: 5, min: 720896, max: 786429 }), + (this.rules[10].opcodes[15] = { type: 5, min: 786432, max: 851965 }), + (this.rules[10].opcodes[16] = { type: 5, min: 851968, max: 917501 }), + (this.rules[10].opcodes[17] = { type: 5, min: 921600, max: 983037 }), + (this.rules[11].opcodes = []), + (this.rules[11].opcodes[0] = { type: 1, children: [1, 2, 3] }), + (this.rules[11].opcodes[1] = { type: 5, min: 57344, max: 63743 }), + (this.rules[11].opcodes[2] = { type: 5, min: 983040, max: 1048573 }), + (this.rules[11].opcodes[3] = { type: 5, min: 1048576, max: 1114109 }), + (this.toString = function toString() { + let s = ''; + return ( + (s += '; OpenAPI Server URL templating ABNF syntax\n'), + (s += 'server-url-template = 1*( literals / server-variable )\n'), + (s += 'server-variable = "{" server-variable-name "}"\n'), + (s += + 'server-variable-name = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" )\n'), + (s += + 'literals = 1*( %x21 / %x23-24 / %x26 / %x28-3B / %x3D / %x3F-5B\n'), + (s += + ' / %x5D / %x5F / %x61-7A / %x7E / ucschar / iprivate\n'), + (s += ' / pct-encoded)\n'), + (s += ' ; any Unicode character except: CTL, SP,\n'), + (s += + ' ; DQUOTE, "\'", "%" (aside from pct-encoded),\n'), + (s += ' ; "<", ">", "\\", "^", "`", "{", "|", "}"\n'), + (s += '\n'), + (s += '; Characters definitions (from RFC 6570)\n'), + (s += 'ALPHA = %x41-5A / %x61-7A ; A-Z / a-z\n'), + (s += 'DIGIT = %x30-39 ; 0-9\n'), + (s += 'HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"\n'), + (s += ' ; case-insensitive\n'), + (s += '\n'), + (s += 'pct-encoded = "%" HEXDIG HEXDIG\n'), + (s += 'unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"\n'), + (s += 'sub-delims = "!" / "$" / "&" / "\'" / "(" / ")"\n'), + (s += ' / "*" / "+" / "," / ";" / "="\n'), + (s += '\n'), + (s += 'ucschar = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF\n'), + (s += ' / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD\n'), + (s += ' / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD\n'), + (s += ' / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD\n'), + (s += ' / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD\n'), + (s += ' / %xD0000-DFFFD / %xE1000-EFFFD\n'), + (s += '\n'), + (s += 'iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD\n'), + '; OpenAPI Server URL templating ABNF syntax\nserver-url-template = 1*( literals / server-variable )\nserver-variable = "{" server-variable-name "}"\nserver-variable-name = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" )\nliterals = 1*( %x21 / %x23-24 / %x26 / %x28-3B / %x3D / %x3F-5B\n / %x5D / %x5F / %x61-7A / %x7E / ucschar / iprivate\n / pct-encoded)\n ; any Unicode character except: CTL, SP,\n ; DQUOTE, "\'", "%" (aside from pct-encoded),\n ; "<", ">", "\\", "^", "`", "{", "|", "}"\n\n; Characters definitions (from RFC 6570)\nALPHA = %x41-5A / %x61-7A ; A-Z / a-z\nDIGIT = %x30-39 ; 0-9\nHEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"\n ; case-insensitive\n\npct-encoded = "%" HEXDIG HEXDIG\nunreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"\nsub-delims = "!" / "$" / "&" / "\'" / "(" / ")"\n / "*" / "+" / "," / ";" / "="\n\nucschar = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF\n / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD\n / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD\n / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD\n / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD\n / %xD0000-DFFFD / %xE1000-EFFFD\n\niprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD\n' + ); + })); + })(), + openapi_server_url_templating_es_parse = (s) => { + const o = new IS(); + ((o.ast = new PS()), + (o.ast.callbacks['server-url-template'] = server_url_template), + (o.ast.callbacks['server-variable'] = callbacks_server_variable), + (o.ast.callbacks['server-variable-name'] = server_variable_name), + (o.ast.callbacks.literals = callbacks_literals)); + return { result: o.parse(NS, 'server-url-template', s), ast: o.ast }; + }, + openapi_server_url_templating_es_test = (s, { strict: o = !1 } = {}) => { + try { + const i = openapi_server_url_templating_es_parse(s); + if (!i.result.success) return !1; + const u = []; + i.ast.translate(u); + const _ = u.some(([s]) => 'server-variable' === s); + if (!o && !_) + try { + return (new URL(s, 'https://vladimirgorej.com'), !0); + } catch { + return !1; + } + return !o || _; + } catch { + return !1; + } + }, + encodeServerVariable = (s) => + ((s) => { + try { + return 'string' == typeof s && decodeURIComponent(s) !== s; + } catch { + return !1; + } + })(s) + ? s + : encodeURIComponent(s).replace(/%5B/g, '[').replace(/%5D/g, ']'), + RS = ['literals', 'server-variable-name'], + es_substitute = (s, o, i = {}) => { + const u = { ...{ encoder: encodeServerVariable }, ...i }, + _ = openapi_server_url_templating_es_parse(s); + if (!_.result.success) return s; + const w = []; + _.ast.translate(w); + const x = w + .filter(([s]) => RS.includes(s)) + .map(([s, i]) => + 'server-variable-name' === s + ? Object.hasOwn(o, i) + ? u.encoder(o[i], i) + : `{${i}}` + : i + ); + return x.join(''); + }; + const callbacks_slash = (s, o, i, u, _) => ( + s === TS.SEM_PRE ? _.push(['slash', MS.charsToString(o, i, u)]) : TS.SEM_POST, + TS.SEM_OK + ), + path_template = (s, o, i, u, _) => { + if (s === TS.SEM_PRE) { + if (!1 === Array.isArray(_)) throw new Error("parser's user data must be an array"); + _.push(['path-template', MS.charsToString(o, i, u)]); + } + return TS.SEM_OK; + }, + callbacks_path = (s, o, i, u, _) => ( + s === TS.SEM_PRE ? _.push(['path', MS.charsToString(o, i, u)]) : TS.SEM_POST, + TS.SEM_OK + ), + path_literal = (s, o, i, u, _) => ( + s === TS.SEM_PRE ? _.push(['path-literal', MS.charsToString(o, i, u)]) : TS.SEM_POST, + TS.SEM_OK + ), + callbacks_query = (s, o, i, u, _) => ( + s === TS.SEM_PRE ? _.push(['query', MS.charsToString(o, i, u)]) : TS.SEM_POST, + TS.SEM_OK + ), + query_marker = (s, o, i, u, _) => ( + s === TS.SEM_PRE ? _.push(['query-marker', MS.charsToString(o, i, u)]) : TS.SEM_POST, + TS.SEM_OK + ), + callbacks_fragment = (s, o, i, u, _) => ( + s === TS.SEM_PRE ? _.push(['fragment', MS.charsToString(o, i, u)]) : TS.SEM_POST, + TS.SEM_OK + ), + fragment_marker = (s, o, i, u, _) => ( + s === TS.SEM_PRE ? _.push(['fragment-marker', MS.charsToString(o, i, u)]) : TS.SEM_POST, + TS.SEM_OK + ), + template_expression = (s, o, i, u, _) => ( + s === TS.SEM_PRE + ? _.push(['template-expression', MS.charsToString(o, i, u)]) + : TS.SEM_POST, + TS.SEM_OK + ), + template_expression_param_name = (s, o, i, u, _) => ( + s === TS.SEM_PRE + ? _.push(['template-expression-param-name', MS.charsToString(o, i, u)]) + : TS.SEM_POST, + TS.SEM_OK + ), + DS = new (function path_templating_grammar() { + ((this.grammarObject = 'grammarObject'), + (this.rules = []), + (this.rules[0] = { + name: 'path-template', + lower: 'path-template', + index: 0, + isBkr: !1 + }), + (this.rules[1] = { name: 'path', lower: 'path', index: 1, isBkr: !1 }), + (this.rules[2] = { + name: 'path-segment', + lower: 'path-segment', + index: 2, + isBkr: !1 + }), + (this.rules[3] = { name: 'query', lower: 'query', index: 3, isBkr: !1 }), + (this.rules[4] = { + name: 'query-literal', + lower: 'query-literal', + index: 4, + isBkr: !1 + }), + (this.rules[5] = { + name: 'query-marker', + lower: 'query-marker', + index: 5, + isBkr: !1 + }), + (this.rules[6] = { name: 'fragment', lower: 'fragment', index: 6, isBkr: !1 }), + (this.rules[7] = { + name: 'fragment-literal', + lower: 'fragment-literal', + index: 7, + isBkr: !1 + }), + (this.rules[8] = { + name: 'fragment-marker', + lower: 'fragment-marker', + index: 8, + isBkr: !1 + }), + (this.rules[9] = { name: 'slash', lower: 'slash', index: 9, isBkr: !1 }), + (this.rules[10] = { + name: 'path-literal', + lower: 'path-literal', + index: 10, + isBkr: !1 + }), + (this.rules[11] = { + name: 'template-expression', + lower: 'template-expression', + index: 11, + isBkr: !1 + }), + (this.rules[12] = { + name: 'template-expression-param-name', + lower: 'template-expression-param-name', + index: 12, + isBkr: !1 + }), + (this.rules[13] = { name: 'unreserved', lower: 'unreserved', index: 13, isBkr: !1 }), + (this.rules[14] = { + name: 'pct-encoded', + lower: 'pct-encoded', + index: 14, + isBkr: !1 + }), + (this.rules[15] = { name: 'sub-delims', lower: 'sub-delims', index: 15, isBkr: !1 }), + (this.rules[16] = { name: 'ALPHA', lower: 'alpha', index: 16, isBkr: !1 }), + (this.rules[17] = { name: 'DIGIT', lower: 'digit', index: 17, isBkr: !1 }), + (this.rules[18] = { name: 'HEXDIG', lower: 'hexdig', index: 18, isBkr: !1 }), + (this.udts = []), + (this.rules[0].opcodes = []), + (this.rules[0].opcodes[0] = { type: 2, children: [1, 2, 6] }), + (this.rules[0].opcodes[1] = { type: 4, index: 1 }), + (this.rules[0].opcodes[2] = { type: 3, min: 0, max: 1 }), + (this.rules[0].opcodes[3] = { type: 2, children: [4, 5] }), + (this.rules[0].opcodes[4] = { type: 4, index: 5 }), + (this.rules[0].opcodes[5] = { type: 4, index: 3 }), + (this.rules[0].opcodes[6] = { type: 3, min: 0, max: 1 }), + (this.rules[0].opcodes[7] = { type: 2, children: [8, 9] }), + (this.rules[0].opcodes[8] = { type: 4, index: 8 }), + (this.rules[0].opcodes[9] = { type: 4, index: 6 }), + (this.rules[1].opcodes = []), + (this.rules[1].opcodes[0] = { type: 2, children: [1, 2, 6] }), + (this.rules[1].opcodes[1] = { type: 4, index: 9 }), + (this.rules[1].opcodes[2] = { type: 3, min: 0, max: 1 / 0 }), + (this.rules[1].opcodes[3] = { type: 2, children: [4, 5] }), + (this.rules[1].opcodes[4] = { type: 4, index: 2 }), + (this.rules[1].opcodes[5] = { type: 4, index: 9 }), + (this.rules[1].opcodes[6] = { type: 3, min: 0, max: 1 }), + (this.rules[1].opcodes[7] = { type: 4, index: 2 }), + (this.rules[2].opcodes = []), + (this.rules[2].opcodes[0] = { type: 3, min: 1, max: 1 / 0 }), + (this.rules[2].opcodes[1] = { type: 1, children: [2, 3] }), + (this.rules[2].opcodes[2] = { type: 4, index: 10 }), + (this.rules[2].opcodes[3] = { type: 4, index: 11 }), + (this.rules[3].opcodes = []), + (this.rules[3].opcodes[0] = { type: 3, min: 0, max: 1 / 0 }), + (this.rules[3].opcodes[1] = { type: 4, index: 4 }), + (this.rules[4].opcodes = []), + (this.rules[4].opcodes[0] = { type: 3, min: 1, max: 1 / 0 }), + (this.rules[4].opcodes[1] = { type: 1, children: [2, 3, 4, 5, 6, 7, 8, 9, 10] }), + (this.rules[4].opcodes[2] = { type: 4, index: 13 }), + (this.rules[4].opcodes[3] = { type: 4, index: 14 }), + (this.rules[4].opcodes[4] = { type: 4, index: 15 }), + (this.rules[4].opcodes[5] = { type: 7, string: [58] }), + (this.rules[4].opcodes[6] = { type: 7, string: [64] }), + (this.rules[4].opcodes[7] = { type: 7, string: [47] }), + (this.rules[4].opcodes[8] = { type: 7, string: [63] }), + (this.rules[4].opcodes[9] = { type: 7, string: [38] }), + (this.rules[4].opcodes[10] = { type: 7, string: [61] }), + (this.rules[5].opcodes = []), + (this.rules[5].opcodes[0] = { type: 7, string: [63] }), + (this.rules[6].opcodes = []), + (this.rules[6].opcodes[0] = { type: 3, min: 0, max: 1 / 0 }), + (this.rules[6].opcodes[1] = { type: 4, index: 7 }), + (this.rules[7].opcodes = []), + (this.rules[7].opcodes[0] = { type: 3, min: 1, max: 1 / 0 }), + (this.rules[7].opcodes[1] = { type: 1, children: [2, 3, 4, 5, 6, 7, 8] }), + (this.rules[7].opcodes[2] = { type: 4, index: 13 }), + (this.rules[7].opcodes[3] = { type: 4, index: 14 }), + (this.rules[7].opcodes[4] = { type: 4, index: 15 }), + (this.rules[7].opcodes[5] = { type: 7, string: [58] }), + (this.rules[7].opcodes[6] = { type: 7, string: [64] }), + (this.rules[7].opcodes[7] = { type: 7, string: [47] }), + (this.rules[7].opcodes[8] = { type: 7, string: [63] }), + (this.rules[8].opcodes = []), + (this.rules[8].opcodes[0] = { type: 7, string: [35] }), + (this.rules[9].opcodes = []), + (this.rules[9].opcodes[0] = { type: 7, string: [47] }), + (this.rules[10].opcodes = []), + (this.rules[10].opcodes[0] = { type: 3, min: 1, max: 1 / 0 }), + (this.rules[10].opcodes[1] = { type: 1, children: [2, 3, 4, 5, 6] }), + (this.rules[10].opcodes[2] = { type: 4, index: 13 }), + (this.rules[10].opcodes[3] = { type: 4, index: 14 }), + (this.rules[10].opcodes[4] = { type: 4, index: 15 }), + (this.rules[10].opcodes[5] = { type: 7, string: [58] }), + (this.rules[10].opcodes[6] = { type: 7, string: [64] }), + (this.rules[11].opcodes = []), + (this.rules[11].opcodes[0] = { type: 2, children: [1, 2, 3] }), + (this.rules[11].opcodes[1] = { type: 7, string: [123] }), + (this.rules[11].opcodes[2] = { type: 4, index: 12 }), + (this.rules[11].opcodes[3] = { type: 7, string: [125] }), + (this.rules[12].opcodes = []), + (this.rules[12].opcodes[0] = { type: 3, min: 1, max: 1 / 0 }), + (this.rules[12].opcodes[1] = { type: 1, children: [2, 3, 4, 5, 6] }), + (this.rules[12].opcodes[2] = { type: 4, index: 13 }), + (this.rules[12].opcodes[3] = { type: 4, index: 14 }), + (this.rules[12].opcodes[4] = { type: 4, index: 15 }), + (this.rules[12].opcodes[5] = { type: 7, string: [58] }), + (this.rules[12].opcodes[6] = { type: 7, string: [64] }), + (this.rules[13].opcodes = []), + (this.rules[13].opcodes[0] = { type: 1, children: [1, 2, 3, 4, 5, 6] }), + (this.rules[13].opcodes[1] = { type: 4, index: 16 }), + (this.rules[13].opcodes[2] = { type: 4, index: 17 }), + (this.rules[13].opcodes[3] = { type: 7, string: [45] }), + (this.rules[13].opcodes[4] = { type: 7, string: [46] }), + (this.rules[13].opcodes[5] = { type: 7, string: [95] }), + (this.rules[13].opcodes[6] = { type: 7, string: [126] }), + (this.rules[14].opcodes = []), + (this.rules[14].opcodes[0] = { type: 2, children: [1, 2, 3] }), + (this.rules[14].opcodes[1] = { type: 7, string: [37] }), + (this.rules[14].opcodes[2] = { type: 4, index: 18 }), + (this.rules[14].opcodes[3] = { type: 4, index: 18 }), + (this.rules[15].opcodes = []), + (this.rules[15].opcodes[0] = { + type: 1, + children: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + }), + (this.rules[15].opcodes[1] = { type: 7, string: [33] }), + (this.rules[15].opcodes[2] = { type: 7, string: [36] }), + (this.rules[15].opcodes[3] = { type: 7, string: [38] }), + (this.rules[15].opcodes[4] = { type: 7, string: [39] }), + (this.rules[15].opcodes[5] = { type: 7, string: [40] }), + (this.rules[15].opcodes[6] = { type: 7, string: [41] }), + (this.rules[15].opcodes[7] = { type: 7, string: [42] }), + (this.rules[15].opcodes[8] = { type: 7, string: [43] }), + (this.rules[15].opcodes[9] = { type: 7, string: [44] }), + (this.rules[15].opcodes[10] = { type: 7, string: [59] }), + (this.rules[15].opcodes[11] = { type: 7, string: [61] }), + (this.rules[16].opcodes = []), + (this.rules[16].opcodes[0] = { type: 1, children: [1, 2] }), + (this.rules[16].opcodes[1] = { type: 5, min: 65, max: 90 }), + (this.rules[16].opcodes[2] = { type: 5, min: 97, max: 122 }), + (this.rules[17].opcodes = []), + (this.rules[17].opcodes[0] = { type: 5, min: 48, max: 57 }), + (this.rules[18].opcodes = []), + (this.rules[18].opcodes[0] = { type: 1, children: [1, 2, 3, 4, 5, 6, 7] }), + (this.rules[18].opcodes[1] = { type: 4, index: 17 }), + (this.rules[18].opcodes[2] = { type: 7, string: [97] }), + (this.rules[18].opcodes[3] = { type: 7, string: [98] }), + (this.rules[18].opcodes[4] = { type: 7, string: [99] }), + (this.rules[18].opcodes[5] = { type: 7, string: [100] }), + (this.rules[18].opcodes[6] = { type: 7, string: [101] }), + (this.rules[18].opcodes[7] = { type: 7, string: [102] }), + (this.toString = function toString() { + let s = ''; + return ( + (s += '; OpenAPI Path Templating ABNF syntax\n'), + (s += + 'path-template = path [ query-marker query ] [ fragment-marker fragment ]\n'), + (s += + 'path = slash *( path-segment slash ) [ path-segment ]\n'), + (s += + 'path-segment = 1*( path-literal / template-expression )\n'), + (s += 'query = *( query-literal )\n'), + (s += + 'query-literal = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" / "&" / "=" )\n'), + (s += 'query-marker = "?"\n'), + (s += 'fragment = *( fragment-literal )\n'), + (s += + 'fragment-literal = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" )\n'), + (s += 'fragment-marker = "#"\n'), + (s += 'slash = "/"\n'), + (s += + 'path-literal = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" )\n'), + (s += + 'template-expression = "{" template-expression-param-name "}"\n'), + (s += + 'template-expression-param-name = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" )\n'), + (s += '\n'), + (s += '; Characters definitions (from RFC 3986)\n'), + (s += 'unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"\n'), + (s += 'pct-encoded = "%" HEXDIG HEXDIG\n'), + (s += 'sub-delims = "!" / "$" / "&" / "\'" / "(" / ")"\n'), + (s += ' / "*" / "+" / "," / ";" / "="\n'), + (s += 'ALPHA = %x41-5A / %x61-7A ; A-Z / a-z\n'), + (s += 'DIGIT = %x30-39 ; 0-9\n'), + (s += 'HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"\n'), + '; OpenAPI Path Templating ABNF syntax\npath-template = path [ query-marker query ] [ fragment-marker fragment ]\npath = slash *( path-segment slash ) [ path-segment ]\npath-segment = 1*( path-literal / template-expression )\nquery = *( query-literal )\nquery-literal = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" / "&" / "=" )\nquery-marker = "?"\nfragment = *( fragment-literal )\nfragment-literal = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" )\nfragment-marker = "#"\nslash = "/"\npath-literal = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" )\ntemplate-expression = "{" template-expression-param-name "}"\ntemplate-expression-param-name = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" )\n\n; Characters definitions (from RFC 3986)\nunreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"\npct-encoded = "%" HEXDIG HEXDIG\nsub-delims = "!" / "$" / "&" / "\'" / "(" / ")"\n / "*" / "+" / "," / ";" / "="\nALPHA = %x41-5A / %x61-7A ; A-Z / a-z\nDIGIT = %x30-39 ; 0-9\nHEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"\n' + ); + })); + })(), + openapi_path_templating_es_parse = (s) => { + const o = new IS(); + ((o.ast = new PS()), + (o.ast.callbacks['path-template'] = path_template), + (o.ast.callbacks.path = callbacks_path), + (o.ast.callbacks.query = callbacks_query), + (o.ast.callbacks['query-marker'] = query_marker), + (o.ast.callbacks.fragment = callbacks_fragment), + (o.ast.callbacks['fragment-marker'] = fragment_marker), + (o.ast.callbacks.slash = callbacks_slash), + (o.ast.callbacks['path-literal'] = path_literal), + (o.ast.callbacks['template-expression'] = template_expression), + (o.ast.callbacks['template-expression-param-name'] = template_expression_param_name)); + return { result: o.parse(DS, 'path-template', s), ast: o.ast }; + }, + encodePathComponent = (s) => + ((s) => { + try { + return 'string' == typeof s && decodeURIComponent(s) !== s; + } catch { + return !1; + } + })(s) + ? s + : encodeURIComponent(s).replace(/%5B/g, '[').replace(/%5D/g, ']'), + LS = [ + 'slash', + 'path-literal', + 'query-marker', + 'query-literal', + 'template-expression-param-name' + ], + openapi_path_templating_es_resolve = (s, o, i = {}) => { + const u = { ...{ encoder: encodePathComponent }, ...i }, + _ = openapi_path_templating_es_parse(s); + if (!_.result.success) return s; + const w = []; + _.ast.translate(w); + const x = w + .filter(([s]) => LS.includes(s)) + .map(([s, i]) => + 'template-expression-param-name' === s + ? Object.hasOwn(o, i) + ? u.encoder(o[i], i) + : `{${i}}` + : i + ); + return x.join(''); + }, + BS = { + body: function bodyBuilder({ req: s, value: o }) { + void 0 !== o && (s.body = o); + }, + header: function headerBuilder({ req: s, parameter: o, value: i }) { + ((s.headers = s.headers || {}), void 0 !== i && (s.headers[o.name] = i)); + }, + query: function queryBuilder({ req: s, value: o, parameter: i }) { + ((s.query = s.query || {}), !1 === o && 'boolean' === i.type && (o = 'false')); + 0 === o && ['number', 'integer'].indexOf(i.type) > -1 && (o = '0'); + if (o) s.query[i.name] = { collectionFormat: i.collectionFormat, value: o }; + else if (i.allowEmptyValue && void 0 !== o) { + const o = i.name; + ((s.query[o] = s.query[o] || {}), (s.query[o].allowEmptyValue = !0)); + } + }, + path: function pathBuilder({ req: s, value: o, parameter: i, baseURL: u }) { + if (void 0 !== o) { + const _ = s.url.replace(u, ''), + w = openapi_path_templating_es_resolve(_, { [i.name]: o }); + s.url = u + w; + } + }, + formData: function formDataBuilder({ req: s, value: o, parameter: i }) { + !1 === o && 'boolean' === i.type && (o = 'false'); + 0 === o && ['number', 'integer'].indexOf(i.type) > -1 && (o = '0'); + if (o) + ((s.form = s.form || {}), + (s.form[i.name] = { collectionFormat: i.collectionFormat, value: o })); + else if (i.allowEmptyValue && void 0 !== o) { + s.form = s.form || {}; + const o = i.name; + ((s.form[o] = s.form[o] || {}), (s.form[o].allowEmptyValue = !0)); + } + } + }; + function serialize(s, o) { + return o.includes('application/json') + ? 'string' == typeof s + ? s + : (Array.isArray(s) && + (s = s.map((s) => { + try { + return JSON.parse(s); + } catch (o) { + return s; + } + })), + JSON.stringify(s)) + : String(s); + } + function parameter_builders_path({ req: s, value: o, parameter: i, baseURL: u }) { + const { name: _, style: w, explode: x, content: C } = i; + if (void 0 === o) return; + const j = s.url.replace(u, ''); + let L; + if (C) { + const s = Object.keys(C)[0]; + L = openapi_path_templating_es_resolve( + j, + { [_]: o }, + { encoder: (o) => encodeCharacters(serialize(o, s)) } + ); + } else + L = openapi_path_templating_es_resolve( + j, + { [_]: o }, + { + encoder: (s) => + stylize({ + key: i.name, + value: s, + style: w || 'simple', + explode: x || !1, + escape: 'reserved' + }) + } + ); + s.url = u + L; + } + function parameter_builders_query({ req: s, value: o, parameter: i }) { + if (((s.query = s.query || {}), void 0 !== o && i.content)) { + const u = serialize(o, Object.keys(i.content)[0]); + if (u) s.query[i.name] = u; + else if (i.allowEmptyValue) { + const o = i.name; + ((s.query[o] = s.query[o] || {}), (s.query[o].allowEmptyValue = !0)); + } + } else if ((!1 === o && (o = 'false'), 0 === o && (o = '0'), o)) { + const { style: u, explode: _, allowReserved: w } = i; + s.query[i.name] = { + value: o, + serializationOption: { style: u, explode: _, allowReserved: w } + }; + } else if (i.allowEmptyValue && void 0 !== o) { + const o = i.name; + ((s.query[o] = s.query[o] || {}), (s.query[o].allowEmptyValue = !0)); + } + } + const FS = ['accept', 'authorization', 'content-type']; + function parameter_builders_header({ req: s, parameter: o, value: i }) { + if (((s.headers = s.headers || {}), !(FS.indexOf(o.name.toLowerCase()) > -1))) + if (void 0 !== i && o.content) { + const u = Object.keys(o.content)[0]; + s.headers[o.name] = serialize(i, u); + } else + void 0 === i || + (Array.isArray(i) && 0 === i.length) || + (s.headers[o.name] = stylize({ + key: o.name, + value: i, + style: o.style || 'simple', + explode: void 0 !== o.explode && o.explode, + escape: !1 + })); + } + function parameter_builders_cookie({ req: s, parameter: o, value: i }) { + s.headers = s.headers || {}; + const u = typeof i; + if (void 0 !== i && o.content) { + const u = Object.keys(o.content)[0]; + s.headers.Cookie = `${o.name}=${serialize(i, u)}`; + } else if (void 0 !== i && (!Array.isArray(i) || 0 !== i.length)) { + const _ = 'object' === u && !Array.isArray(i) && o.explode ? '' : `${o.name}=`; + s.headers.Cookie = + _ + + stylize({ + key: o.name, + value: i, + escape: !1, + style: o.style || 'form', + explode: void 0 !== o.explode && o.explode + }); + } + } + const qS = + 'undefined' != typeof globalThis + ? globalThis + : 'undefined' != typeof self + ? self + : window, + { btoa: $S } = qS, + VS = $S; + function buildRequest(s, o) { + const { + operation: i, + requestBody: u, + securities: _, + spec: w, + attachContentTypeForEmptyPayload: x + } = s; + let { requestContentType: C } = s; + o = (function applySecurities({ + request: s, + securities: o = {}, + operation: i = {}, + spec: u + }) { + var _; + const w = { ...s }, + { authorized: x = {} } = o, + C = i.security || u.security || [], + j = x && !!Object.keys(x).length, + L = + (null == u || null === (_ = u.components) || void 0 === _ + ? void 0 + : _.securitySchemes) || {}; + if ( + ((w.headers = w.headers || {}), + (w.query = w.query || {}), + !Object.keys(o).length || + !j || + !C || + (Array.isArray(i.security) && !i.security.length)) + ) + return s; + return ( + C.forEach((s) => { + Object.keys(s).forEach((s) => { + const o = x[s], + i = L[s]; + if (!o) return; + const u = o.value || o, + { type: _ } = i; + if (o) + if ('apiKey' === _) + ('query' === i.in && (w.query[i.name] = u), + 'header' === i.in && (w.headers[i.name] = u), + 'cookie' === i.in && (w.cookies[i.name] = u)); + else if ('http' === _) { + if (/^basic$/i.test(i.scheme)) { + const s = u.username || '', + o = u.password || '', + i = VS(`${s}:${o}`); + w.headers.Authorization = `Basic ${i}`; + } + /^bearer$/i.test(i.scheme) && (w.headers.Authorization = `Bearer ${u}`); + } else if ('oauth2' === _ || 'openIdConnect' === _) { + const s = o.token || {}, + u = s[i['x-tokenName'] || 'access_token']; + let _ = s.token_type; + ((_ && 'bearer' !== _.toLowerCase()) || (_ = 'Bearer'), + (w.headers.Authorization = `${_} ${u}`)); + } + }); + }), + w + ); + })({ request: o, securities: _, operation: i, spec: w }); + const j = i.requestBody || {}, + L = Object.keys(j.content || {}), + B = C && L.indexOf(C) > -1; + if (u || x) { + if (C && B) o.headers['Content-Type'] = C; + else if (!C) { + const s = L[0]; + s && ((o.headers['Content-Type'] = s), (C = s)); + } + } else C && B && (o.headers['Content-Type'] = C); + if (!s.responseContentType && i.responses) { + const s = Object.entries(i.responses) + .filter(([s, o]) => { + const i = parseInt(s, 10); + return i >= 200 && i < 300 && ku(o.content); + }) + .reduce((s, [, o]) => s.concat(Object.keys(o.content)), []); + s.length > 0 && (o.headers.accept = s.join(', ')); + } + if (u) + if (C) { + if (L.indexOf(C) > -1) + if ('application/x-www-form-urlencoded' === C || 'multipart/form-data' === C) + if ('object' == typeof u) { + var $, V; + const s = + null !== + ($ = null === (V = j.content[C]) || void 0 === V ? void 0 : V.encoding) && + void 0 !== $ + ? $ + : {}; + ((o.form = {}), + Object.keys(u).forEach((i) => { + let _; + try { + _ = JSON.parse(u[i]); + } catch { + _ = u[i]; + } + o.form[i] = { value: _, encoding: s[i] || {} }; + })); + } else if ('string' == typeof u) { + var U, z; + const s = + null !== + (U = null === (z = j.content[C]) || void 0 === z ? void 0 : z.encoding) && + void 0 !== U + ? U + : {}; + try { + o.form = {}; + const i = JSON.parse(u); + Object.entries(i).forEach(([i, u]) => { + o.form[i] = { value: u, encoding: s[i] || {} }; + }); + } catch { + o.form = u; + } + } else o.form = u; + else o.body = u; + } else o.body = u; + return o; + } + function build_request_buildRequest(s, o) { + const { + spec: i, + operation: u, + securities: _, + requestContentType: w, + responseContentType: x, + attachContentTypeForEmptyPayload: C + } = s; + if ( + ((o = (function build_request_applySecurities({ + request: s, + securities: o = {}, + operation: i = {}, + spec: u + }) { + const _ = { ...s }, + { authorized: w = {}, specSecurity: x = [] } = o, + C = i.security || x, + j = w && !!Object.keys(w).length, + L = u.securityDefinitions; + if ( + ((_.headers = _.headers || {}), + (_.query = _.query || {}), + !Object.keys(o).length || + !j || + !C || + (Array.isArray(i.security) && !i.security.length)) + ) + return s; + return ( + C.forEach((s) => { + Object.keys(s).forEach((s) => { + const o = w[s]; + if (!o) return; + const { token: i } = o, + u = o.value || o, + x = L[s], + { type: C } = x, + j = x['x-tokenName'] || 'access_token', + B = i && i[j]; + let $ = i && i.token_type; + if (o) + if ('apiKey' === C) { + const s = 'query' === x.in ? 'query' : 'headers'; + ((_[s] = _[s] || {}), (_[s][x.name] = u)); + } else if ('basic' === C) + if (u.header) _.headers.authorization = u.header; + else { + const s = u.username || '', + o = u.password || ''; + ((u.base64 = VS(`${s}:${o}`)), + (_.headers.authorization = `Basic ${u.base64}`)); + } + else + 'oauth2' === C && + B && + (($ = $ && 'bearer' !== $.toLowerCase() ? $ : 'Bearer'), + (_.headers.authorization = `${$} ${B}`)); + }); + }), + _ + ); + })({ request: o, securities: _, operation: u, spec: i })), + o.body || o.form || C) + ) + w + ? (o.headers['Content-Type'] = w) + : Array.isArray(u.consumes) + ? ([o.headers['Content-Type']] = u.consumes) + : Array.isArray(i.consumes) + ? ([o.headers['Content-Type']] = i.consumes) + : u.parameters && u.parameters.filter((s) => 'file' === s.type).length + ? (o.headers['Content-Type'] = 'multipart/form-data') + : u.parameters && + u.parameters.filter((s) => 'formData' === s.in).length && + (o.headers['Content-Type'] = 'application/x-www-form-urlencoded'); + else if (w) { + const s = u.parameters && u.parameters.filter((s) => 'body' === s.in).length > 0, + i = u.parameters && u.parameters.filter((s) => 'formData' === s.in).length > 0; + (s || i) && (o.headers['Content-Type'] = w); + } + return ( + !x && + Array.isArray(u.produces) && + u.produces.length > 0 && + (o.headers.accept = u.produces.join(', ')), + o + ); + } + function idFromPathMethodLegacy(s, o) { + return `${o.toLowerCase()}-${s}`; + } + const arrayOrEmpty = (s) => (Array.isArray(s) ? s : []), + parseURIReference = (s) => { + try { + return new URL(s); + } catch { + const o = new URL(s, Nc), + i = String(s).startsWith('/') ? o.pathname : o.pathname.substring(1); + return { + hash: o.hash, + host: '', + hostname: '', + href: '', + origin: '', + password: '', + pathname: i, + port: '', + protocol: '', + search: o.search, + searchParams: o.searchParams + }; + } + }; + class OperationNotFoundError extends Jo {} + const US = { buildRequest: execute_buildRequest }; + function execute_execute({ + http: s, + fetch: o, + spec: i, + operationId: u, + pathName: _, + method: w, + parameters: x, + securities: C, + ...j + }) { + const L = s || o || http_http; + _ && w && !u && (u = idFromPathMethodLegacy(_, w)); + const B = US.buildRequest({ + spec: i, + operationId: u, + parameters: x, + securities: C, + http: L, + ...j + }); + return ( + B.body && (ku(B.body) || Array.isArray(B.body)) && (B.body = JSON.stringify(B.body)), + L(B) + ); + } + function execute_buildRequest(s) { + var o; + const { + spec: i, + operationId: u, + responseContentType: _, + scheme: w, + requestInterceptor: x, + responseInterceptor: C, + contextUrl: j, + userFetch: L, + server: B, + serverVariables: $, + http: V, + signal: U, + serverVariableEncoder: z + } = s; + let { parameters: Y, parameterBuilders: Z, baseURL: ee } = s; + const ie = isOpenAPI3(i); + Z || (Z = ie ? fe : BS); + let ae = { + url: '', + credentials: V && V.withCredentials ? 'include' : 'same-origin', + headers: {}, + cookies: {} + }; + (U && (ae.signal = U), + x && (ae.requestInterceptor = x), + C && (ae.responseInterceptor = C), + L && (ae.userFetch = L)); + const le = (function getOperationRaw(s, o) { + return s && s.paths + ? (function findOperation(s, o) { + return ( + (function eachOperation(s, o, i) { + if (!s || 'object' != typeof s || !s.paths || 'object' != typeof s.paths) + return null; + const { paths: u } = s; + for (const _ in u) + for (const w in u[_]) { + if ('PARAMETERS' === w.toUpperCase()) continue; + const x = u[_][w]; + if (!x || 'object' != typeof x) continue; + const C = { spec: s, pathName: _, method: w.toUpperCase(), operation: x }, + j = o(C); + if (i && j) return C; + } + })(s, o, !0) || null + ); + })(s, ({ pathName: s, method: i, operation: u }) => { + if (!u || 'object' != typeof u) return !1; + const _ = u.operationId; + return [opId(u, s, i), idFromPathMethodLegacy(s, i), _].some((s) => s && s === o); + }) + : null; + })(i, u); + if (!le) throw new OperationNotFoundError(`Operation ${u} not found`); + const { operation: ce = {}, method: pe, pathName: de } = le; + if ( + ((ee = + null !== (o = ee) && void 0 !== o + ? o + : (function baseUrl(s) { + const o = isOpenAPI3(s.spec); + return o + ? (function oas3BaseUrl({ + spec: s, + pathName: o, + method: i, + server: u, + contextUrl: _, + serverVariables: w = {}, + serverVariableEncoder: x + }) { + var C, j; + let L, + B = [], + $ = ''; + const V = + null == s || + null === (C = s.paths) || + void 0 === C || + null === (C = C[o]) || + void 0 === C || + null === (C = C[(i || '').toLowerCase()]) || + void 0 === C + ? void 0 + : C.servers, + U = + null == s || + null === (j = s.paths) || + void 0 === j || + null === (j = j[o]) || + void 0 === j + ? void 0 + : j.servers, + z = null == s ? void 0 : s.servers; + ((B = isNonEmptyServerList(V) + ? V + : isNonEmptyServerList(U) + ? U + : isNonEmptyServerList(z) + ? z + : [Rc]), + u && ((L = B.find((s) => s.url === u)), L && ($ = u))); + $ || (([L] = B), ($ = L.url)); + if (openapi_server_url_templating_es_test($, { strict: !0 })) { + const s = Object.entries({ ...L.variables }).reduce( + (s, [o, i]) => ((s[o] = i.default), s), + {} + ); + $ = es_substitute( + $, + { ...s, ...w }, + { encoder: 'function' == typeof x ? x : Ip } + ); + } + return (function buildOas3UrlWithContext(s = '', o = '') { + const i = parseURIReference(s && o ? resolve(o, s) : s), + u = parseURIReference(o), + _ = stripNonAlpha(i.protocol) || stripNonAlpha(u.protocol), + w = i.host || u.host, + x = i.pathname; + let C; + C = _ && w ? `${_}://${w + x}` : x; + return '/' === C[C.length - 1] ? C.slice(0, -1) : C; + })($, _); + })(s) + : (function swagger2BaseUrl({ spec: s, scheme: o, contextUrl: i = '' }) { + const u = parseURIReference(i), + _ = Array.isArray(s.schemes) ? s.schemes[0] : null, + w = o || _ || stripNonAlpha(u.protocol) || 'http', + x = s.host || u.host || '', + C = s.basePath || ''; + let j; + j = w && x ? `${w}://${x + C}` : C; + return '/' === j[j.length - 1] ? j.slice(0, -1) : j; + })(s); + })({ + spec: i, + scheme: w, + contextUrl: j, + server: B, + serverVariables: $, + pathName: de, + method: pe, + serverVariableEncoder: z + })), + (ae.url += ee), + !u) + ) + return (delete ae.cookies, ae); + ((ae.url += de), (ae.method = `${pe}`.toUpperCase()), (Y = Y || {})); + const ye = i.paths[de] || {}; + _ && (ae.headers.accept = _); + const be = ((s) => { + const o = {}; + s.forEach((s) => { + (o[s.in] || (o[s.in] = {}), (o[s.in][s.name] = s)); + }); + const i = []; + return ( + Object.keys(o).forEach((s) => { + Object.keys(o[s]).forEach((u) => { + i.push(o[s][u]); + }); + }), + i + ); + })([].concat(arrayOrEmpty(ce.parameters)).concat(arrayOrEmpty(ye.parameters))); + be.forEach((s) => { + const o = Z[s.in]; + let u; + if ( + ('body' === s.in && s.schema && s.schema.properties && (u = Y), + (u = s && s.name && Y[s.name]), + void 0 === u + ? (u = s && s.name && Y[`${s.in}.${s.name}`]) + : ((s, o) => o.filter((o) => o.name === s))(s.name, be).length > 1 && + console.warn( + `Parameter '${s.name}' is ambiguous because the defined spec has more than one parameter with the name: '${s.name}' and the passed-in parameter values did not define an 'in' value.` + ), + null !== u) + ) { + if ( + (void 0 !== s.default && void 0 === u && (u = s.default), + void 0 === u && s.required && !s.allowEmptyValue) + ) + throw new Error(`Required parameter ${s.name} is not provided`); + if (ie && s.schema && 'object' === s.schema.type && 'string' == typeof u) + try { + u = JSON.parse(u); + } catch (s) { + throw new Error('Could not parse object parameter value string as JSON'); + } + o && o({ req: ae, parameter: s, value: u, operation: ce, spec: i, baseURL: ee }); + } + }); + const _e = { ...s, operation: ce }; + if ( + ((ae = ie ? buildRequest(_e, ae) : build_request_buildRequest(_e, ae)), + ae.cookies && Object.keys(ae.cookies).length) + ) { + const s = Object.keys(ae.cookies).reduce((s, o) => { + const i = ae.cookies[o]; + return s + (s ? '&' : '') + jS.serialize(o, i); + }, ''); + ae.headers.Cookie = s; + } + return (ae.cookies && delete ae.cookies, serializeRequest(ae)); + } + const stripNonAlpha = (s) => (s ? s.replace(/\W/g, '') : null); + const isNonEmptyServerList = (s) => Array.isArray(s) && s.length > 0; + const makeResolveSubtree = + (s) => + async (o, i, u = {}) => + (async (s, o, i = {}) => { + const { + returnEntireTree: u, + baseDoc: _, + requestInterceptor: w, + responseInterceptor: x, + parameterMacro: C, + modelPropertyMacro: j, + useCircularStructures: L, + strategies: B + } = i, + $ = { + spec: s, + pathDiscriminator: o, + baseDoc: _, + requestInterceptor: w, + responseInterceptor: x, + parameterMacro: C, + modelPropertyMacro: j, + useCircularStructures: L, + strategies: B + }, + V = B.find((o) => o.match(s)).normalize(s), + U = await AS({ + spec: V, + ...$, + allowMetaPatches: !0, + skipNormalization: !isOpenAPI31(s) + }); + return ( + !u && + Array.isArray(o) && + o.length && + (U.spec = o.reduce((s, o) => (null == s ? void 0 : s[o]), U.spec) || null), + U + ); + })(o, i, { ...s, ...u }), + zS = + (makeResolveSubtree({ strategies: [fu, hu, uu] }), + (s, o) => + (...i) => { + s(...i); + const u = o.getConfigs().withCredentials; + o.fn.fetch.withCredentials = u; + }); + function swagger_client({ configs: s, getConfigs: o }) { + return { + fn: { + fetch: + ((i = http_http), + (u = s.preFetch), + (_ = s.postFetch), + (_ = _ || ((s) => s)), + (u = u || ((s) => s)), + (s) => ( + 'string' == typeof s && (s = { url: s }), + (s = serializeRequest(s)), + (s = u(s)), + _(i(s)) + )), + buildRequest: execute_buildRequest, + execute: execute_execute, + resolve: makeResolve({ strategies: [OS, fu, hu, uu] }), + resolveSubtree: async (s, i, u = {}) => { + const _ = o(), + w = { + modelPropertyMacro: _.modelPropertyMacro, + parameterMacro: _.parameterMacro, + requestInterceptor: _.requestInterceptor, + responseInterceptor: _.responseInterceptor, + strategies: [OS, fu, hu, uu] + }; + return makeResolveSubtree(w)(s, i, u); + }, + serializeRes: serializeResponse, + opId + }, + statePlugins: { configs: { wrapActions: { loaded: zS } } } + }; + var i, u, _; + } + function util() { + return { fn: { shallowEqualKeys } }; + } + var WS = __webpack_require__(40961), + KS = __webpack_require__(78418), + HS = Pe, + JS = Symbol.for('react-redux-context'), + GS = 'undefined' != typeof globalThis ? globalThis : {}; + function getContext() { + if (!HS.createContext) return {}; + const s = GS[JS] ?? (GS[JS] = new Map()); + let o = s.get(HS.createContext); + return (o || ((o = HS.createContext(null)), s.set(HS.createContext, o)), o); + } + var YS = getContext(), + notInitialized = () => { + throw new Error('uSES not initialized!'); + }; + var XS = Symbol.for('react.element'), + ZS = Symbol.for('react.portal'), + QS = Symbol.for('react.fragment'), + ex = Symbol.for('react.strict_mode'), + tx = Symbol.for('react.profiler'), + rx = Symbol.for('react.provider'), + nx = Symbol.for('react.context'), + sx = Symbol.for('react.server_context'), + ox = Symbol.for('react.forward_ref'), + ix = Symbol.for('react.suspense'), + ax = Symbol.for('react.suspense_list'), + lx = Symbol.for('react.memo'), + cx = Symbol.for('react.lazy'), + ux = (Symbol.for('react.offscreen'), Symbol.for('react.client.reference'), ox), + px = lx; + function typeOf(s) { + if ('object' == typeof s && null !== s) { + const o = s.$$typeof; + switch (o) { + case XS: { + const i = s.type; + switch (i) { + case QS: + case tx: + case ex: + case ix: + case ax: + return i; + default: { + const s = i && i.$$typeof; + switch (s) { + case sx: + case nx: + case ox: + case cx: + case lx: + case rx: + return s; + default: + return o; + } + } + } + } + case ZS: + return o; + } + } + } + function pureFinalPropsSelectorFactory( + s, + o, + i, + u, + { areStatesEqual: _, areOwnPropsEqual: w, areStatePropsEqual: x } + ) { + let C, + j, + L, + B, + $, + V = !1; + function handleSubsequentCalls(V, U) { + const z = !w(U, j), + Y = !_(V, C, U, j); + return ( + (C = V), + (j = U), + z && Y + ? (function handleNewPropsAndNewState() { + return ( + (L = s(C, j)), + o.dependsOnOwnProps && (B = o(u, j)), + ($ = i(L, B, j)), + $ + ); + })() + : z + ? (function handleNewProps() { + return ( + s.dependsOnOwnProps && (L = s(C, j)), + o.dependsOnOwnProps && (B = o(u, j)), + ($ = i(L, B, j)), + $ + ); + })() + : Y + ? (function handleNewState() { + const o = s(C, j), + u = !x(o, L); + return ((L = o), u && ($ = i(L, B, j)), $); + })() + : $ + ); + } + return function pureFinalPropsSelector(_, w) { + return V + ? handleSubsequentCalls(_, w) + : (function handleFirstCall(_, w) { + return ( + (C = _), + (j = w), + (L = s(C, j)), + (B = o(u, j)), + ($ = i(L, B, j)), + (V = !0), + $ + ); + })(_, w); + }; + } + function wrapMapToPropsConstant(s) { + return function initConstantSelector(o) { + const i = s(o); + function constantSelector() { + return i; + } + return ((constantSelector.dependsOnOwnProps = !1), constantSelector); + }; + } + function getDependsOnOwnProps(s) { + return s.dependsOnOwnProps ? Boolean(s.dependsOnOwnProps) : 1 !== s.length; + } + function wrapMapToPropsFunc(s, o) { + return function initProxySelector(o, { displayName: i }) { + const u = function mapToPropsProxy(s, o) { + return u.dependsOnOwnProps ? u.mapToProps(s, o) : u.mapToProps(s, void 0); + }; + return ( + (u.dependsOnOwnProps = !0), + (u.mapToProps = function detectFactoryAndVerify(o, i) { + ((u.mapToProps = s), (u.dependsOnOwnProps = getDependsOnOwnProps(s))); + let _ = u(o, i); + return ( + 'function' == typeof _ && + ((u.mapToProps = _), + (u.dependsOnOwnProps = getDependsOnOwnProps(_)), + (_ = u(o, i))), + _ + ); + }), + u + ); + }; + } + function createInvalidArgFactory(s, o) { + return (i, u) => { + throw new Error( + `Invalid value of type ${typeof s} for ${o} argument when connecting component ${u.wrappedComponentName}.` + ); + }; + } + function defaultMergeProps(s, o, i) { + return { ...i, ...s, ...o }; + } + function defaultNoopBatch(s) { + s(); + } + var hx = { notify() {}, get: () => [] }; + function createSubscription(s, o) { + let i, + u = hx, + _ = 0, + w = !1; + function handleChangeWrapper() { + x.onStateChange && x.onStateChange(); + } + function trySubscribe() { + (_++, + i || + ((i = o ? o.addNestedSub(handleChangeWrapper) : s.subscribe(handleChangeWrapper)), + (u = (function createListenerCollection() { + let s = null, + o = null; + return { + clear() { + ((s = null), (o = null)); + }, + notify() { + defaultNoopBatch(() => { + let o = s; + for (; o; ) (o.callback(), (o = o.next)); + }); + }, + get() { + const o = []; + let i = s; + for (; i; ) (o.push(i), (i = i.next)); + return o; + }, + subscribe(i) { + let u = !0; + const _ = (o = { callback: i, next: null, prev: o }); + return ( + _.prev ? (_.prev.next = _) : (s = _), + function unsubscribe() { + u && + null !== s && + ((u = !1), + _.next ? (_.next.prev = _.prev) : (o = _.prev), + _.prev ? (_.prev.next = _.next) : (s = _.next)); + } + ); + } + }; + })()))); + } + function tryUnsubscribe() { + (_--, i && 0 === _ && (i(), (i = void 0), u.clear(), (u = hx))); + } + const x = { + addNestedSub: function addNestedSub(s) { + trySubscribe(); + const o = u.subscribe(s); + let i = !1; + return () => { + i || ((i = !0), o(), tryUnsubscribe()); + }; + }, + notifyNestedSubs: function notifyNestedSubs() { + u.notify(); + }, + handleChangeWrapper, + isSubscribed: function isSubscribed() { + return w; + }, + trySubscribe: function trySubscribeSelf() { + w || ((w = !0), trySubscribe()); + }, + tryUnsubscribe: function tryUnsubscribeSelf() { + w && ((w = !1), tryUnsubscribe()); + }, + getListeners: () => u + }; + return x; + } + var dx = !( + 'undefined' == typeof window || + void 0 === window.document || + void 0 === window.document.createElement + ), + fx = 'undefined' != typeof navigator && 'ReactNative' === navigator.product, + mx = dx || fx ? HS.useLayoutEffect : HS.useEffect; + function is(s, o) { + return s === o ? 0 !== s || 0 !== o || 1 / s == 1 / o : s != s && o != o; + } + function shallowEqual(s, o) { + if (is(s, o)) return !0; + if ('object' != typeof s || null === s || 'object' != typeof o || null === o) return !1; + const i = Object.keys(s), + u = Object.keys(o); + if (i.length !== u.length) return !1; + for (let u = 0; u < i.length; u++) + if (!Object.prototype.hasOwnProperty.call(o, i[u]) || !is(s[i[u]], o[i[u]])) return !1; + return !0; + } + var gx = { + childContextTypes: !0, + contextType: !0, + contextTypes: !0, + defaultProps: !0, + displayName: !0, + getDefaultProps: !0, + getDerivedStateFromError: !0, + getDerivedStateFromProps: !0, + mixins: !0, + propTypes: !0, + type: !0 + }, + yx = { + name: !0, + length: !0, + prototype: !0, + caller: !0, + callee: !0, + arguments: !0, + arity: !0 + }, + vx = { + $$typeof: !0, + compare: !0, + defaultProps: !0, + displayName: !0, + propTypes: !0, + type: !0 + }, + bx = { + [ux]: { $$typeof: !0, render: !0, defaultProps: !0, displayName: !0, propTypes: !0 }, + [px]: vx + }; + function getStatics(s) { + return (function isMemo(s) { + return typeOf(s) === lx; + })(s) + ? vx + : bx[s.$$typeof] || gx; + } + var _x = Object.defineProperty, + Ex = Object.getOwnPropertyNames, + wx = Object.getOwnPropertySymbols, + Sx = Object.getOwnPropertyDescriptor, + xx = Object.getPrototypeOf, + kx = Object.prototype; + function hoistNonReactStatics(s, o) { + if ('string' != typeof o) { + if (kx) { + const i = xx(o); + i && i !== kx && hoistNonReactStatics(s, i); + } + let i = Ex(o); + wx && (i = i.concat(wx(o))); + const u = getStatics(s), + _ = getStatics(o); + for (let w = 0; w < i.length; ++w) { + const x = i[w]; + if (!(yx[x] || (_ && _[x]) || (u && u[x]))) { + const i = Sx(o, x); + try { + _x(s, x, i); + } catch (s) {} + } + } + } + return s; + } + var Cx = notInitialized, + Ox = [null, null]; + function captureWrapperProps(s, o, i, u, _, w) { + ((s.current = u), (i.current = !1), _.current && ((_.current = null), w())); + } + function strictEqual(s, o) { + return s === o; + } + var Ax = function connect( + s, + o, + i, + { + pure: u, + areStatesEqual: _ = strictEqual, + areOwnPropsEqual: w = shallowEqual, + areStatePropsEqual: x = shallowEqual, + areMergedPropsEqual: C = shallowEqual, + forwardRef: j = !1, + context: L = YS + } = {} + ) { + const B = L, + $ = (function mapStateToPropsFactory(s) { + return s + ? 'function' == typeof s + ? wrapMapToPropsFunc(s) + : createInvalidArgFactory(s, 'mapStateToProps') + : wrapMapToPropsConstant(() => ({})); + })(s), + V = (function mapDispatchToPropsFactory(s) { + return s && 'object' == typeof s + ? wrapMapToPropsConstant((o) => + (function react_redux_bindActionCreators(s, o) { + const i = {}; + for (const u in s) { + const _ = s[u]; + 'function' == typeof _ && (i[u] = (...s) => o(_(...s))); + } + return i; + })(s, o) + ) + : s + ? 'function' == typeof s + ? wrapMapToPropsFunc(s) + : createInvalidArgFactory(s, 'mapDispatchToProps') + : wrapMapToPropsConstant((s) => ({ dispatch: s })); + })(o), + U = (function mergePropsFactory(s) { + return s + ? 'function' == typeof s + ? (function wrapMergePropsFunc(s) { + return function initMergePropsProxy( + o, + { displayName: i, areMergedPropsEqual: u } + ) { + let _, + w = !1; + return function mergePropsProxy(o, i, x) { + const C = s(o, i, x); + return (w ? u(C, _) || (_ = C) : ((w = !0), (_ = C)), _); + }; + }; + })(s) + : createInvalidArgFactory(s, 'mergeProps') + : () => defaultMergeProps; + })(i), + z = Boolean(s); + return (s) => { + const o = s.displayName || s.name || 'Component', + i = `Connect(${o})`, + u = { + shouldHandleStateChanges: z, + displayName: i, + wrappedComponentName: o, + WrappedComponent: s, + initMapStateToProps: $, + initMapDispatchToProps: V, + initMergeProps: U, + areStatesEqual: _, + areStatePropsEqual: x, + areOwnPropsEqual: w, + areMergedPropsEqual: C + }; + function ConnectFunction(o) { + const [i, _, w] = HS.useMemo(() => { + const { reactReduxForwardedRef: s, ...i } = o; + return [o.context, s, i]; + }, [o]), + x = HS.useMemo(() => B, [i, B]), + C = HS.useContext(x), + j = Boolean(o.store) && Boolean(o.store.getState) && Boolean(o.store.dispatch), + L = Boolean(C) && Boolean(C.store); + const $ = j ? o.store : C.store, + V = L ? C.getServerState : $.getState, + U = HS.useMemo( + () => + (function finalPropsSelectorFactory( + s, + { initMapStateToProps: o, initMapDispatchToProps: i, initMergeProps: u, ..._ } + ) { + return pureFinalPropsSelectorFactory(o(s, _), i(s, _), u(s, _), s, _); + })($.dispatch, u), + [$] + ), + [Y, Z] = HS.useMemo(() => { + if (!z) return Ox; + const s = createSubscription($, j ? void 0 : C.subscription), + o = s.notifyNestedSubs.bind(s); + return [s, o]; + }, [$, j, C]), + ee = HS.useMemo(() => (j ? C : { ...C, subscription: Y }), [j, C, Y]), + ie = HS.useRef(void 0), + ae = HS.useRef(w), + le = HS.useRef(void 0), + ce = HS.useRef(!1), + pe = HS.useRef(!1), + de = HS.useRef(void 0); + mx( + () => ( + (pe.current = !0), + () => { + pe.current = !1; + } + ), + [] + ); + const fe = HS.useMemo( + () => () => (le.current && w === ae.current ? le.current : U($.getState(), w)), + [$, w] + ), + ye = HS.useMemo( + () => (s) => + Y + ? (function subscribeUpdates(s, o, i, u, _, w, x, C, j, L, B) { + if (!s) return () => {}; + let $ = !1, + V = null; + const checkForUpdates = () => { + if ($ || !C.current) return; + const s = o.getState(); + let i, U; + try { + i = u(s, _.current); + } catch (s) { + ((U = s), (V = s)); + } + (U || (V = null), + i === w.current + ? x.current || L() + : ((w.current = i), (j.current = i), (x.current = !0), B())); + }; + return ( + (i.onStateChange = checkForUpdates), + i.trySubscribe(), + checkForUpdates(), + () => { + if ((($ = !0), i.tryUnsubscribe(), (i.onStateChange = null), V)) + throw V; + } + ); + })(z, $, Y, U, ae, ie, ce, pe, le, Z, s) + : () => {}, + [Y] + ); + let be; + !(function useIsomorphicLayoutEffectWithArgs(s, o, i) { + mx(() => s(...o), i); + })(captureWrapperProps, [ae, ie, ce, w, le, Z]); + try { + be = Cx(ye, fe, V ? () => U(V(), w) : fe); + } catch (s) { + throw ( + de.current && + (s.message += `\nThe error may be correlated with this previous error:\n${de.current.stack}\n\n`), + s + ); + } + mx(() => { + ((de.current = void 0), (le.current = void 0), (ie.current = be)); + }); + const _e = HS.useMemo(() => HS.createElement(s, { ...be, ref: _ }), [_, s, be]); + return HS.useMemo( + () => (z ? HS.createElement(x.Provider, { value: ee }, _e) : _e), + [x, _e, ee] + ); + } + const L = HS.memo(ConnectFunction); + if (((L.WrappedComponent = s), (L.displayName = ConnectFunction.displayName = i), j)) { + const o = HS.forwardRef(function forwardConnectRef(s, o) { + return HS.createElement(L, { ...s, reactReduxForwardedRef: o }); + }); + return ((o.displayName = i), (o.WrappedComponent = s), hoistNonReactStatics(o, s)); + } + return hoistNonReactStatics(L, s); + }; + }; + var jx = function Provider({ + store: s, + context: o, + children: i, + serverState: u, + stabilityCheck: _ = 'once', + identityFunctionCheck: w = 'once' + }) { + const x = HS.useMemo(() => { + const o = createSubscription(s); + return { + store: s, + subscription: o, + getServerState: u ? () => u : void 0, + stabilityCheck: _, + identityFunctionCheck: w + }; + }, [s, u, _, w]), + C = HS.useMemo(() => s.getState(), [s]); + mx(() => { + const { subscription: o } = x; + return ( + (o.onStateChange = o.notifyNestedSubs), + o.trySubscribe(), + C !== s.getState() && o.notifyNestedSubs(), + () => { + (o.tryUnsubscribe(), (o.onStateChange = void 0)); + } + ); + }, [x, C]); + const j = o || YS; + return HS.createElement(j.Provider, { value: x }, i); + }; + var Ix; + ((Ix = KS.useSyncExternalStoreWithSelector), + ((s) => { + Cx = s; + })(Pe.useSyncExternalStore)); + var Px = __webpack_require__(83488), + Mx = __webpack_require__.n(Px); + const withSystem = (s) => (o) => { + const { fn: i } = s(); + class WithSystem extends Pe.Component { + render() { + return Pe.createElement(o, Rn()({}, s(), this.props, this.context)); + } + } + return ((WithSystem.displayName = `WithSystem(${i.getDisplayName(o)})`), WithSystem); + }, + withRoot = (s, o) => (i) => { + const { fn: u } = s(); + class WithRoot extends Pe.Component { + render() { + return Pe.createElement( + jx, + { store: o }, + Pe.createElement(i, Rn()({}, this.props, this.context)) + ); + } + } + return ((WithRoot.displayName = `WithRoot(${u.getDisplayName(i)})`), WithRoot); + }, + withConnect = (s, o, i) => + compose( + i ? withRoot(s, i) : Mx(), + Ax((i, u) => { + const _ = { ...u, ...s() }, + w = o.prototype?.mapStateToProps || ((s) => ({ state: s })); + return w(i, _); + }), + withSystem(s) + )(o), + handleProps = (s, o, i, u) => { + for (const _ in o) { + const w = o[_]; + 'function' == typeof w && w(i[_], u[_], s()); + } + }, + withMappedContainer = (s, o, i) => (o, u) => { + const { fn: _ } = s(), + w = i(o, 'root'); + class WithMappedContainer extends Pe.Component { + constructor(o, i) { + (super(o, i), handleProps(s, u, o, {})); + } + UNSAFE_componentWillReceiveProps(o) { + handleProps(s, u, o, this.props); + } + render() { + const s = Yt()(this.props, u ? Object.keys(u) : []); + return Pe.createElement(w, s); + } + } + return ( + (WithMappedContainer.displayName = `WithMappedContainer(${_.getDisplayName(w)})`), + WithMappedContainer + ); + }, + render = (s, o, i, u) => (_) => { + const w = i(s, o, u)('App', 'root'), + { createRoot: x } = WS; + x(_).render(Pe.createElement(w, null)); + }, + getComponent = + (s, o, i) => + (u, _, w = {}) => { + if ('string' != typeof u) + throw new TypeError('Need a string, to fetch a component. Was given a ' + typeof u); + const x = i(u); + return x + ? _ + ? 'root' === _ + ? withConnect(s, x, o()) + : withConnect(s, x) + : x + : (w.failSilently || s().log.warn('Could not find component:', u), null); + }, + getDisplayName = (s) => s.displayName || s.name || 'Component', + view = ({ getComponents: s, getStore: o, getSystem: i }) => { + const u = ((s) => jt(s, (...s) => JSON.stringify(s)))(getComponent(i, o, s)), + _ = ((s) => utils_memoizeN(s, (...s) => s))(withMappedContainer(i, 0, u)); + return { + rootInjects: { + getComponent: u, + makeMappedContainer: _, + render: render(i, o, getComponent, s) + }, + fn: { getDisplayName } + }; + }, + view_legacy = ({ React: s, getSystem: o, getStore: i, getComponents: u }) => { + const _ = {}, + w = parseInt(s?.version, 10); + return ( + w >= 16 && + w < 18 && + (_.render = ((s, o, i, u) => (_) => { + const w = i(s, o, u)('App', 'root'); + WS.render(Pe.createElement(w, null), _); + })(o, i, getComponent, u)), + { rootInjects: _ } + ); + }; + function downloadUrlPlugin(s) { + let { fn: o } = s; + const i = { + download: + (s) => + ({ errActions: i, specSelectors: u, specActions: _, getConfigs: w }) => { + let { fetch: x } = o; + const C = w(); + function next(o) { + if (o instanceof Error || o.status >= 400) + return ( + _.updateLoadingStatus('failed'), + i.newThrownErr( + Object.assign(new Error((o.message || o.statusText) + ' ' + s), { + source: 'fetch' + }) + ), + void ( + !o.status && + o instanceof Error && + (function checkPossibleFailReasons() { + try { + let o; + if ( + ('URL' in at + ? (o = new URL(s)) + : ((o = document.createElement('a')), (o.href = s)), + 'https:' !== o.protocol && 'https:' === at.location.protocol) + ) { + const s = Object.assign( + new Error( + `Possible mixed-content issue? The page was loaded over https:// but a ${o.protocol}// URL was specified. Check that you are not attempting to load mixed content.` + ), + { source: 'fetch' } + ); + return void i.newThrownErr(s); + } + if (o.origin !== at.location.origin) { + const s = Object.assign( + new Error( + `Possible cross-origin (CORS) issue? The URL origin (${o.origin}) does not match the page (${at.location.origin}). Check the server returns the correct 'Access-Control-Allow-*' headers.` + ), + { source: 'fetch' } + ); + i.newThrownErr(s); + } + } catch (s) { + return; + } + })() + ) + ); + (_.updateLoadingStatus('success'), + _.updateSpec(o.text), + u.url() !== s && _.updateUrl(s)); + } + ((s = s || u.url()), + _.updateLoadingStatus('loading'), + i.clear({ source: 'fetch' }), + x({ + url: s, + loadSpec: !0, + requestInterceptor: C.requestInterceptor || ((s) => s), + responseInterceptor: C.responseInterceptor || ((s) => s), + credentials: 'same-origin', + headers: { Accept: 'application/json,*/*' } + }).then(next, next)); + }, + updateLoadingStatus: (s) => { + let o = [null, 'loading', 'failed', 'success', 'failedConfig']; + return ( + -1 === o.indexOf(s) && + console.error(`Error: ${s} is not one of ${JSON.stringify(o)}`), + { type: 'spec_update_loading_status', payload: s } + ); + } + }; + let u = { + loadingStatus: Ut( + (s) => s || (0, qe.Map)(), + (s) => s.get('loadingStatus') || null + ) + }; + return { + statePlugins: { + spec: { + actions: i, + reducers: { + spec_update_loading_status: (s, o) => + 'string' == typeof o.payload ? s.set('loadingStatus', o.payload) : s + }, + selectors: u + } + } + }; + } + function arrayLikeToArray_arrayLikeToArray(s, o) { + (null == o || o > s.length) && (o = s.length); + for (var i = 0, u = Array(o); i < o; i++) u[i] = s[i]; + return u; + } + function toConsumableArray_toConsumableArray(s) { + return ( + (function arrayWithoutHoles_arrayWithoutHoles(s) { + if (Array.isArray(s)) return arrayLikeToArray_arrayLikeToArray(s); + })(s) || + (function iterableToArray_iterableToArray(s) { + if ( + ('undefined' != typeof Symbol && null != s[Symbol.iterator]) || + null != s['@@iterator'] + ) + return Array.from(s); + })(s) || + (function unsupportedIterableToArray_unsupportedIterableToArray(s, o) { + if (s) { + if ('string' == typeof s) return arrayLikeToArray_arrayLikeToArray(s, o); + var i = {}.toString.call(s).slice(8, -1); + return ( + 'Object' === i && s.constructor && (i = s.constructor.name), + 'Map' === i || 'Set' === i + ? Array.from(s) + : 'Arguments' === i || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i) + ? arrayLikeToArray_arrayLikeToArray(s, o) + : void 0 + ); + } + })(s) || + (function nonIterableSpread_nonIterableSpread() { + throw new TypeError( + 'Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' + ); + })() + ); + } + function typeof_typeof(s) { + return ( + (typeof_typeof = + 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator + ? function (s) { + return typeof s; + } + : function (s) { + return s && + 'function' == typeof Symbol && + s.constructor === Symbol && + s !== Symbol.prototype + ? 'symbol' + : typeof s; + }), + typeof_typeof(s) + ); + } + function toPropertyKey(s) { + var o = (function toPrimitive(s, o) { + if ('object' != typeof_typeof(s) || !s) return s; + var i = s[Symbol.toPrimitive]; + if (void 0 !== i) { + var u = i.call(s, o || 'default'); + if ('object' != typeof_typeof(u)) return u; + throw new TypeError('@@toPrimitive must return a primitive value.'); + } + return ('string' === o ? String : Number)(s); + })(s, 'string'); + return 'symbol' == typeof_typeof(o) ? o : o + ''; + } + function defineProperty_defineProperty(s, o, i) { + return ( + (o = toPropertyKey(o)) in s + ? Object.defineProperty(s, o, { + value: i, + enumerable: !0, + configurable: !0, + writable: !0 + }) + : (s[o] = i), + s + ); + } + function extends_extends() { + return ( + (extends_extends = Object.assign + ? Object.assign.bind() + : function (s) { + for (var o = 1; o < arguments.length; o++) { + var i = arguments[o]; + for (var u in i) ({}).hasOwnProperty.call(i, u) && (s[u] = i[u]); + } + return s; + }), + extends_extends.apply(null, arguments) + ); + } + function create_element_ownKeys(s, o) { + var i = Object.keys(s); + if (Object.getOwnPropertySymbols) { + var u = Object.getOwnPropertySymbols(s); + (o && + (u = u.filter(function (o) { + return Object.getOwnPropertyDescriptor(s, o).enumerable; + })), + i.push.apply(i, u)); + } + return i; + } + function _objectSpread(s) { + for (var o = 1; o < arguments.length; o++) { + var i = null != arguments[o] ? arguments[o] : {}; + o % 2 + ? create_element_ownKeys(Object(i), !0).forEach(function (o) { + defineProperty_defineProperty(s, o, i[o]); + }) + : Object.getOwnPropertyDescriptors + ? Object.defineProperties(s, Object.getOwnPropertyDescriptors(i)) + : create_element_ownKeys(Object(i)).forEach(function (o) { + Object.defineProperty(s, o, Object.getOwnPropertyDescriptor(i, o)); + }); + } + return s; + } + var Tx = {}; + function createStyleObject(s) { + var o = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}, + i = arguments.length > 2 ? arguments[2] : void 0; + return (function getClassNameCombinations(s) { + if (0 === s.length || 1 === s.length) return s; + var o = s.join('.'); + return ( + Tx[o] || + (Tx[o] = (function powerSetPermutations(s) { + var o = s.length; + return 0 === o || 1 === o + ? s + : 2 === o + ? [ + s[0], + s[1], + ''.concat(s[0], '.').concat(s[1]), + ''.concat(s[1], '.').concat(s[0]) + ] + : 3 === o + ? [ + s[0], + s[1], + s[2], + ''.concat(s[0], '.').concat(s[1]), + ''.concat(s[0], '.').concat(s[2]), + ''.concat(s[1], '.').concat(s[0]), + ''.concat(s[1], '.').concat(s[2]), + ''.concat(s[2], '.').concat(s[0]), + ''.concat(s[2], '.').concat(s[1]), + ''.concat(s[0], '.').concat(s[1], '.').concat(s[2]), + ''.concat(s[0], '.').concat(s[2], '.').concat(s[1]), + ''.concat(s[1], '.').concat(s[0], '.').concat(s[2]), + ''.concat(s[1], '.').concat(s[2], '.').concat(s[0]), + ''.concat(s[2], '.').concat(s[0], '.').concat(s[1]), + ''.concat(s[2], '.').concat(s[1], '.').concat(s[0]) + ] + : o >= 4 + ? [ + s[0], + s[1], + s[2], + s[3], + ''.concat(s[0], '.').concat(s[1]), + ''.concat(s[0], '.').concat(s[2]), + ''.concat(s[0], '.').concat(s[3]), + ''.concat(s[1], '.').concat(s[0]), + ''.concat(s[1], '.').concat(s[2]), + ''.concat(s[1], '.').concat(s[3]), + ''.concat(s[2], '.').concat(s[0]), + ''.concat(s[2], '.').concat(s[1]), + ''.concat(s[2], '.').concat(s[3]), + ''.concat(s[3], '.').concat(s[0]), + ''.concat(s[3], '.').concat(s[1]), + ''.concat(s[3], '.').concat(s[2]), + ''.concat(s[0], '.').concat(s[1], '.').concat(s[2]), + ''.concat(s[0], '.').concat(s[1], '.').concat(s[3]), + ''.concat(s[0], '.').concat(s[2], '.').concat(s[1]), + ''.concat(s[0], '.').concat(s[2], '.').concat(s[3]), + ''.concat(s[0], '.').concat(s[3], '.').concat(s[1]), + ''.concat(s[0], '.').concat(s[3], '.').concat(s[2]), + ''.concat(s[1], '.').concat(s[0], '.').concat(s[2]), + ''.concat(s[1], '.').concat(s[0], '.').concat(s[3]), + ''.concat(s[1], '.').concat(s[2], '.').concat(s[0]), + ''.concat(s[1], '.').concat(s[2], '.').concat(s[3]), + ''.concat(s[1], '.').concat(s[3], '.').concat(s[0]), + ''.concat(s[1], '.').concat(s[3], '.').concat(s[2]), + ''.concat(s[2], '.').concat(s[0], '.').concat(s[1]), + ''.concat(s[2], '.').concat(s[0], '.').concat(s[3]), + ''.concat(s[2], '.').concat(s[1], '.').concat(s[0]), + ''.concat(s[2], '.').concat(s[1], '.').concat(s[3]), + ''.concat(s[2], '.').concat(s[3], '.').concat(s[0]), + ''.concat(s[2], '.').concat(s[3], '.').concat(s[1]), + ''.concat(s[3], '.').concat(s[0], '.').concat(s[1]), + ''.concat(s[3], '.').concat(s[0], '.').concat(s[2]), + ''.concat(s[3], '.').concat(s[1], '.').concat(s[0]), + ''.concat(s[3], '.').concat(s[1], '.').concat(s[2]), + ''.concat(s[3], '.').concat(s[2], '.').concat(s[0]), + ''.concat(s[3], '.').concat(s[2], '.').concat(s[1]), + ''.concat(s[0], '.').concat(s[1], '.').concat(s[2], '.').concat(s[3]), + ''.concat(s[0], '.').concat(s[1], '.').concat(s[3], '.').concat(s[2]), + ''.concat(s[0], '.').concat(s[2], '.').concat(s[1], '.').concat(s[3]), + ''.concat(s[0], '.').concat(s[2], '.').concat(s[3], '.').concat(s[1]), + ''.concat(s[0], '.').concat(s[3], '.').concat(s[1], '.').concat(s[2]), + ''.concat(s[0], '.').concat(s[3], '.').concat(s[2], '.').concat(s[1]), + ''.concat(s[1], '.').concat(s[0], '.').concat(s[2], '.').concat(s[3]), + ''.concat(s[1], '.').concat(s[0], '.').concat(s[3], '.').concat(s[2]), + ''.concat(s[1], '.').concat(s[2], '.').concat(s[0], '.').concat(s[3]), + ''.concat(s[1], '.').concat(s[2], '.').concat(s[3], '.').concat(s[0]), + ''.concat(s[1], '.').concat(s[3], '.').concat(s[0], '.').concat(s[2]), + ''.concat(s[1], '.').concat(s[3], '.').concat(s[2], '.').concat(s[0]), + ''.concat(s[2], '.').concat(s[0], '.').concat(s[1], '.').concat(s[3]), + ''.concat(s[2], '.').concat(s[0], '.').concat(s[3], '.').concat(s[1]), + ''.concat(s[2], '.').concat(s[1], '.').concat(s[0], '.').concat(s[3]), + ''.concat(s[2], '.').concat(s[1], '.').concat(s[3], '.').concat(s[0]), + ''.concat(s[2], '.').concat(s[3], '.').concat(s[0], '.').concat(s[1]), + ''.concat(s[2], '.').concat(s[3], '.').concat(s[1], '.').concat(s[0]), + ''.concat(s[3], '.').concat(s[0], '.').concat(s[1], '.').concat(s[2]), + ''.concat(s[3], '.').concat(s[0], '.').concat(s[2], '.').concat(s[1]), + ''.concat(s[3], '.').concat(s[1], '.').concat(s[0], '.').concat(s[2]), + ''.concat(s[3], '.').concat(s[1], '.').concat(s[2], '.').concat(s[0]), + ''.concat(s[3], '.').concat(s[2], '.').concat(s[0], '.').concat(s[1]), + ''.concat(s[3], '.').concat(s[2], '.').concat(s[1], '.').concat(s[0]) + ] + : void 0; + })(s)), + Tx[o] + ); + })( + s.filter(function (s) { + return 'token' !== s; + }) + ).reduce(function (s, o) { + return _objectSpread(_objectSpread({}, s), i[o]); + }, o); + } + function createClassNameString(s) { + return s.join(' '); + } + function createElement(s) { + var o = s.node, + i = s.stylesheet, + u = s.style, + _ = void 0 === u ? {} : u, + w = s.useInlineStyles, + x = s.key, + C = o.properties, + j = o.type, + L = o.tagName, + B = o.value; + if ('text' === j) return B; + if (L) { + var $, + V = (function createChildren(s, o) { + var i = 0; + return function (u) { + return ( + (i += 1), + u.map(function (u, _) { + return createElement({ + node: u, + stylesheet: s, + useInlineStyles: o, + key: 'code-segment-'.concat(i, '-').concat(_) + }); + }) + ); + }; + })(i, w); + if (w) { + var U = Object.keys(i).reduce(function (s, o) { + return ( + o.split('.').forEach(function (o) { + s.includes(o) || s.push(o); + }), + s + ); + }, []), + z = C.className && C.className.includes('token') ? ['token'] : [], + Y = + C.className && + z.concat( + C.className.filter(function (s) { + return !U.includes(s); + }) + ); + $ = _objectSpread( + _objectSpread({}, C), + {}, + { + className: createClassNameString(Y) || void 0, + style: createStyleObject(C.className, Object.assign({}, C.style, _), i) + } + ); + } else + $ = _objectSpread( + _objectSpread({}, C), + {}, + { className: createClassNameString(C.className) } + ); + var Z = V(o.children); + return Pe.createElement(L, extends_extends({ key: x }, $), Z); + } + } + var Nx = [ + 'language', + 'children', + 'style', + 'customStyle', + 'codeTagProps', + 'useInlineStyles', + 'showLineNumbers', + 'showInlineLineNumbers', + 'startingLineNumber', + 'lineNumberContainerStyle', + 'lineNumberStyle', + 'wrapLines', + 'wrapLongLines', + 'lineProps', + 'renderer', + 'PreTag', + 'CodeTag', + 'code', + 'astGenerator' + ]; + function highlight_ownKeys(s, o) { + var i = Object.keys(s); + if (Object.getOwnPropertySymbols) { + var u = Object.getOwnPropertySymbols(s); + (o && + (u = u.filter(function (o) { + return Object.getOwnPropertyDescriptor(s, o).enumerable; + })), + i.push.apply(i, u)); + } + return i; + } + function highlight_objectSpread(s) { + for (var o = 1; o < arguments.length; o++) { + var i = null != arguments[o] ? arguments[o] : {}; + o % 2 + ? highlight_ownKeys(Object(i), !0).forEach(function (o) { + defineProperty_defineProperty(s, o, i[o]); + }) + : Object.getOwnPropertyDescriptors + ? Object.defineProperties(s, Object.getOwnPropertyDescriptors(i)) + : highlight_ownKeys(Object(i)).forEach(function (o) { + Object.defineProperty(s, o, Object.getOwnPropertyDescriptor(i, o)); + }); + } + return s; + } + var Rx = /\n/g; + function AllLineNumbers(s) { + var o = s.codeString, + i = s.codeStyle, + u = s.containerStyle, + _ = void 0 === u ? { float: 'left', paddingRight: '10px' } : u, + w = s.numberStyle, + x = void 0 === w ? {} : w, + C = s.startingLineNumber; + return Pe.createElement( + 'code', + { style: Object.assign({}, i, _) }, + (function getAllLineNumbers(s) { + var o = s.lines, + i = s.startingLineNumber, + u = s.style; + return o.map(function (s, o) { + var _ = o + i; + return Pe.createElement( + 'span', + { + key: 'line-'.concat(o), + className: 'react-syntax-highlighter-line-number', + style: 'function' == typeof u ? u(_) : u + }, + ''.concat(_, '\n') + ); + }); + })({ lines: o.replace(/\n$/, '').split('\n'), style: x, startingLineNumber: C }) + ); + } + function getInlineLineNumber(s, o) { + return { + type: 'element', + tagName: 'span', + properties: { + key: 'line-number--'.concat(s), + className: ['comment', 'linenumber', 'react-syntax-highlighter-line-number'], + style: o + }, + children: [{ type: 'text', value: s }] + }; + } + function assembleLineNumberStyles(s, o, i) { + var u, + _ = { + display: 'inline-block', + minWidth: ((u = i), ''.concat(u.toString().length, '.25em')), + paddingRight: '1em', + textAlign: 'right', + userSelect: 'none' + }, + w = 'function' == typeof s ? s(o) : s; + return highlight_objectSpread(highlight_objectSpread({}, _), w); + } + function createLineElement(s) { + var o = s.children, + i = s.lineNumber, + u = s.lineNumberStyle, + _ = s.largestLineNumber, + w = s.showInlineLineNumbers, + x = s.lineProps, + C = void 0 === x ? {} : x, + j = s.className, + L = void 0 === j ? [] : j, + B = s.showLineNumbers, + $ = s.wrapLongLines, + V = s.wrapLines, + U = + void 0 !== V && V + ? highlight_objectSpread({}, 'function' == typeof C ? C(i) : C) + : {}; + if ( + ((U.className = U.className + ? [].concat( + toConsumableArray_toConsumableArray(U.className.trim().split(/\s+/)), + toConsumableArray_toConsumableArray(L) + ) + : L), + i && w) + ) { + var z = assembleLineNumberStyles(u, i, _); + o.unshift(getInlineLineNumber(i, z)); + } + return ( + $ & B && (U.style = highlight_objectSpread({ display: 'flex' }, U.style)), + { type: 'element', tagName: 'span', properties: U, children: o } + ); + } + function flattenCodeTree(s) { + for ( + var o = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : [], + i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : [], + u = 0; + u < s.length; + u++ + ) { + var _ = s[u]; + if ('text' === _.type) + i.push( + createLineElement({ + children: [_], + className: toConsumableArray_toConsumableArray(new Set(o)) + }) + ); + else if (_.children) { + var w = o.concat(_.properties.className); + flattenCodeTree(_.children, w).forEach(function (s) { + return i.push(s); + }); + } + } + return i; + } + function processLines(s, o, i, u, _, w, x, C, j) { + var L, + B = flattenCodeTree(s.value), + $ = [], + V = -1, + U = 0; + function createLine(s, w) { + var L = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : []; + return o || L.length > 0 + ? (function createWrappedLine(s, w) { + return createLineElement({ + children: s, + lineNumber: w, + lineNumberStyle: C, + largestLineNumber: x, + showInlineLineNumbers: _, + lineProps: i, + className: arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : [], + showLineNumbers: u, + wrapLongLines: j, + wrapLines: o + }); + })(s, w, L) + : (function createUnwrappedLine(s, o) { + if (u && o && _) { + var i = assembleLineNumberStyles(C, o, x); + s.unshift(getInlineLineNumber(o, i)); + } + return s; + })(s, w); + } + for ( + var z = function _loop() { + var s = B[U], + o = s.children[0].value, + i = (function getNewLines(s) { + return s.match(Rx); + })(o); + if (i) { + var _ = o.split('\n'); + (_.forEach(function (o, i) { + var x = u && $.length + w, + C = { type: 'text', value: ''.concat(o, '\n') }; + if (0 === i) { + var j = createLine( + B.slice(V + 1, U).concat( + createLineElement({ children: [C], className: s.properties.className }) + ), + x + ); + $.push(j); + } else if (i === _.length - 1) { + var L = B[U + 1] && B[U + 1].children && B[U + 1].children[0], + z = { type: 'text', value: ''.concat(o) }; + if (L) { + var Y = createLineElement({ + children: [z], + className: s.properties.className + }); + B.splice(U + 1, 0, Y); + } else { + var Z = createLine([z], x, s.properties.className); + $.push(Z); + } + } else { + var ee = createLine([C], x, s.properties.className); + $.push(ee); + } + }), + (V = U)); + } + U++; + }; + U < B.length; + ) + z(); + if (V !== B.length - 1) { + var Y = B.slice(V + 1, B.length); + if (Y && Y.length) { + var Z = createLine(Y, u && $.length + w); + $.push(Z); + } + } + return o ? $ : (L = []).concat.apply(L, $); + } + function defaultRenderer(s) { + var o = s.rows, + i = s.stylesheet, + u = s.useInlineStyles; + return o.map(function (s, o) { + return createElement({ + node: s, + stylesheet: i, + useInlineStyles: u, + key: 'code-segement'.concat(o) + }); + }); + } + function isHighlightJs(s) { + return s && void 0 !== s.highlightAuto; + } + var Dx = __webpack_require__(43768), + Lx = (function highlight(s, o) { + return function SyntaxHighlighter(i) { + var u = i.language, + _ = i.children, + w = i.style, + x = void 0 === w ? o : w, + C = i.customStyle, + j = void 0 === C ? {} : C, + L = i.codeTagProps, + B = + void 0 === L + ? { + className: u ? 'language-'.concat(u) : void 0, + style: highlight_objectSpread( + highlight_objectSpread({}, x['code[class*="language-"]']), + x['code[class*="language-'.concat(u, '"]')] + ) + } + : L, + $ = i.useInlineStyles, + V = void 0 === $ || $, + U = i.showLineNumbers, + z = void 0 !== U && U, + Y = i.showInlineLineNumbers, + Z = void 0 === Y || Y, + ee = i.startingLineNumber, + ie = void 0 === ee ? 1 : ee, + ae = i.lineNumberContainerStyle, + le = i.lineNumberStyle, + ce = void 0 === le ? {} : le, + pe = i.wrapLines, + de = i.wrapLongLines, + fe = void 0 !== de && de, + ye = i.lineProps, + be = void 0 === ye ? {} : ye, + _e = i.renderer, + we = i.PreTag, + Se = void 0 === we ? 'pre' : we, + xe = i.CodeTag, + Te = void 0 === xe ? 'code' : xe, + Re = i.code, + qe = void 0 === Re ? (Array.isArray(_) ? _[0] : _) || '' : Re, + $e = i.astGenerator, + ze = (function _objectWithoutProperties(s, o) { + if (null == s) return {}; + var i, + u, + _ = (function _objectWithoutPropertiesLoose(s, o) { + if (null == s) return {}; + var i = {}; + for (var u in s) + if ({}.hasOwnProperty.call(s, u)) { + if (o.includes(u)) continue; + i[u] = s[u]; + } + return i; + })(s, o); + if (Object.getOwnPropertySymbols) { + var w = Object.getOwnPropertySymbols(s); + for (u = 0; u < w.length; u++) + ((i = w[u]), + o.includes(i) || ({}.propertyIsEnumerable.call(s, i) && (_[i] = s[i]))); + } + return _; + })(i, Nx); + $e = $e || s; + var We = z + ? Pe.createElement(AllLineNumbers, { + containerStyle: ae, + codeStyle: B.style || {}, + numberStyle: ce, + startingLineNumber: ie, + codeString: qe + }) + : null, + He = x.hljs || x['pre[class*="language-"]'] || { backgroundColor: '#fff' }, + Ye = isHighlightJs($e) ? 'hljs' : 'prismjs', + Xe = V + ? Object.assign({}, ze, { style: Object.assign({}, He, j) }) + : Object.assign({}, ze, { + className: ze.className ? ''.concat(Ye, ' ').concat(ze.className) : Ye, + style: Object.assign({}, j) + }); + if ( + ((B.style = highlight_objectSpread( + fe ? { whiteSpace: 'pre-wrap' } : { whiteSpace: 'pre' }, + B.style + )), + !$e) + ) + return Pe.createElement(Se, Xe, We, Pe.createElement(Te, B, qe)); + (((void 0 === pe && _e) || fe) && (pe = !0), (_e = _e || defaultRenderer)); + var Qe = [{ type: 'text', value: qe }], + et = (function getCodeTree(s) { + var o = s.astGenerator, + i = s.language, + u = s.code, + _ = s.defaultCodeValue; + if (isHighlightJs(o)) { + var w = (function (s, o) { + return -1 !== s.listLanguages().indexOf(o); + })(o, i); + return 'text' === i + ? { value: _, language: 'text' } + : w + ? o.highlight(i, u) + : o.highlightAuto(u); + } + try { + return i && 'text' !== i ? { value: o.highlight(u, i) } : { value: _ }; + } catch (s) { + return { value: _ }; + } + })({ astGenerator: $e, language: u, code: qe, defaultCodeValue: Qe }); + null === et.language && (et.value = Qe); + var tt = et.value.length; + 1 === tt && + 'text' === et.value[0].type && + (tt = et.value[0].value.split('\n').length); + var rt = processLines(et, pe, be, z, Z, ie, tt + ie, ce, fe); + return Pe.createElement( + Se, + Xe, + Pe.createElement( + Te, + B, + !Z && We, + _e({ rows: rt, stylesheet: x, useInlineStyles: V }) + ) + ); + }; + })(Dx, {}); + Lx.registerLanguage = Dx.registerLanguage; + const Bx = Lx; + var Fx = __webpack_require__(95089); + const qx = __webpack_require__.n(Fx)(); + var $x = __webpack_require__(65772); + const Vx = __webpack_require__.n($x)(); + var Ux = __webpack_require__(17285); + const zx = __webpack_require__.n(Ux)(); + var Wx = __webpack_require__(35344); + const Kx = __webpack_require__.n(Wx)(); + var Hx = __webpack_require__(17533); + const Jx = __webpack_require__.n(Hx)(); + var Gx = __webpack_require__(73402); + const Yx = __webpack_require__.n(Gx)(); + var Xx = __webpack_require__(26571); + const Zx = __webpack_require__.n(Xx)(), + after_load = () => { + (Bx.registerLanguage('json', Vx), + Bx.registerLanguage('js', qx), + Bx.registerLanguage('xml', zx), + Bx.registerLanguage('yaml', Jx), + Bx.registerLanguage('http', Yx), + Bx.registerLanguage('bash', Kx), + Bx.registerLanguage('powershell', Zx), + Bx.registerLanguage('javascript', qx)); + }, + Qx = { + hljs: { + display: 'block', + overflowX: 'auto', + padding: '0.5em', + background: '#333', + color: 'white' + }, + 'hljs-name': { fontWeight: 'bold' }, + 'hljs-strong': { fontWeight: 'bold' }, + 'hljs-code': { fontStyle: 'italic', color: '#888' }, + 'hljs-emphasis': { fontStyle: 'italic' }, + 'hljs-tag': { color: '#62c8f3' }, + 'hljs-variable': { color: '#ade5fc' }, + 'hljs-template-variable': { color: '#ade5fc' }, + 'hljs-selector-id': { color: '#ade5fc' }, + 'hljs-selector-class': { color: '#ade5fc' }, + 'hljs-string': { color: '#a2fca2' }, + 'hljs-bullet': { color: '#d36363' }, + 'hljs-type': { color: '#ffa' }, + 'hljs-title': { color: '#ffa' }, + 'hljs-section': { color: '#ffa' }, + 'hljs-attribute': { color: '#ffa' }, + 'hljs-quote': { color: '#ffa' }, + 'hljs-built_in': { color: '#ffa' }, + 'hljs-builtin-name': { color: '#ffa' }, + 'hljs-number': { color: '#d36363' }, + 'hljs-symbol': { color: '#d36363' }, + 'hljs-keyword': { color: '#fcc28c' }, + 'hljs-selector-tag': { color: '#fcc28c' }, + 'hljs-literal': { color: '#fcc28c' }, + 'hljs-comment': { color: '#888' }, + 'hljs-deletion': { color: '#333', backgroundColor: '#fc9b9b' }, + 'hljs-regexp': { color: '#c6b4f0' }, + 'hljs-link': { color: '#c6b4f0' }, + 'hljs-meta': { color: '#fc9b9b' }, + 'hljs-addition': { backgroundColor: '#a2fca2', color: '#333' } + }, + tk = { + agate: Qx, + arta: { + hljs: { + display: 'block', + overflowX: 'auto', + padding: '0.5em', + background: '#222', + color: '#aaa' + }, + 'hljs-subst': { color: '#aaa' }, + 'hljs-section': { color: '#fff', fontWeight: 'bold' }, + 'hljs-comment': { color: '#444' }, + 'hljs-quote': { color: '#444' }, + 'hljs-meta': { color: '#444' }, + 'hljs-string': { color: '#ffcc33' }, + 'hljs-symbol': { color: '#ffcc33' }, + 'hljs-bullet': { color: '#ffcc33' }, + 'hljs-regexp': { color: '#ffcc33' }, + 'hljs-number': { color: '#00cc66' }, + 'hljs-addition': { color: '#00cc66' }, + 'hljs-built_in': { color: '#32aaee' }, + 'hljs-builtin-name': { color: '#32aaee' }, + 'hljs-literal': { color: '#32aaee' }, + 'hljs-type': { color: '#32aaee' }, + 'hljs-template-variable': { color: '#32aaee' }, + 'hljs-attribute': { color: '#32aaee' }, + 'hljs-link': { color: '#32aaee' }, + 'hljs-keyword': { color: '#6644aa' }, + 'hljs-selector-tag': { color: '#6644aa' }, + 'hljs-name': { color: '#6644aa' }, + 'hljs-selector-id': { color: '#6644aa' }, + 'hljs-selector-class': { color: '#6644aa' }, + 'hljs-title': { color: '#bb1166' }, + 'hljs-variable': { color: '#bb1166' }, + 'hljs-deletion': { color: '#bb1166' }, + 'hljs-template-tag': { color: '#bb1166' }, + 'hljs-doctag': { fontWeight: 'bold' }, + 'hljs-strong': { fontWeight: 'bold' }, + 'hljs-emphasis': { fontStyle: 'italic' } + }, + monokai: { + hljs: { + display: 'block', + overflowX: 'auto', + padding: '0.5em', + background: '#272822', + color: '#ddd' + }, + 'hljs-tag': { color: '#f92672' }, + 'hljs-keyword': { color: '#f92672', fontWeight: 'bold' }, + 'hljs-selector-tag': { color: '#f92672', fontWeight: 'bold' }, + 'hljs-literal': { color: '#f92672', fontWeight: 'bold' }, + 'hljs-strong': { color: '#f92672' }, + 'hljs-name': { color: '#f92672' }, + 'hljs-code': { color: '#66d9ef' }, + 'hljs-class .hljs-title': { color: 'white' }, + 'hljs-attribute': { color: '#bf79db' }, + 'hljs-symbol': { color: '#bf79db' }, + 'hljs-regexp': { color: '#bf79db' }, + 'hljs-link': { color: '#bf79db' }, + 'hljs-string': { color: '#a6e22e' }, + 'hljs-bullet': { color: '#a6e22e' }, + 'hljs-subst': { color: '#a6e22e' }, + 'hljs-title': { color: '#a6e22e', fontWeight: 'bold' }, + 'hljs-section': { color: '#a6e22e', fontWeight: 'bold' }, + 'hljs-emphasis': { color: '#a6e22e' }, + 'hljs-type': { color: '#a6e22e', fontWeight: 'bold' }, + 'hljs-built_in': { color: '#a6e22e' }, + 'hljs-builtin-name': { color: '#a6e22e' }, + 'hljs-selector-attr': { color: '#a6e22e' }, + 'hljs-selector-pseudo': { color: '#a6e22e' }, + 'hljs-addition': { color: '#a6e22e' }, + 'hljs-variable': { color: '#a6e22e' }, + 'hljs-template-tag': { color: '#a6e22e' }, + 'hljs-template-variable': { color: '#a6e22e' }, + 'hljs-comment': { color: '#75715e' }, + 'hljs-quote': { color: '#75715e' }, + 'hljs-deletion': { color: '#75715e' }, + 'hljs-meta': { color: '#75715e' }, + 'hljs-doctag': { fontWeight: 'bold' }, + 'hljs-selector-id': { fontWeight: 'bold' } + }, + nord: { + hljs: { + display: 'block', + overflowX: 'auto', + padding: '0.5em', + background: '#2E3440', + color: '#D8DEE9' + }, + 'hljs-subst': { color: '#D8DEE9' }, + 'hljs-selector-tag': { color: '#81A1C1' }, + 'hljs-selector-id': { color: '#8FBCBB', fontWeight: 'bold' }, + 'hljs-selector-class': { color: '#8FBCBB' }, + 'hljs-selector-attr': { color: '#8FBCBB' }, + 'hljs-selector-pseudo': { color: '#88C0D0' }, + 'hljs-addition': { backgroundColor: 'rgba(163, 190, 140, 0.5)' }, + 'hljs-deletion': { backgroundColor: 'rgba(191, 97, 106, 0.5)' }, + 'hljs-built_in': { color: '#8FBCBB' }, + 'hljs-type': { color: '#8FBCBB' }, + 'hljs-class': { color: '#8FBCBB' }, + 'hljs-function': { color: '#88C0D0' }, + 'hljs-function > .hljs-title': { color: '#88C0D0' }, + 'hljs-keyword': { color: '#81A1C1' }, + 'hljs-literal': { color: '#81A1C1' }, + 'hljs-symbol': { color: '#81A1C1' }, + 'hljs-number': { color: '#B48EAD' }, + 'hljs-regexp': { color: '#EBCB8B' }, + 'hljs-string': { color: '#A3BE8C' }, + 'hljs-title': { color: '#8FBCBB' }, + 'hljs-params': { color: '#D8DEE9' }, + 'hljs-bullet': { color: '#81A1C1' }, + 'hljs-code': { color: '#8FBCBB' }, + 'hljs-emphasis': { fontStyle: 'italic' }, + 'hljs-formula': { color: '#8FBCBB' }, + 'hljs-strong': { fontWeight: 'bold' }, + 'hljs-link:hover': { textDecoration: 'underline' }, + 'hljs-quote': { color: '#4C566A' }, + 'hljs-comment': { color: '#4C566A' }, + 'hljs-doctag': { color: '#8FBCBB' }, + 'hljs-meta': { color: '#5E81AC' }, + 'hljs-meta-keyword': { color: '#5E81AC' }, + 'hljs-meta-string': { color: '#A3BE8C' }, + 'hljs-attr': { color: '#8FBCBB' }, + 'hljs-attribute': { color: '#D8DEE9' }, + 'hljs-builtin-name': { color: '#81A1C1' }, + 'hljs-name': { color: '#81A1C1' }, + 'hljs-section': { color: '#88C0D0' }, + 'hljs-tag': { color: '#81A1C1' }, + 'hljs-variable': { color: '#D8DEE9' }, + 'hljs-template-variable': { color: '#D8DEE9' }, + 'hljs-template-tag': { color: '#5E81AC' }, + 'abnf .hljs-attribute': { color: '#88C0D0' }, + 'abnf .hljs-symbol': { color: '#EBCB8B' }, + 'apache .hljs-attribute': { color: '#88C0D0' }, + 'apache .hljs-section': { color: '#81A1C1' }, + 'arduino .hljs-built_in': { color: '#88C0D0' }, + 'aspectj .hljs-meta': { color: '#D08770' }, + 'aspectj > .hljs-title': { color: '#88C0D0' }, + 'bnf .hljs-attribute': { color: '#8FBCBB' }, + 'clojure .hljs-name': { color: '#88C0D0' }, + 'clojure .hljs-symbol': { color: '#EBCB8B' }, + 'coq .hljs-built_in': { color: '#88C0D0' }, + 'cpp .hljs-meta-string': { color: '#8FBCBB' }, + 'css .hljs-built_in': { color: '#88C0D0' }, + 'css .hljs-keyword': { color: '#D08770' }, + 'diff .hljs-meta': { color: '#8FBCBB' }, + 'ebnf .hljs-attribute': { color: '#8FBCBB' }, + 'glsl .hljs-built_in': { color: '#88C0D0' }, + 'groovy .hljs-meta:not(:first-child)': { color: '#D08770' }, + 'haxe .hljs-meta': { color: '#D08770' }, + 'java .hljs-meta': { color: '#D08770' }, + 'ldif .hljs-attribute': { color: '#8FBCBB' }, + 'lisp .hljs-name': { color: '#88C0D0' }, + 'lua .hljs-built_in': { color: '#88C0D0' }, + 'moonscript .hljs-built_in': { color: '#88C0D0' }, + 'nginx .hljs-attribute': { color: '#88C0D0' }, + 'nginx .hljs-section': { color: '#5E81AC' }, + 'pf .hljs-built_in': { color: '#88C0D0' }, + 'processing .hljs-built_in': { color: '#88C0D0' }, + 'scss .hljs-keyword': { color: '#81A1C1' }, + 'stylus .hljs-keyword': { color: '#81A1C1' }, + 'swift .hljs-meta': { color: '#D08770' }, + 'vim .hljs-built_in': { color: '#88C0D0', fontStyle: 'italic' }, + 'yaml .hljs-meta': { color: '#D08770' } + }, + obsidian: { + hljs: { + display: 'block', + overflowX: 'auto', + padding: '0.5em', + background: '#282b2e', + color: '#e0e2e4' + }, + 'hljs-keyword': { color: '#93c763', fontWeight: 'bold' }, + 'hljs-selector-tag': { color: '#93c763', fontWeight: 'bold' }, + 'hljs-literal': { color: '#93c763', fontWeight: 'bold' }, + 'hljs-selector-id': { color: '#93c763' }, + 'hljs-number': { color: '#ffcd22' }, + 'hljs-attribute': { color: '#668bb0' }, + 'hljs-code': { color: 'white' }, + 'hljs-class .hljs-title': { color: 'white' }, + 'hljs-section': { color: 'white', fontWeight: 'bold' }, + 'hljs-regexp': { color: '#d39745' }, + 'hljs-link': { color: '#d39745' }, + 'hljs-meta': { color: '#557182' }, + 'hljs-tag': { color: '#8cbbad' }, + 'hljs-name': { color: '#8cbbad', fontWeight: 'bold' }, + 'hljs-bullet': { color: '#8cbbad' }, + 'hljs-subst': { color: '#8cbbad' }, + 'hljs-emphasis': { color: '#8cbbad' }, + 'hljs-type': { color: '#8cbbad', fontWeight: 'bold' }, + 'hljs-built_in': { color: '#8cbbad' }, + 'hljs-selector-attr': { color: '#8cbbad' }, + 'hljs-selector-pseudo': { color: '#8cbbad' }, + 'hljs-addition': { color: '#8cbbad' }, + 'hljs-variable': { color: '#8cbbad' }, + 'hljs-template-tag': { color: '#8cbbad' }, + 'hljs-template-variable': { color: '#8cbbad' }, + 'hljs-string': { color: '#ec7600' }, + 'hljs-symbol': { color: '#ec7600' }, + 'hljs-comment': { color: '#818e96' }, + 'hljs-quote': { color: '#818e96' }, + 'hljs-deletion': { color: '#818e96' }, + 'hljs-selector-class': { color: '#A082BD' }, + 'hljs-doctag': { fontWeight: 'bold' }, + 'hljs-title': { fontWeight: 'bold' }, + 'hljs-strong': { fontWeight: 'bold' } + }, + 'tomorrow-night': { + 'hljs-comment': { color: '#969896' }, + 'hljs-quote': { color: '#969896' }, + 'hljs-variable': { color: '#cc6666' }, + 'hljs-template-variable': { color: '#cc6666' }, + 'hljs-tag': { color: '#cc6666' }, + 'hljs-name': { color: '#cc6666' }, + 'hljs-selector-id': { color: '#cc6666' }, + 'hljs-selector-class': { color: '#cc6666' }, + 'hljs-regexp': { color: '#cc6666' }, + 'hljs-deletion': { color: '#cc6666' }, + 'hljs-number': { color: '#de935f' }, + 'hljs-built_in': { color: '#de935f' }, + 'hljs-builtin-name': { color: '#de935f' }, + 'hljs-literal': { color: '#de935f' }, + 'hljs-type': { color: '#de935f' }, + 'hljs-params': { color: '#de935f' }, + 'hljs-meta': { color: '#de935f' }, + 'hljs-link': { color: '#de935f' }, + 'hljs-attribute': { color: '#f0c674' }, + 'hljs-string': { color: '#b5bd68' }, + 'hljs-symbol': { color: '#b5bd68' }, + 'hljs-bullet': { color: '#b5bd68' }, + 'hljs-addition': { color: '#b5bd68' }, + 'hljs-title': { color: '#81a2be' }, + 'hljs-section': { color: '#81a2be' }, + 'hljs-keyword': { color: '#b294bb' }, + 'hljs-selector-tag': { color: '#b294bb' }, + hljs: { + display: 'block', + overflowX: 'auto', + background: '#1d1f21', + color: '#c5c8c6', + padding: '0.5em' + }, + 'hljs-emphasis': { fontStyle: 'italic' }, + 'hljs-strong': { fontWeight: 'bold' } + }, + idea: { + hljs: { + display: 'block', + overflowX: 'auto', + padding: '0.5em', + color: '#000', + background: '#fff' + }, + 'hljs-subst': { fontWeight: 'normal', color: '#000' }, + 'hljs-title': { fontWeight: 'normal', color: '#000' }, + 'hljs-comment': { color: '#808080', fontStyle: 'italic' }, + 'hljs-quote': { color: '#808080', fontStyle: 'italic' }, + 'hljs-meta': { color: '#808000' }, + 'hljs-tag': { background: '#efefef' }, + 'hljs-section': { fontWeight: 'bold', color: '#000080' }, + 'hljs-name': { fontWeight: 'bold', color: '#000080' }, + 'hljs-literal': { fontWeight: 'bold', color: '#000080' }, + 'hljs-keyword': { fontWeight: 'bold', color: '#000080' }, + 'hljs-selector-tag': { fontWeight: 'bold', color: '#000080' }, + 'hljs-type': { fontWeight: 'bold', color: '#000080' }, + 'hljs-selector-id': { fontWeight: 'bold', color: '#000080' }, + 'hljs-selector-class': { fontWeight: 'bold', color: '#000080' }, + 'hljs-attribute': { fontWeight: 'bold', color: '#0000ff' }, + 'hljs-number': { fontWeight: 'normal', color: '#0000ff' }, + 'hljs-regexp': { fontWeight: 'normal', color: '#0000ff' }, + 'hljs-link': { fontWeight: 'normal', color: '#0000ff' }, + 'hljs-string': { color: '#008000', fontWeight: 'bold' }, + 'hljs-symbol': { color: '#000', background: '#d0eded', fontStyle: 'italic' }, + 'hljs-bullet': { color: '#000', background: '#d0eded', fontStyle: 'italic' }, + 'hljs-formula': { color: '#000', background: '#d0eded', fontStyle: 'italic' }, + 'hljs-doctag': { textDecoration: 'underline' }, + 'hljs-variable': { color: '#660e7a' }, + 'hljs-template-variable': { color: '#660e7a' }, + 'hljs-addition': { background: '#baeeba' }, + 'hljs-deletion': { background: '#ffc8bd' }, + 'hljs-emphasis': { fontStyle: 'italic' }, + 'hljs-strong': { fontWeight: 'bold' } + } + }, + rk = Qx, + components_SyntaxHighlighter = ({ + language: s, + className: o = '', + getConfigs: i, + syntaxHighlighting: u = {}, + children: _ = '' + }) => { + const w = i().syntaxHighlight.theme, + { styles: x, defaultStyle: C } = u, + j = x?.[w] ?? C; + return Pe.createElement(Bx, { language: s, className: o, style: j }, _); + }; + var nk = __webpack_require__(5419), + sk = __webpack_require__.n(nk); + const components_HighlightCode = ({ + fileName: s = 'response.txt', + className: o, + downloadable: i, + getComponent: u, + canCopy: _, + language: w, + children: x + }) => { + const C = (0, Pe.useRef)(null), + j = u('SyntaxHighlighter', !0), + handlePreventYScrollingBeyondElement = (s) => { + const { target: o, deltaY: i } = s, + { scrollHeight: u, offsetHeight: _, scrollTop: w } = o; + u > _ && ((0 === w && i < 0) || (_ + w >= u && i > 0)) && s.preventDefault(); + }; + return ( + (0, Pe.useEffect)(() => { + const s = Array.from(C.current.childNodes).filter( + (s) => !!s.nodeType && s.classList.contains('microlight') + ); + return ( + s.forEach((s) => + s.addEventListener('mousewheel', handlePreventYScrollingBeyondElement, { + passive: !1 + }) + ), + () => { + s.forEach((s) => + s.removeEventListener('mousewheel', handlePreventYScrollingBeyondElement) + ); + } + ); + }, [x, o, w]), + Pe.createElement( + 'div', + { className: 'highlight-code', ref: C }, + _ && + Pe.createElement( + 'div', + { className: 'copy-to-clipboard' }, + Pe.createElement( + Jn.CopyToClipboard, + { text: x }, + Pe.createElement('button', null) + ) + ), + i + ? Pe.createElement( + 'button', + { + className: 'download-contents', + onClick: () => { + sk()(x, s); + } + }, + 'Download' + ) + : null, + Pe.createElement( + j, + { + language: w, + className: Hn()(o, 'microlight'), + renderPlainText: ({ children: s, PlainTextViewer: i }) => + Pe.createElement(i, { className: o }, s) + }, + x + ) + ) + ); + }, + components_PlainTextViewer = ({ className: s = '', children: o }) => + Pe.createElement('pre', { className: Hn()('microlight', s) }, o), + wrap_components_SyntaxHighlighter = + (s, o) => + ({ renderPlainText: i, children: u, ..._ }) => { + const w = o.getConfigs().syntaxHighlight.activated, + x = o.getComponent('PlainTextViewer'); + return w || 'function' != typeof i + ? w + ? Pe.createElement(s, _, u) + : Pe.createElement(x, null, u) + : i({ children: u, PlainTextViewer: x }); + }, + SyntaxHighlightingPlugin1 = () => ({ + afterLoad: after_load, + rootInjects: { syntaxHighlighting: { styles: tk, defaultStyle: rk } }, + components: { + SyntaxHighlighter: components_SyntaxHighlighter, + HighlightCode: components_HighlightCode, + PlainTextViewer: components_PlainTextViewer + } + }), + SyntaxHighlightingPlugin2 = () => ({ + wrapComponents: { SyntaxHighlighter: wrap_components_SyntaxHighlighter } + }), + syntax_highlighting = () => [SyntaxHighlightingPlugin1, SyntaxHighlightingPlugin2], + versions_after_load = () => { + const { + GIT_DIRTY: s, + GIT_COMMIT: o, + PACKAGE_VERSION: i, + BUILD_TIME: u + } = { + PACKAGE_VERSION: '5.18.2', + GIT_COMMIT: 'g1dd1f7cc', + GIT_DIRTY: !0, + BUILD_TIME: 'Thu, 07 Nov 2024 14:01:17 GMT' + }; + ((at.versions = at.versions || {}), + (at.versions.swaggerUI = { + version: i, + gitRevision: o, + gitDirty: s, + buildTimestamp: u + })); + }, + versions = () => ({ afterLoad: versions_after_load }); + var ok = __webpack_require__(47248), + lk = __webpack_require__.n(ok); + const uk = console.error, + withErrorBoundary = (s) => (o) => { + const { getComponent: i, fn: u } = s(), + _ = i('ErrorBoundary'), + w = u.getDisplayName(o); + class WithErrorBoundary extends Pe.Component { + render() { + return Pe.createElement( + _, + { targetName: w, getComponent: i, fn: u }, + Pe.createElement(o, Rn()({}, this.props, this.context)) + ); + } + } + var x; + return ( + (WithErrorBoundary.displayName = `WithErrorBoundary(${w})`), + (x = o).prototype && + x.prototype.isReactComponent && + (WithErrorBoundary.prototype.mapStateToProps = o.prototype.mapStateToProps), + WithErrorBoundary + ); + }, + fallback = ({ name: s }) => + Pe.createElement( + 'div', + { className: 'fallback' }, + '😱 ', + Pe.createElement( + 'i', + null, + 'Could not render ', + 't' === s ? 'this component' : s, + ', see the console.' + ) + ); + class ErrorBoundary extends Pe.Component { + static defaultProps = { + targetName: 'this component', + getComponent: () => fallback, + fn: { componentDidCatch: uk }, + children: null + }; + static getDerivedStateFromError(s) { + return { hasError: !0, error: s }; + } + constructor(...s) { + (super(...s), (this.state = { hasError: !1, error: null })); + } + componentDidCatch(s, o) { + this.props.fn.componentDidCatch(s, o); + } + render() { + const { getComponent: s, targetName: o, children: i } = this.props; + if (this.state.hasError) { + const i = s('Fallback'); + return Pe.createElement(i, { name: o }); + } + return i; + } + } + const pk = ErrorBoundary, + safe_render = + ({ componentList: s = [], fullOverride: o = !1 } = {}) => + ({ getSystem: i }) => { + const u = o + ? s + : [ + 'App', + 'BaseLayout', + 'VersionPragmaFilter', + 'InfoContainer', + 'ServersContainer', + 'SchemesContainer', + 'AuthorizeBtnContainer', + 'FilterContainer', + 'Operations', + 'OperationContainer', + 'parameters', + 'responses', + 'OperationServers', + 'Models', + 'ModelWrapper', + ...s + ], + _ = lk()( + u, + Array(u.length).fill((s, { fn: o }) => o.withErrorBoundary(s)) + ); + return { + fn: { componentDidCatch: uk, withErrorBoundary: withErrorBoundary(i) }, + components: { ErrorBoundary: pk, Fallback: fallback }, + wrapComponents: _ + }; + }; + class App extends Pe.Component { + getLayout() { + const { getComponent: s, layoutSelectors: o } = this.props, + i = o.current(), + u = s(i, !0); + return u || (() => Pe.createElement('h1', null, ' No layout defined for "', i, '" ')); + } + render() { + const s = this.getLayout(); + return Pe.createElement(s, null); + } + } + const fk = App; + class AuthorizationPopup extends Pe.Component { + close = () => { + let { authActions: s } = this.props; + s.showDefinitions(!1); + }; + render() { + let { + authSelectors: s, + authActions: o, + getComponent: i, + errSelectors: u, + specSelectors: _, + fn: { AST: w = {} } + } = this.props, + x = s.shownDefinitions(); + const C = i('auths'), + j = i('CloseIcon'); + return Pe.createElement( + 'div', + { className: 'dialog-ux' }, + Pe.createElement('div', { className: 'backdrop-ux' }), + Pe.createElement( + 'div', + { className: 'modal-ux' }, + Pe.createElement( + 'div', + { className: 'modal-dialog-ux' }, + Pe.createElement( + 'div', + { className: 'modal-ux-inner' }, + Pe.createElement( + 'div', + { className: 'modal-ux-header' }, + Pe.createElement('h3', null, 'Available authorizations'), + Pe.createElement( + 'button', + { type: 'button', className: 'close-modal', onClick: this.close }, + Pe.createElement(j, null) + ) + ), + Pe.createElement( + 'div', + { className: 'modal-ux-content' }, + x.valueSeq().map((x, j) => + Pe.createElement(C, { + key: j, + AST: w, + definitions: x, + getComponent: i, + errSelectors: u, + authSelectors: s, + authActions: o, + specSelectors: _ + }) + ) + ) + ) + ) + ) + ); + } + } + class AuthorizeBtn extends Pe.Component { + render() { + let { isAuthorized: s, showPopup: o, onClick: i, getComponent: u } = this.props; + const _ = u('authorizationPopup', !0), + w = u('LockAuthIcon', !0), + x = u('UnlockAuthIcon', !0); + return Pe.createElement( + 'div', + { className: 'auth-wrapper' }, + Pe.createElement( + 'button', + { className: s ? 'btn authorize locked' : 'btn authorize unlocked', onClick: i }, + Pe.createElement('span', null, 'Authorize'), + s ? Pe.createElement(w, null) : Pe.createElement(x, null) + ), + o && Pe.createElement(_, null) + ); + } + } + class AuthorizeBtnContainer extends Pe.Component { + render() { + const { + authActions: s, + authSelectors: o, + specSelectors: i, + getComponent: u + } = this.props, + _ = i.securityDefinitions(), + w = o.definitionsToAuthorize(), + x = u('authorizeBtn'); + return _ + ? Pe.createElement(x, { + onClick: () => s.showDefinitions(w), + isAuthorized: !!o.authorized().size, + showPopup: !!o.shownDefinitions(), + getComponent: u + }) + : null; + } + } + class AuthorizeOperationBtn extends Pe.Component { + onClick = (s) => { + s.stopPropagation(); + let { onClick: o } = this.props; + o && o(); + }; + render() { + let { isAuthorized: s, getComponent: o } = this.props; + const i = o('LockAuthOperationIcon', !0), + u = o('UnlockAuthOperationIcon', !0); + return Pe.createElement( + 'button', + { + className: 'authorization__btn', + 'aria-label': s ? 'authorization button locked' : 'authorization button unlocked', + onClick: this.onClick + }, + s + ? Pe.createElement(i, { className: 'locked' }) + : Pe.createElement(u, { className: 'unlocked' }) + ); + } + } + class Auths extends Pe.Component { + constructor(s, o) { + (super(s, o), (this.state = {})); + } + onAuthChange = (s) => { + let { name: o } = s; + this.setState({ [o]: s }); + }; + submitAuth = (s) => { + s.preventDefault(); + let { authActions: o } = this.props; + o.authorizeWithPersistOption(this.state); + }; + logoutClick = (s) => { + s.preventDefault(); + let { authActions: o, definitions: i } = this.props, + u = i.map((s, o) => o).toArray(); + (this.setState(u.reduce((s, o) => ((s[o] = ''), s), {})), o.logoutWithPersistOption(u)); + }; + close = (s) => { + s.preventDefault(); + let { authActions: o } = this.props; + o.showDefinitions(!1); + }; + render() { + let { definitions: s, getComponent: o, authSelectors: i, errSelectors: u } = this.props; + const _ = o('AuthItem'), + w = o('oauth2', !0), + x = o('Button'); + let C = i.authorized(), + j = s.filter((s, o) => !!C.get(o)), + L = s.filter((s) => 'oauth2' !== s.get('type')), + B = s.filter((s) => 'oauth2' === s.get('type')); + return Pe.createElement( + 'div', + { className: 'auth-container' }, + !!L.size && + Pe.createElement( + 'form', + { onSubmit: this.submitAuth }, + L.map((s, i) => + Pe.createElement(_, { + key: i, + schema: s, + name: i, + getComponent: o, + onAuthChange: this.onAuthChange, + authorized: C, + errSelectors: u + }) + ).toArray(), + Pe.createElement( + 'div', + { className: 'auth-btn-wrapper' }, + L.size === j.size + ? Pe.createElement( + x, + { + className: 'btn modal-btn auth', + onClick: this.logoutClick, + 'aria-label': 'Remove authorization' + }, + 'Logout' + ) + : Pe.createElement( + x, + { + type: 'submit', + className: 'btn modal-btn auth authorize', + 'aria-label': 'Apply credentials' + }, + 'Authorize' + ), + Pe.createElement( + x, + { className: 'btn modal-btn auth btn-done', onClick: this.close }, + 'Close' + ) + ) + ), + B && B.size + ? Pe.createElement( + 'div', + null, + Pe.createElement( + 'div', + { className: 'scope-def' }, + Pe.createElement( + 'p', + null, + 'Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.' + ), + Pe.createElement( + 'p', + null, + 'API requires the following scopes. Select which ones you want to grant to Swagger UI.' + ) + ), + s + .filter((s) => 'oauth2' === s.get('type')) + .map((s, o) => + Pe.createElement( + 'div', + { key: o }, + Pe.createElement(w, { authorized: C, schema: s, name: o }) + ) + ) + .toArray() + ) + : null + ); + } + } + class auth_item_Auths extends Pe.Component { + render() { + let { + schema: s, + name: o, + getComponent: i, + onAuthChange: u, + authorized: _, + errSelectors: w + } = this.props; + const x = i('apiKeyAuth'), + C = i('basicAuth'); + let j; + const L = s.get('type'); + switch (L) { + case 'apiKey': + j = Pe.createElement(x, { + key: o, + schema: s, + name: o, + errSelectors: w, + authorized: _, + getComponent: i, + onChange: u + }); + break; + case 'basic': + j = Pe.createElement(C, { + key: o, + schema: s, + name: o, + errSelectors: w, + authorized: _, + getComponent: i, + onChange: u + }); + break; + default: + j = Pe.createElement('div', { key: o }, 'Unknown security definition type ', L); + } + return Pe.createElement('div', { key: `${o}-jump` }, j); + } + } + class AuthError extends Pe.Component { + render() { + let { error: s } = this.props, + o = s.get('level'), + i = s.get('message'), + u = s.get('source'); + return Pe.createElement( + 'div', + { className: 'errors' }, + Pe.createElement('b', null, u, ' ', o), + Pe.createElement('span', null, i) + ); + } + } + class ApiKeyAuth extends Pe.Component { + constructor(s, o) { + super(s, o); + let { name: i, schema: u } = this.props, + _ = this.getValue(); + this.state = { name: i, schema: u, value: _ }; + } + getValue() { + let { name: s, authorized: o } = this.props; + return o && o.getIn([s, 'value']); + } + onChange = (s) => { + let { onChange: o } = this.props, + i = s.target.value, + u = Object.assign({}, this.state, { value: i }); + (this.setState(u), o(u)); + }; + render() { + let { schema: s, getComponent: o, errSelectors: i, name: u } = this.props; + const _ = o('Input'), + w = o('Row'), + x = o('Col'), + C = o('authError'), + j = o('Markdown', !0), + L = o('JumpToPath', !0); + let B = this.getValue(), + $ = i.allErrors().filter((s) => s.get('authId') === u); + return Pe.createElement( + 'div', + null, + Pe.createElement( + 'h4', + null, + Pe.createElement('code', null, u || s.get('name')), + ' (apiKey)', + Pe.createElement(L, { path: ['securityDefinitions', u] }) + ), + B && Pe.createElement('h6', null, 'Authorized'), + Pe.createElement(w, null, Pe.createElement(j, { source: s.get('description') })), + Pe.createElement( + w, + null, + Pe.createElement('p', null, 'Name: ', Pe.createElement('code', null, s.get('name'))) + ), + Pe.createElement( + w, + null, + Pe.createElement('p', null, 'In: ', Pe.createElement('code', null, s.get('in'))) + ), + Pe.createElement( + w, + null, + Pe.createElement('label', { htmlFor: 'api_key_value' }, 'Value:'), + B + ? Pe.createElement('code', null, ' ****** ') + : Pe.createElement( + x, + null, + Pe.createElement(_, { + id: 'api_key_value', + type: 'text', + onChange: this.onChange, + autoFocus: !0 + }) + ) + ), + $.valueSeq().map((s, o) => Pe.createElement(C, { error: s, key: o })) + ); + } + } + class BasicAuth extends Pe.Component { + constructor(s, o) { + super(s, o); + let { schema: i, name: u } = this.props, + _ = this.getValue().username; + this.state = { name: u, schema: i, value: _ ? { username: _ } : {} }; + } + getValue() { + let { authorized: s, name: o } = this.props; + return (s && s.getIn([o, 'value'])) || {}; + } + onChange = (s) => { + let { onChange: o } = this.props, + { value: i, name: u } = s.target, + _ = this.state.value; + ((_[u] = i), this.setState({ value: _ }), o(this.state)); + }; + render() { + let { schema: s, getComponent: o, name: i, errSelectors: u } = this.props; + const _ = o('Input'), + w = o('Row'), + x = o('Col'), + C = o('authError'), + j = o('JumpToPath', !0), + L = o('Markdown', !0); + let B = this.getValue().username, + $ = u.allErrors().filter((s) => s.get('authId') === i); + return Pe.createElement( + 'div', + null, + Pe.createElement( + 'h4', + null, + 'Basic authorization', + Pe.createElement(j, { path: ['securityDefinitions', i] }) + ), + B && Pe.createElement('h6', null, 'Authorized'), + Pe.createElement(w, null, Pe.createElement(L, { source: s.get('description') })), + Pe.createElement( + w, + null, + Pe.createElement('label', { htmlFor: 'auth_username' }, 'Username:'), + B + ? Pe.createElement('code', null, ' ', B, ' ') + : Pe.createElement( + x, + null, + Pe.createElement(_, { + id: 'auth_username', + type: 'text', + required: 'required', + name: 'username', + onChange: this.onChange, + autoFocus: !0 + }) + ) + ), + Pe.createElement( + w, + null, + Pe.createElement('label', { htmlFor: 'auth_password' }, 'Password:'), + B + ? Pe.createElement('code', null, ' ****** ') + : Pe.createElement( + x, + null, + Pe.createElement(_, { + id: 'auth_password', + autoComplete: 'new-password', + name: 'password', + type: 'password', + onChange: this.onChange + }) + ) + ), + $.valueSeq().map((s, o) => Pe.createElement(C, { error: s, key: o })) + ); + } + } + function example_Example(s) { + const { example: o, showValue: i, getComponent: u } = s, + _ = u('Markdown', !0), + w = u('HighlightCode', !0); + return o + ? Pe.createElement( + 'div', + { className: 'example' }, + o.get('description') + ? Pe.createElement( + 'section', + { className: 'example__section' }, + Pe.createElement( + 'div', + { className: 'example__section-header' }, + 'Example Description' + ), + Pe.createElement( + 'p', + null, + Pe.createElement(_, { source: o.get('description') }) + ) + ) + : null, + i && o.has('value') + ? Pe.createElement( + 'section', + { className: 'example__section' }, + Pe.createElement( + 'div', + { className: 'example__section-header' }, + 'Example Value' + ), + Pe.createElement(w, null, stringify(o.get('value'))) + ) + : null + ) + : null; + } + class ExamplesSelect extends Pe.PureComponent { + static defaultProps = { + examples: $e().Map({}), + onSelect: (...s) => + console.log('DEBUG: ExamplesSelect was not given an onSelect callback', ...s), + currentExampleKey: null, + showLabels: !0 + }; + _onSelect = (s, { isSyntheticChange: o = !1 } = {}) => { + 'function' == typeof this.props.onSelect && + this.props.onSelect(s, { isSyntheticChange: o }); + }; + _onDomSelect = (s) => { + if ('function' == typeof this.props.onSelect) { + const o = s.target.selectedOptions[0].getAttribute('value'); + this._onSelect(o, { isSyntheticChange: !1 }); + } + }; + getCurrentExample = () => { + const { examples: s, currentExampleKey: o } = this.props, + i = s.get(o), + u = s.keySeq().first(), + _ = s.get(u); + return i || _ || Map({}); + }; + componentDidMount() { + const { onSelect: s, examples: o } = this.props; + if ('function' == typeof s) { + const s = o.first(), + i = o.keyOf(s); + this._onSelect(i, { isSyntheticChange: !0 }); + } + } + UNSAFE_componentWillReceiveProps(s) { + const { currentExampleKey: o, examples: i } = s; + if (i !== this.props.examples && !i.has(o)) { + const s = i.first(), + o = i.keyOf(s); + this._onSelect(o, { isSyntheticChange: !0 }); + } + } + render() { + const { + examples: s, + currentExampleKey: o, + isValueModified: i, + isModifiedValueAvailable: u, + showLabels: _ + } = this.props; + return Pe.createElement( + 'div', + { className: 'examples-select' }, + _ + ? Pe.createElement( + 'span', + { className: 'examples-select__section-label' }, + 'Examples: ' + ) + : null, + Pe.createElement( + 'select', + { + className: 'examples-select-element', + onChange: this._onDomSelect, + value: u && i ? '__MODIFIED__VALUE__' : o || '' + }, + u + ? Pe.createElement('option', { value: '__MODIFIED__VALUE__' }, '[Modified value]') + : null, + s + .map((s, o) => + Pe.createElement('option', { key: o, value: o }, s.get('summary') || o) + ) + .valueSeq() + ) + ); + } + } + const stringifyUnlessList = (s) => (qe.List.isList(s) ? s : stringify(s)); + class ExamplesSelectValueRetainer extends Pe.PureComponent { + static defaultProps = { + userHasEditedBody: !1, + examples: (0, qe.Map)({}), + currentNamespace: '__DEFAULT__NAMESPACE__', + setRetainRequestBodyValueFlag: () => {}, + onSelect: (...s) => + console.log('ExamplesSelectValueRetainer: no `onSelect` function was provided', ...s), + updateValue: (...s) => + console.log( + 'ExamplesSelectValueRetainer: no `updateValue` function was provided', + ...s + ) + }; + constructor(s) { + super(s); + const o = this._getCurrentExampleValue(); + this.state = { + [s.currentNamespace]: (0, qe.Map)({ + lastUserEditedValue: this.props.currentUserInputValue, + lastDownstreamValue: o, + isModifiedValueSelected: + this.props.userHasEditedBody || this.props.currentUserInputValue !== o + }) + }; + } + componentWillUnmount() { + this.props.setRetainRequestBodyValueFlag(!1); + } + _getStateForCurrentNamespace = () => { + const { currentNamespace: s } = this.props; + return (this.state[s] || (0, qe.Map)()).toObject(); + }; + _setStateForCurrentNamespace = (s) => { + const { currentNamespace: o } = this.props; + return this._setStateForNamespace(o, s); + }; + _setStateForNamespace = (s, o) => { + const i = (this.state[s] || (0, qe.Map)()).mergeDeep(o); + return this.setState({ [s]: i }); + }; + _isCurrentUserInputSameAsExampleValue = () => { + const { currentUserInputValue: s } = this.props; + return this._getCurrentExampleValue() === s; + }; + _getValueForExample = (s, o) => { + const { examples: i } = o || this.props; + return stringifyUnlessList((i || (0, qe.Map)({})).getIn([s, 'value'])); + }; + _getCurrentExampleValue = (s) => { + const { currentKey: o } = s || this.props; + return this._getValueForExample(o, s || this.props); + }; + _onExamplesSelect = (s, { isSyntheticChange: o } = {}, ...i) => { + const { + onSelect: u, + updateValue: _, + currentUserInputValue: w, + userHasEditedBody: x + } = this.props, + { lastUserEditedValue: C } = this._getStateForCurrentNamespace(), + j = this._getValueForExample(s); + if ('__MODIFIED__VALUE__' === s) + return ( + _(stringifyUnlessList(C)), + this._setStateForCurrentNamespace({ isModifiedValueSelected: !0 }) + ); + ('function' == typeof u && u(s, { isSyntheticChange: o }, ...i), + this._setStateForCurrentNamespace({ + lastDownstreamValue: j, + isModifiedValueSelected: (o && x) || (!!w && w !== j) + }), + o || ('function' == typeof _ && _(stringifyUnlessList(j)))); + }; + UNSAFE_componentWillReceiveProps(s) { + const { currentUserInputValue: o, examples: i, onSelect: u, userHasEditedBody: _ } = s, + { lastUserEditedValue: w, lastDownstreamValue: x } = + this._getStateForCurrentNamespace(), + C = this._getValueForExample(s.currentKey, s), + j = i.filter((s) => s.get('value') === o || stringify(s.get('value')) === o); + if (j.size) { + let o; + ((o = j.has(s.currentKey) ? s.currentKey : j.keySeq().first()), + u(o, { isSyntheticChange: !0 })); + } else + o !== this.props.currentUserInputValue && + o !== w && + o !== x && + (this.props.setRetainRequestBodyValueFlag(!0), + this._setStateForNamespace(s.currentNamespace, { + lastUserEditedValue: s.currentUserInputValue, + isModifiedValueSelected: _ || o !== C + })); + } + render() { + const { + currentUserInputValue: s, + examples: o, + currentKey: i, + getComponent: u, + userHasEditedBody: _ + } = this.props, + { + lastDownstreamValue: w, + lastUserEditedValue: x, + isModifiedValueSelected: C + } = this._getStateForCurrentNamespace(), + j = u('ExamplesSelect'); + return Pe.createElement(j, { + examples: o, + currentExampleKey: i, + onSelect: this._onExamplesSelect, + isModifiedValueAvailable: !!x && x !== w, + isValueModified: (void 0 !== s && C && s !== this._getCurrentExampleValue()) || _ + }); + } + } + function oauth2_authorize_authorize({ + auth: s, + authActions: o, + errActions: i, + configs: u, + authConfigs: _ = {}, + currentServer: w + }) { + let { schema: x, scopes: C, name: j, clientId: L } = s, + B = x.get('flow'), + $ = []; + switch (B) { + case 'password': + return void o.authorizePassword(s); + case 'application': + case 'clientCredentials': + case 'client_credentials': + return void o.authorizeApplication(s); + case 'accessCode': + case 'authorizationCode': + case 'authorization_code': + $.push('response_type=code'); + break; + case 'implicit': + $.push('response_type=token'); + } + 'string' == typeof L && $.push('client_id=' + encodeURIComponent(L)); + let V = u.oauth2RedirectUrl; + if (void 0 === V) + return void i.newAuthErr({ + authId: j, + source: 'validation', + level: 'error', + message: + 'oauth2RedirectUrl configuration is not passed. Oauth2 authorization cannot be performed.' + }); + $.push('redirect_uri=' + encodeURIComponent(V)); + let U = []; + if ( + (Array.isArray(C) ? (U = C) : $e().List.isList(C) && (U = C.toArray()), U.length > 0) + ) { + let s = _.scopeSeparator || ' '; + $.push('scope=' + encodeURIComponent(U.join(s))); + } + let z = utils_btoa(new Date()); + if ( + ($.push('state=' + encodeURIComponent(z)), + void 0 !== _.realm && $.push('realm=' + encodeURIComponent(_.realm)), + ('authorizationCode' === B || 'authorization_code' === B || 'accessCode' === B) && + _.usePkceWithAuthorizationCodeGrant) + ) { + const o = (function generateCodeVerifier() { + return b64toB64UrlEncoded(St()(32).toString('base64')); + })(), + i = (function createCodeChallenge(s) { + return b64toB64UrlEncoded(kt()('sha256').update(s).digest('base64')); + })(o); + ($.push('code_challenge=' + i), + $.push('code_challenge_method=S256'), + (s.codeVerifier = o)); + } + let { additionalQueryStringParams: Y } = _; + for (let s in Y) void 0 !== Y[s] && $.push([s, Y[s]].map(encodeURIComponent).join('=')); + const Z = x.get('authorizationUrl'); + let ee; + ee = w ? Mt()(sanitizeUrl(Z), w, !0).toString() : sanitizeUrl(Z); + let ie, + ae = [ee, $.join('&')].join(-1 === Z.indexOf('?') ? '?' : '&'); + ((ie = + 'implicit' === B + ? o.preAuthorizeImplicit + : _.useBasicAuthenticationWithAccessCodeGrant + ? o.authorizeAccessCodeWithBasicAuthentication + : o.authorizeAccessCodeWithFormParams), + o.authPopup(ae, { + auth: s, + state: z, + redirectUrl: V, + callback: ie, + errCb: i.newAuthErr + })); + } + class Oauth2 extends Pe.Component { + constructor(s, o) { + super(s, o); + let { name: i, schema: u, authorized: _, authSelectors: w } = this.props, + x = _ && _.get(i), + C = w.getConfigs() || {}, + j = (x && x.get('username')) || '', + L = (x && x.get('clientId')) || C.clientId || '', + B = (x && x.get('clientSecret')) || C.clientSecret || '', + $ = (x && x.get('passwordType')) || 'basic', + V = (x && x.get('scopes')) || C.scopes || []; + ('string' == typeof V && (V = V.split(C.scopeSeparator || ' ')), + (this.state = { + appName: C.appName, + name: i, + schema: u, + scopes: V, + clientId: L, + clientSecret: B, + username: j, + password: '', + passwordType: $ + })); + } + close = (s) => { + s.preventDefault(); + let { authActions: o } = this.props; + o.showDefinitions(!1); + }; + authorize = () => { + let { + authActions: s, + errActions: o, + getConfigs: i, + authSelectors: u, + oas3Selectors: _ + } = this.props, + w = i(), + x = u.getConfigs(); + (o.clear({ authId: name, type: 'auth', source: 'auth' }), + oauth2_authorize_authorize({ + auth: this.state, + currentServer: _.serverEffectiveValue(_.selectedServer()), + authActions: s, + errActions: o, + configs: w, + authConfigs: x + })); + }; + onScopeChange = (s) => { + let { target: o } = s, + { checked: i } = o, + u = o.dataset.value; + if (i && -1 === this.state.scopes.indexOf(u)) { + let s = this.state.scopes.concat([u]); + this.setState({ scopes: s }); + } else + !i && + this.state.scopes.indexOf(u) > -1 && + this.setState({ scopes: this.state.scopes.filter((s) => s !== u) }); + }; + onInputChange = (s) => { + let { + target: { + dataset: { name: o }, + value: i + } + } = s, + u = { [o]: i }; + this.setState(u); + }; + selectScopes = (s) => { + s.target.dataset.all + ? this.setState({ + scopes: Array.from( + ( + this.props.schema.get('allowedScopes') || this.props.schema.get('scopes') + ).keys() + ) + }) + : this.setState({ scopes: [] }); + }; + logout = (s) => { + s.preventDefault(); + let { authActions: o, errActions: i, name: u } = this.props; + (i.clear({ authId: u, type: 'auth', source: 'auth' }), o.logoutWithPersistOption([u])); + }; + render() { + let { + schema: s, + getComponent: o, + authSelectors: i, + errSelectors: u, + name: _, + specSelectors: w + } = this.props; + const x = o('Input'), + C = o('Row'), + j = o('Col'), + L = o('Button'), + B = o('authError'), + $ = o('JumpToPath', !0), + V = o('Markdown', !0), + U = o('InitializedInput'), + { isOAS3: z } = w; + let Y = z() ? s.get('openIdConnectUrl') : null; + const Z = 'implicit', + ee = 'password', + ie = z() ? (Y ? 'authorization_code' : 'authorizationCode') : 'accessCode', + ae = z() ? (Y ? 'client_credentials' : 'clientCredentials') : 'application'; + let le = !!(i.getConfigs() || {}).usePkceWithAuthorizationCodeGrant, + ce = s.get('flow'), + pe = ce === ie && le ? ce + ' with PKCE' : ce, + de = s.get('allowedScopes') || s.get('scopes'), + fe = !!i.authorized().get(_), + ye = u.allErrors().filter((s) => s.get('authId') === _), + be = !ye.filter((s) => 'validation' === s.get('source')).size, + _e = s.get('description'); + return Pe.createElement( + 'div', + null, + Pe.createElement( + 'h4', + null, + _, + ' (OAuth2, ', + pe, + ') ', + Pe.createElement($, { path: ['securityDefinitions', _] }) + ), + this.state.appName + ? Pe.createElement('h5', null, 'Application: ', this.state.appName, ' ') + : null, + _e && Pe.createElement(V, { source: s.get('description') }), + fe && Pe.createElement('h6', null, 'Authorized'), + Y && + Pe.createElement( + 'p', + null, + 'OpenID Connect URL: ', + Pe.createElement('code', null, Y) + ), + (ce === Z || ce === ie) && + Pe.createElement( + 'p', + null, + 'Authorization URL: ', + Pe.createElement('code', null, s.get('authorizationUrl')) + ), + (ce === ee || ce === ie || ce === ae) && + Pe.createElement( + 'p', + null, + 'Token URL:', + Pe.createElement('code', null, ' ', s.get('tokenUrl')) + ), + Pe.createElement( + 'p', + { className: 'flow' }, + 'Flow: ', + Pe.createElement('code', null, pe) + ), + ce !== ee + ? null + : Pe.createElement( + C, + null, + Pe.createElement( + C, + null, + Pe.createElement('label', { htmlFor: 'oauth_username' }, 'username:'), + fe + ? Pe.createElement('code', null, ' ', this.state.username, ' ') + : Pe.createElement( + j, + { tablet: 10, desktop: 10 }, + Pe.createElement('input', { + id: 'oauth_username', + type: 'text', + 'data-name': 'username', + onChange: this.onInputChange, + autoFocus: !0 + }) + ) + ), + Pe.createElement( + C, + null, + Pe.createElement('label', { htmlFor: 'oauth_password' }, 'password:'), + fe + ? Pe.createElement('code', null, ' ****** ') + : Pe.createElement( + j, + { tablet: 10, desktop: 10 }, + Pe.createElement('input', { + id: 'oauth_password', + type: 'password', + 'data-name': 'password', + onChange: this.onInputChange + }) + ) + ), + Pe.createElement( + C, + null, + Pe.createElement( + 'label', + { htmlFor: 'password_type' }, + 'Client credentials location:' + ), + fe + ? Pe.createElement('code', null, ' ', this.state.passwordType, ' ') + : Pe.createElement( + j, + { tablet: 10, desktop: 10 }, + Pe.createElement( + 'select', + { + id: 'password_type', + 'data-name': 'passwordType', + onChange: this.onInputChange + }, + Pe.createElement( + 'option', + { value: 'basic' }, + 'Authorization header' + ), + Pe.createElement('option', { value: 'request-body' }, 'Request body') + ) + ) + ) + ), + (ce === ae || ce === Z || ce === ie || ce === ee) && + (!fe || (fe && this.state.clientId)) && + Pe.createElement( + C, + null, + Pe.createElement('label', { htmlFor: `client_id_${ce}` }, 'client_id:'), + fe + ? Pe.createElement('code', null, ' ****** ') + : Pe.createElement( + j, + { tablet: 10, desktop: 10 }, + Pe.createElement(U, { + id: `client_id_${ce}`, + type: 'text', + required: ce === ee, + initialValue: this.state.clientId, + 'data-name': 'clientId', + onChange: this.onInputChange + }) + ) + ), + (ce === ae || ce === ie || ce === ee) && + Pe.createElement( + C, + null, + Pe.createElement('label', { htmlFor: `client_secret_${ce}` }, 'client_secret:'), + fe + ? Pe.createElement('code', null, ' ****** ') + : Pe.createElement( + j, + { tablet: 10, desktop: 10 }, + Pe.createElement(U, { + id: `client_secret_${ce}`, + initialValue: this.state.clientSecret, + type: 'password', + 'data-name': 'clientSecret', + onChange: this.onInputChange + }) + ) + ), + !fe && de && de.size + ? Pe.createElement( + 'div', + { className: 'scopes' }, + Pe.createElement( + 'h2', + null, + 'Scopes:', + Pe.createElement( + 'a', + { onClick: this.selectScopes, 'data-all': !0 }, + 'select all' + ), + Pe.createElement('a', { onClick: this.selectScopes }, 'select none') + ), + de + .map((s, o) => + Pe.createElement( + C, + { key: o }, + Pe.createElement( + 'div', + { className: 'checkbox' }, + Pe.createElement(x, { + 'data-value': o, + id: `${o}-${ce}-checkbox-${this.state.name}`, + disabled: fe, + checked: this.state.scopes.includes(o), + type: 'checkbox', + onChange: this.onScopeChange + }), + Pe.createElement( + 'label', + { htmlFor: `${o}-${ce}-checkbox-${this.state.name}` }, + Pe.createElement('span', { className: 'item' }), + Pe.createElement( + 'div', + { className: 'text' }, + Pe.createElement('p', { className: 'name' }, o), + Pe.createElement('p', { className: 'description' }, s) + ) + ) + ) + ) + ) + .toArray() + ) + : null, + ye.valueSeq().map((s, o) => Pe.createElement(B, { error: s, key: o })), + Pe.createElement( + 'div', + { className: 'auth-btn-wrapper' }, + be && + (fe + ? Pe.createElement( + L, + { + className: 'btn modal-btn auth authorize', + onClick: this.logout, + 'aria-label': 'Remove authorization' + }, + 'Logout' + ) + : Pe.createElement( + L, + { + className: 'btn modal-btn auth authorize', + onClick: this.authorize, + 'aria-label': 'Apply given OAuth2 credentials' + }, + 'Authorize' + )), + Pe.createElement( + L, + { className: 'btn modal-btn auth btn-done', onClick: this.close }, + 'Close' + ) + ) + ); + } + } + class Clear extends Pe.Component { + onClick = () => { + let { specActions: s, path: o, method: i } = this.props; + (s.clearResponse(o, i), s.clearRequest(o, i)); + }; + render() { + return Pe.createElement( + 'button', + { className: 'btn btn-clear opblock-control__btn', onClick: this.onClick }, + 'Clear' + ); + } + } + const live_response_Headers = ({ headers: s }) => + Pe.createElement( + 'div', + null, + Pe.createElement('h5', null, 'Response headers'), + Pe.createElement('pre', { className: 'microlight' }, s) + ), + Duration = ({ duration: s }) => + Pe.createElement( + 'div', + null, + Pe.createElement('h5', null, 'Request duration'), + Pe.createElement('pre', { className: 'microlight' }, s, ' ms') + ); + class LiveResponse extends Pe.Component { + shouldComponentUpdate(s) { + return ( + this.props.response !== s.response || + this.props.path !== s.path || + this.props.method !== s.method || + this.props.displayRequestDuration !== s.displayRequestDuration + ); + } + render() { + const { + response: s, + getComponent: o, + getConfigs: i, + displayRequestDuration: u, + specSelectors: _, + path: w, + method: x + } = this.props, + { showMutatedRequest: C, requestSnippetsEnabled: j } = i(), + L = C ? _.mutatedRequestFor(w, x) : _.requestFor(w, x), + B = s.get('status'), + $ = L.get('url'), + V = s.get('headers').toJS(), + U = s.get('notDocumented'), + z = s.get('error'), + Y = s.get('text'), + Z = s.get('duration'), + ee = Object.keys(V), + ie = V['content-type'] || V['Content-Type'], + ae = o('responseBody'), + le = ee.map((s) => { + var o = Array.isArray(V[s]) ? V[s].join() : V[s]; + return Pe.createElement( + 'span', + { className: 'headerline', key: s }, + ' ', + s, + ': ', + o, + ' ' + ); + }), + ce = 0 !== le.length, + pe = o('Markdown', !0), + de = o('RequestSnippets', !0), + fe = o('curl', !0); + return Pe.createElement( + 'div', + null, + L && j ? Pe.createElement(de, { request: L }) : Pe.createElement(fe, { request: L }), + $ && + Pe.createElement( + 'div', + null, + Pe.createElement( + 'div', + { className: 'request-url' }, + Pe.createElement('h4', null, 'Request URL'), + Pe.createElement('pre', { className: 'microlight' }, $) + ) + ), + Pe.createElement('h4', null, 'Server response'), + Pe.createElement( + 'table', + { className: 'responses-table live-responses-table' }, + Pe.createElement( + 'thead', + null, + Pe.createElement( + 'tr', + { className: 'responses-header' }, + Pe.createElement('td', { className: 'col_header response-col_status' }, 'Code'), + Pe.createElement( + 'td', + { className: 'col_header response-col_description' }, + 'Details' + ) + ) + ), + Pe.createElement( + 'tbody', + null, + Pe.createElement( + 'tr', + { className: 'response' }, + Pe.createElement( + 'td', + { className: 'response-col_status' }, + B, + U + ? Pe.createElement( + 'div', + { className: 'response-undocumented' }, + Pe.createElement('i', null, ' Undocumented ') + ) + : null + ), + Pe.createElement( + 'td', + { className: 'response-col_description' }, + z + ? Pe.createElement(pe, { + source: `${'' !== s.get('name') ? `${s.get('name')}: ` : ''}${s.get('message')}` + }) + : null, + Y + ? Pe.createElement(ae, { + content: Y, + contentType: ie, + url: $, + headers: V, + getConfigs: i, + getComponent: o + }) + : null, + ce ? Pe.createElement(live_response_Headers, { headers: le }) : null, + u && Z ? Pe.createElement(Duration, { duration: Z }) : null + ) + ) + ) + ) + ); + } + } + class OnlineValidatorBadge extends Pe.Component { + constructor(s, o) { + super(s, o); + let { getConfigs: i } = s, + { validatorUrl: u } = i(); + this.state = { + url: this.getDefinitionUrl(), + validatorUrl: void 0 === u ? 'https://validator.swagger.io/validator' : u + }; + } + getDefinitionUrl = () => { + let { specSelectors: s } = this.props; + return new (Mt())(s.url(), at.location).toString(); + }; + UNSAFE_componentWillReceiveProps(s) { + let { getConfigs: o } = s, + { validatorUrl: i } = o(); + this.setState({ + url: this.getDefinitionUrl(), + validatorUrl: void 0 === i ? 'https://validator.swagger.io/validator' : i + }); + } + render() { + let { getConfigs: s } = this.props, + { spec: o } = s(), + i = sanitizeUrl(this.state.validatorUrl); + return 'object' == typeof o && Object.keys(o).length + ? null + : this.state.url && + requiresValidationURL(this.state.validatorUrl) && + requiresValidationURL(this.state.url) + ? Pe.createElement( + 'span', + { className: 'float-right' }, + Pe.createElement( + 'a', + { + target: '_blank', + rel: 'noopener noreferrer', + href: `${i}/debug?url=${encodeURIComponent(this.state.url)}` + }, + Pe.createElement(ValidatorImage, { + src: `${i}?url=${encodeURIComponent(this.state.url)}`, + alt: 'Online validator badge' + }) + ) + ) + : null; + } + } + class ValidatorImage extends Pe.Component { + constructor(s) { + (super(s), (this.state = { loaded: !1, error: !1 })); + } + componentDidMount() { + const s = new Image(); + ((s.onload = () => { + this.setState({ loaded: !0 }); + }), + (s.onerror = () => { + this.setState({ error: !0 }); + }), + (s.src = this.props.src)); + } + UNSAFE_componentWillReceiveProps(s) { + if (s.src !== this.props.src) { + const o = new Image(); + ((o.onload = () => { + this.setState({ loaded: !0 }); + }), + (o.onerror = () => { + this.setState({ error: !0 }); + }), + (o.src = s.src)); + } + } + render() { + return this.state.error + ? Pe.createElement('img', { alt: 'Error' }) + : this.state.loaded + ? Pe.createElement('img', { src: this.props.src, alt: this.props.alt }) + : null; + } + } + class Operations extends Pe.Component { + render() { + let { specSelectors: s } = this.props; + const o = s.taggedOperations(); + return 0 === o.size + ? Pe.createElement('h3', null, ' No operations defined in spec!') + : Pe.createElement( + 'div', + null, + o.map(this.renderOperationTag).toArray(), + o.size < 1 + ? Pe.createElement('h3', null, ' No operations defined in spec! ') + : null + ); + } + renderOperationTag = (s, o) => { + const { + specSelectors: i, + getComponent: u, + oas3Selectors: _, + layoutSelectors: w, + layoutActions: x, + getConfigs: C + } = this.props, + j = i.validOperationMethods(), + L = u('OperationContainer', !0), + B = u('OperationTag'), + $ = s.get('operations'); + return Pe.createElement( + B, + { + key: 'operation-' + o, + tagObj: s, + tag: o, + oas3Selectors: _, + layoutSelectors: w, + layoutActions: x, + getConfigs: C, + getComponent: u, + specUrl: i.url() + }, + Pe.createElement( + 'div', + { className: 'operation-tag-content' }, + $.map((s) => { + const i = s.get('path'), + u = s.get('method'), + _ = $e().List(['paths', i, u]); + return -1 === j.indexOf(u) + ? null + : Pe.createElement(L, { + key: `${i}-${u}`, + specPath: _, + op: s, + path: i, + method: u, + tag: o + }); + }).toArray() + ) + ); + }; + } + function isAbsoluteUrl(s) { + return s.match(/^(?:[a-z]+:)?\/\//i); + } + function buildBaseUrl(s, o) { + return s + ? isAbsoluteUrl(s) + ? (function addProtocol(s) { + return s.match(/^\/\//i) ? `${window.location.protocol}${s}` : s; + })(s) + : new URL(s, o).href + : o; + } + function safeBuildUrl(s, o, { selectedServer: i = '' } = {}) { + try { + return (function buildUrl(s, o, { selectedServer: i = '' } = {}) { + if (!s) return; + if (isAbsoluteUrl(s)) return s; + const u = buildBaseUrl(i, o); + return isAbsoluteUrl(u) ? new URL(s, u).href : new URL(s, window.location.href).href; + })(s, o, { selectedServer: i }); + } catch { + return; + } + } + class OperationTag extends Pe.Component { + static defaultProps = { tagObj: $e().fromJS({}), tag: '' }; + render() { + const { + tagObj: s, + tag: o, + children: i, + oas3Selectors: u, + layoutSelectors: _, + layoutActions: w, + getConfigs: x, + getComponent: C, + specUrl: j + } = this.props; + let { docExpansion: L, deepLinking: B } = x(); + const $ = C('Collapse'), + V = C('Markdown', !0), + U = C('DeepLink'), + z = C('Link'), + Y = C('ArrowUpIcon'), + Z = C('ArrowDownIcon'); + let ee, + ie = s.getIn(['tagDetails', 'description'], null), + ae = s.getIn(['tagDetails', 'externalDocs', 'description']), + le = s.getIn(['tagDetails', 'externalDocs', 'url']); + ee = + isFunc(u) && isFunc(u.selectedServer) + ? safeBuildUrl(le, j, { selectedServer: u.selectedServer() }) + : le; + let ce = ['operations-tag', o], + pe = _.isShown(ce, 'full' === L || 'list' === L); + return Pe.createElement( + 'div', + { className: pe ? 'opblock-tag-section is-open' : 'opblock-tag-section' }, + Pe.createElement( + 'h3', + { + onClick: () => w.show(ce, !pe), + className: ie ? 'opblock-tag' : 'opblock-tag no-desc', + id: ce.map((s) => escapeDeepLinkPath(s)).join('-'), + 'data-tag': o, + 'data-is-open': pe + }, + Pe.createElement(U, { + enabled: B, + isShown: pe, + path: createDeepLinkPath(o), + text: o + }), + ie + ? Pe.createElement('small', null, Pe.createElement(V, { source: ie })) + : Pe.createElement('small', null), + ee + ? Pe.createElement( + 'div', + { className: 'info__externaldocs' }, + Pe.createElement( + 'small', + null, + Pe.createElement( + z, + { + href: sanitizeUrl(ee), + onClick: (s) => s.stopPropagation(), + target: '_blank' + }, + ae || ee + ) + ) + ) + : null, + Pe.createElement( + 'button', + { + 'aria-expanded': pe, + className: 'expand-operation', + title: pe ? 'Collapse operation' : 'Expand operation', + onClick: () => w.show(ce, !pe) + }, + pe + ? Pe.createElement(Y, { className: 'arrow' }) + : Pe.createElement(Z, { className: 'arrow' }) + ) + ), + Pe.createElement($, { isOpened: pe }, i) + ); + } + } + class operation_Operation extends Pe.PureComponent { + static defaultProps = { + operation: null, + response: null, + request: null, + specPath: (0, qe.List)(), + summary: '' + }; + render() { + let { + specPath: s, + response: o, + request: i, + toggleShown: u, + onTryoutClick: _, + onResetClick: w, + onCancelClick: x, + onExecute: C, + fn: j, + getComponent: L, + getConfigs: B, + specActions: $, + specSelectors: V, + authActions: U, + authSelectors: z, + oas3Actions: Y, + oas3Selectors: Z + } = this.props, + ee = this.props.operation, + { + deprecated: ie, + isShown: ae, + path: le, + method: ce, + op: pe, + tag: de, + operationId: fe, + allowTryItOut: ye, + displayRequestDuration: be, + tryItOutEnabled: _e, + executeInProgress: we + } = ee.toJS(), + { description: Se, externalDocs: xe, schemes: Te } = pe; + const Re = xe + ? safeBuildUrl(xe.url, V.url(), { selectedServer: Z.selectedServer() }) + : ''; + let qe = ee.getIn(['op']), + ze = qe.get('responses'), + We = (function getList(s, o) { + if (!$e().Iterable.isIterable(s)) return $e().List(); + let i = s.getIn(Array.isArray(o) ? o : [o]); + return $e().List.isList(i) ? i : $e().List(); + })(qe, ['parameters']), + He = V.operationScheme(le, ce), + Ye = ['operations', de, fe], + Xe = getExtensions(qe); + const Qe = L('responses'), + et = L('parameters'), + tt = L('execute'), + rt = L('clear'), + nt = L('Collapse'), + st = L('Markdown', !0), + ot = L('schemes'), + it = L('OperationServers'), + at = L('OperationExt'), + lt = L('OperationSummary'), + ct = L('Link'), + { showExtensions: ut } = B(); + if (ze && o && o.size > 0) { + let s = !ze.get(String(o.get('status'))) && !ze.get('default'); + o = o.set('notDocumented', s); + } + let pt = [le, ce]; + const ht = V.validationErrors([le, ce]); + return Pe.createElement( + 'div', + { + className: ie + ? 'opblock opblock-deprecated' + : ae + ? `opblock opblock-${ce} is-open` + : `opblock opblock-${ce}`, + id: escapeDeepLinkPath(Ye.join('-')) + }, + Pe.createElement(lt, { + operationProps: ee, + isShown: ae, + toggleShown: u, + getComponent: L, + authActions: U, + authSelectors: z, + specPath: s + }), + Pe.createElement( + nt, + { isOpened: ae }, + Pe.createElement( + 'div', + { className: 'opblock-body' }, + (qe && qe.size) || null === qe + ? null + : Pe.createElement(rolling_load, { + height: '32px', + width: '32px', + className: 'opblock-loading-animation' + }), + ie && + Pe.createElement( + 'h4', + { className: 'opblock-title_normal' }, + ' Warning: Deprecated' + ), + Se && + Pe.createElement( + 'div', + { className: 'opblock-description-wrapper' }, + Pe.createElement( + 'div', + { className: 'opblock-description' }, + Pe.createElement(st, { source: Se }) + ) + ), + Re + ? Pe.createElement( + 'div', + { className: 'opblock-external-docs-wrapper' }, + Pe.createElement( + 'h4', + { className: 'opblock-title_normal' }, + 'Find more details' + ), + Pe.createElement( + 'div', + { className: 'opblock-external-docs' }, + xe.description && + Pe.createElement( + 'span', + { className: 'opblock-external-docs__description' }, + Pe.createElement(st, { source: xe.description }) + ), + Pe.createElement( + ct, + { + target: '_blank', + className: 'opblock-external-docs__link', + href: sanitizeUrl(Re) + }, + Re + ) + ) + ) + : null, + qe && qe.size + ? Pe.createElement(et, { + parameters: We, + specPath: s.push('parameters'), + operation: qe, + onChangeKey: pt, + onTryoutClick: _, + onResetClick: w, + onCancelClick: x, + tryItOutEnabled: _e, + allowTryItOut: ye, + fn: j, + getComponent: L, + specActions: $, + specSelectors: V, + pathMethod: [le, ce], + getConfigs: B, + oas3Actions: Y, + oas3Selectors: Z + }) + : null, + _e + ? Pe.createElement(it, { + getComponent: L, + path: le, + method: ce, + operationServers: qe.get('servers'), + pathServers: V.paths().getIn([le, 'servers']), + getSelectedServer: Z.selectedServer, + setSelectedServer: Y.setSelectedServer, + setServerVariableValue: Y.setServerVariableValue, + getServerVariable: Z.serverVariableValue, + getEffectiveServerValue: Z.serverEffectiveValue + }) + : null, + _e && ye && Te && Te.size + ? Pe.createElement( + 'div', + { className: 'opblock-schemes' }, + Pe.createElement(ot, { + schemes: Te, + path: le, + method: ce, + specActions: $, + currentScheme: He + }) + ) + : null, + !_e || !ye || ht.length <= 0 + ? null + : Pe.createElement( + 'div', + { className: 'validation-errors errors-wrapper' }, + 'Please correct the following validation errors and try again.', + Pe.createElement( + 'ul', + null, + ht.map((s, o) => Pe.createElement('li', { key: o }, ' ', s, ' ')) + ) + ), + Pe.createElement( + 'div', + { className: _e && o && ye ? 'btn-group' : 'execute-wrapper' }, + _e && ye + ? Pe.createElement(tt, { + operation: qe, + specActions: $, + specSelectors: V, + oas3Selectors: Z, + oas3Actions: Y, + path: le, + method: ce, + onExecute: C, + disabled: we + }) + : null, + _e && o && ye + ? Pe.createElement(rt, { specActions: $, path: le, method: ce }) + : null + ), + we + ? Pe.createElement( + 'div', + { className: 'loading-container' }, + Pe.createElement('div', { className: 'loading' }) + ) + : null, + ze + ? Pe.createElement(Qe, { + responses: ze, + request: i, + tryItOutResponse: o, + getComponent: L, + getConfigs: B, + specSelectors: V, + oas3Actions: Y, + oas3Selectors: Z, + specActions: $, + produces: V.producesOptionsFor([le, ce]), + producesValue: V.currentProducesFor([le, ce]), + specPath: s.push('responses'), + path: le, + method: ce, + displayRequestDuration: be, + fn: j + }) + : null, + ut && Xe.size ? Pe.createElement(at, { extensions: Xe, getComponent: L }) : null + ) + ) + ); + } + } + class OperationContainer extends Pe.PureComponent { + constructor(s, o) { + super(s, o); + const { tryItOutEnabled: i } = s.getConfigs(); + this.state = { tryItOutEnabled: i, executeInProgress: !1 }; + } + static defaultProps = { + showSummary: !0, + response: null, + allowTryItOut: !0, + displayOperationId: !1, + displayRequestDuration: !1 + }; + mapStateToProps(s, o) { + const { op: i, layoutSelectors: u, getConfigs: _ } = o, + { + docExpansion: w, + deepLinking: x, + displayOperationId: C, + displayRequestDuration: j, + supportedSubmitMethods: L + } = _(), + B = u.showSummary(), + $ = + i.getIn(['operation', '__originalOperationId']) || + i.getIn(['operation', 'operationId']) || + opId(i.get('operation'), o.path, o.method) || + i.get('id'), + V = ['operations', o.tag, $], + U = + L.indexOf(o.method) >= 0 && + (void 0 === o.allowTryItOut + ? o.specSelectors.allowTryItOutFor(o.path, o.method) + : o.allowTryItOut), + z = i.getIn(['operation', 'security']) || o.specSelectors.security(); + return { + operationId: $, + isDeepLinkingEnabled: x, + showSummary: B, + displayOperationId: C, + displayRequestDuration: j, + allowTryItOut: U, + security: z, + isAuthorized: o.authSelectors.isAuthorized(z), + isShown: u.isShown(V, 'full' === w), + jumpToKey: `paths.${o.path}.${o.method}`, + response: o.specSelectors.responseFor(o.path, o.method), + request: o.specSelectors.requestFor(o.path, o.method) + }; + } + componentDidMount() { + const { isShown: s } = this.props, + o = this.getResolvedSubtree(); + s && void 0 === o && this.requestResolvedSubtree(); + } + UNSAFE_componentWillReceiveProps(s) { + const { response: o, isShown: i } = s, + u = this.getResolvedSubtree(); + (o !== this.props.response && this.setState({ executeInProgress: !1 }), + i && void 0 === u && this.requestResolvedSubtree()); + } + toggleShown = () => { + let { layoutActions: s, tag: o, operationId: i, isShown: u } = this.props; + const _ = this.getResolvedSubtree(); + (u || void 0 !== _ || this.requestResolvedSubtree(), s.show(['operations', o, i], !u)); + }; + onCancelClick = () => { + this.setState({ tryItOutEnabled: !this.state.tryItOutEnabled }); + }; + onTryoutClick = () => { + this.setState({ tryItOutEnabled: !this.state.tryItOutEnabled }); + }; + onResetClick = (s) => { + const o = this.props.oas3Selectors.selectDefaultRequestBodyValue(...s); + this.props.oas3Actions.setRequestBodyValue({ value: o, pathMethod: s }); + }; + onExecute = () => { + this.setState({ executeInProgress: !0 }); + }; + getResolvedSubtree = () => { + const { specSelectors: s, path: o, method: i, specPath: u } = this.props; + return u ? s.specResolvedSubtree(u.toJS()) : s.specResolvedSubtree(['paths', o, i]); + }; + requestResolvedSubtree = () => { + const { specActions: s, path: o, method: i, specPath: u } = this.props; + return u + ? s.requestResolvedSubtree(u.toJS()) + : s.requestResolvedSubtree(['paths', o, i]); + }; + render() { + let { + op: s, + tag: o, + path: i, + method: u, + security: _, + isAuthorized: w, + operationId: x, + showSummary: C, + isShown: j, + jumpToKey: L, + allowTryItOut: B, + response: $, + request: V, + displayOperationId: U, + displayRequestDuration: z, + isDeepLinkingEnabled: Y, + specPath: Z, + specSelectors: ee, + specActions: ie, + getComponent: ae, + getConfigs: le, + layoutSelectors: ce, + layoutActions: pe, + authActions: de, + authSelectors: fe, + oas3Actions: ye, + oas3Selectors: be, + fn: _e + } = this.props; + const we = ae('operation'), + Se = this.getResolvedSubtree() || (0, qe.Map)(), + xe = (0, qe.fromJS)({ + op: Se, + tag: o, + path: i, + summary: s.getIn(['operation', 'summary']) || '', + deprecated: Se.get('deprecated') || s.getIn(['operation', 'deprecated']) || !1, + method: u, + security: _, + isAuthorized: w, + operationId: x, + originalOperationId: Se.getIn(['operation', '__originalOperationId']), + showSummary: C, + isShown: j, + jumpToKey: L, + allowTryItOut: B, + request: V, + displayOperationId: U, + displayRequestDuration: z, + isDeepLinkingEnabled: Y, + executeInProgress: this.state.executeInProgress, + tryItOutEnabled: this.state.tryItOutEnabled + }); + return Pe.createElement(we, { + operation: xe, + response: $, + request: V, + isShown: j, + toggleShown: this.toggleShown, + onTryoutClick: this.onTryoutClick, + onResetClick: this.onResetClick, + onCancelClick: this.onCancelClick, + onExecute: this.onExecute, + specPath: Z, + specActions: ie, + specSelectors: ee, + oas3Actions: ye, + oas3Selectors: be, + layoutActions: pe, + layoutSelectors: ce, + authActions: de, + authSelectors: fe, + getComponent: ae, + getConfigs: le, + fn: _e + }); + } + } + var mk = __webpack_require__(13222), + yk = __webpack_require__.n(mk); + class OperationSummary extends Pe.PureComponent { + static defaultProps = { operationProps: null, specPath: (0, qe.List)(), summary: '' }; + render() { + let { + isShown: s, + toggleShown: o, + getComponent: i, + authActions: u, + authSelectors: _, + operationProps: w, + specPath: x + } = this.props, + { + summary: C, + isAuthorized: j, + method: L, + op: B, + showSummary: $, + path: V, + operationId: U, + originalOperationId: z, + displayOperationId: Y + } = w.toJS(), + { summary: Z } = B, + ee = w.get('security'); + const ie = i('authorizeOperationBtn', !0), + ae = i('OperationSummaryMethod'), + le = i('OperationSummaryPath'), + ce = i('JumpToPath', !0), + pe = i('CopyToClipboardBtn', !0), + de = i('ArrowUpIcon'), + fe = i('ArrowDownIcon'), + ye = ee && !!ee.count(), + be = ye && 1 === ee.size && ee.first().isEmpty(), + _e = !ye || be; + return Pe.createElement( + 'div', + { className: `opblock-summary opblock-summary-${L}` }, + Pe.createElement( + 'button', + { 'aria-expanded': s, className: 'opblock-summary-control', onClick: o }, + Pe.createElement(ae, { method: L }), + Pe.createElement( + 'div', + { className: 'opblock-summary-path-description-wrapper' }, + Pe.createElement(le, { getComponent: i, operationProps: w, specPath: x }), + $ + ? Pe.createElement( + 'div', + { className: 'opblock-summary-description' }, + yk()(Z || C) + ) + : null + ), + Y && (z || U) + ? Pe.createElement('span', { className: 'opblock-summary-operation-id' }, z || U) + : null + ), + Pe.createElement(pe, { textToCopy: `${x.get(1)}` }), + _e + ? null + : Pe.createElement(ie, { + isAuthorized: j, + onClick: () => { + const s = _.definitionsForRequirements(ee); + u.showDefinitions(s); + } + }), + Pe.createElement(ce, { path: x }), + Pe.createElement( + 'button', + { + 'aria-label': `${L} ${V.replace(/\//g, '​/')}`, + className: 'opblock-control-arrow', + 'aria-expanded': s, + tabIndex: '-1', + onClick: o + }, + s + ? Pe.createElement(de, { className: 'arrow' }) + : Pe.createElement(fe, { className: 'arrow' }) + ) + ); + } + } + class OperationSummaryMethod extends Pe.PureComponent { + static defaultProps = { operationProps: null }; + render() { + let { method: s } = this.props; + return Pe.createElement( + 'span', + { className: 'opblock-summary-method' }, + s.toUpperCase() + ); + } + } + class OperationSummaryPath extends Pe.PureComponent { + render() { + let { getComponent: s, operationProps: o } = this.props, + { + deprecated: i, + isShown: u, + path: _, + tag: w, + operationId: x, + isDeepLinkingEnabled: C + } = o.toJS(); + const j = _.split(/(?=\/)/g); + for (let s = 1; s < j.length; s += 2) + j.splice(s, 0, Pe.createElement('wbr', { key: s })); + const L = s('DeepLink'); + return Pe.createElement( + 'span', + { + className: i ? 'opblock-summary-path__deprecated' : 'opblock-summary-path', + 'data-path': _ + }, + Pe.createElement(L, { + enabled: C, + isShown: u, + path: createDeepLinkPath(`${w}/${x}`), + text: j + }) + ); + } + } + const operation_extensions = ({ extensions: s, getComponent: o }) => { + let i = o('OperationExtRow'); + return Pe.createElement( + 'div', + { className: 'opblock-section' }, + Pe.createElement( + 'div', + { className: 'opblock-section-header' }, + Pe.createElement('h4', null, 'Extensions') + ), + Pe.createElement( + 'div', + { className: 'table-container' }, + Pe.createElement( + 'table', + null, + Pe.createElement( + 'thead', + null, + Pe.createElement( + 'tr', + null, + Pe.createElement('td', { className: 'col_header' }, 'Field'), + Pe.createElement('td', { className: 'col_header' }, 'Value') + ) + ), + Pe.createElement( + 'tbody', + null, + s + .entrySeq() + .map(([s, o]) => Pe.createElement(i, { key: `${s}-${o}`, xKey: s, xVal: o })) + ) + ) + ) + ); + }, + operation_extension_row = ({ xKey: s, xVal: o }) => { + const i = o ? (o.toJS ? o.toJS() : o) : null; + return Pe.createElement( + 'tr', + null, + Pe.createElement('td', null, s), + Pe.createElement('td', null, JSON.stringify(i)) + ); + }; + function createHtmlReadyId(s, o = '_') { + return s.replace(/[^\w-]/g, o); + } + class responses_Responses extends Pe.Component { + static defaultProps = { + tryItOutResponse: null, + produces: (0, qe.fromJS)(['application/json']), + displayRequestDuration: !1 + }; + onChangeProducesWrapper = (s) => + this.props.specActions.changeProducesValue([this.props.path, this.props.method], s); + onResponseContentTypeChange = ({ controlsAcceptHeader: s, value: o }) => { + const { oas3Actions: i, path: u, method: _ } = this.props; + s && i.setResponseContentType({ value: o, path: u, method: _ }); + }; + render() { + let { + responses: s, + tryItOutResponse: o, + getComponent: i, + getConfigs: u, + specSelectors: _, + fn: w, + producesValue: x, + displayRequestDuration: C, + specPath: j, + path: L, + method: B, + oas3Selectors: $, + oas3Actions: V + } = this.props, + U = (function defaultStatusCode(s) { + let o = s.keySeq(); + return o.contains(At) + ? At + : o + .filter((s) => '2' === (s + '')[0]) + .sort() + .first(); + })(s); + const z = i('contentType'), + Y = i('liveResponse'), + Z = i('response'); + let ee = + this.props.produces && this.props.produces.size + ? this.props.produces + : responses_Responses.defaultProps.produces; + const ie = _.isOAS3() + ? (function getAcceptControllingResponse(s) { + if (!$e().OrderedMap.isOrderedMap(s)) return null; + if (!s.size) return null; + const o = s.find( + (s, o) => + o.startsWith('2') && Object.keys(s.get('content') || {}).length > 0 + ), + i = s.get('default') || $e().OrderedMap(), + u = (i.get('content') || $e().OrderedMap()).keySeq().toJS().length ? i : null; + return o || u; + })(s) + : null, + ae = createHtmlReadyId(`${B}${L}_responses`), + le = `${ae}_select`; + return Pe.createElement( + 'div', + { className: 'responses-wrapper' }, + Pe.createElement( + 'div', + { className: 'opblock-section-header' }, + Pe.createElement('h4', null, 'Responses'), + _.isOAS3() + ? null + : Pe.createElement( + 'label', + { htmlFor: le }, + Pe.createElement('span', null, 'Response content type'), + Pe.createElement(z, { + value: x, + ariaControls: ae, + ariaLabel: 'Response content type', + className: 'execute-content-type', + contentTypes: ee, + controlId: le, + onChange: this.onChangeProducesWrapper + }) + ) + ), + Pe.createElement( + 'div', + { className: 'responses-inner' }, + o + ? Pe.createElement( + 'div', + null, + Pe.createElement(Y, { + response: o, + getComponent: i, + getConfigs: u, + specSelectors: _, + path: this.props.path, + method: this.props.method, + displayRequestDuration: C + }), + Pe.createElement('h4', null, 'Responses') + ) + : null, + Pe.createElement( + 'table', + { 'aria-live': 'polite', className: 'responses-table', id: ae, role: 'region' }, + Pe.createElement( + 'thead', + null, + Pe.createElement( + 'tr', + { className: 'responses-header' }, + Pe.createElement( + 'td', + { className: 'col_header response-col_status' }, + 'Code' + ), + Pe.createElement( + 'td', + { className: 'col_header response-col_description' }, + 'Description' + ), + _.isOAS3() + ? Pe.createElement( + 'td', + { className: 'col col_header response-col_links' }, + 'Links' + ) + : null + ) + ), + Pe.createElement( + 'tbody', + null, + s + .entrySeq() + .map(([s, C]) => { + let z = o && o.get('status') == s ? 'response_current' : ''; + return Pe.createElement(Z, { + key: s, + path: L, + method: B, + specPath: j.push(s), + isDefault: U === s, + fn: w, + className: z, + code: s, + response: C, + specSelectors: _, + controlsAcceptHeader: C === ie, + onContentTypeChange: this.onResponseContentTypeChange, + contentType: x, + getConfigs: u, + activeExamplesKey: $.activeExamplesMember(L, B, 'responses', s), + oas3Actions: V, + getComponent: i + }); + }) + .toArray() + ) + ) + ) + ); + } + } + function getKnownSyntaxHighlighterLanguage(s) { + const o = (function canJsonParse(s) { + try { + return !!JSON.parse(s); + } catch (s) { + return null; + } + })(s); + return o ? 'json' : null; + } + class response_Response extends Pe.Component { + constructor(s, o) { + (super(s, o), (this.state = { responseContentType: '' })); + } + static defaultProps = { response: (0, qe.fromJS)({}), onContentTypeChange: () => {} }; + _onContentTypeChange = (s) => { + const { onContentTypeChange: o, controlsAcceptHeader: i } = this.props; + (this.setState({ responseContentType: s }), o({ value: s, controlsAcceptHeader: i })); + }; + getTargetExamplesKey = () => { + const { response: s, contentType: o, activeExamplesKey: i } = this.props, + u = this.state.responseContentType || o, + _ = s + .getIn(['content', u], (0, qe.Map)({})) + .get('examples', null) + .keySeq() + .first(); + return i || _; + }; + render() { + let { + path: s, + method: o, + code: i, + response: u, + className: _, + specPath: w, + fn: x, + getComponent: C, + getConfigs: j, + specSelectors: L, + contentType: B, + controlsAcceptHeader: $, + oas3Actions: V + } = this.props, + { inferSchema: U, getSampleSchema: z } = x, + Y = L.isOAS3(); + const { showExtensions: Z } = j(); + let ee = Z ? getExtensions(u) : null, + ie = u.get('headers'), + ae = u.get('links'); + const le = C('ResponseExtension'), + ce = C('headers'), + pe = C('HighlightCode', !0), + de = C('modelExample'), + fe = C('Markdown', !0), + ye = C('operationLink'), + be = C('contentType'), + _e = C('ExamplesSelect'), + we = C('Example'); + var Se, xe; + const Te = this.state.responseContentType || B, + Re = u.getIn(['content', Te], (0, qe.Map)({})), + $e = Re.get('examples', null); + if (Y) { + const s = Re.get('schema'); + ((Se = s ? U(s.toJS()) : null), + (xe = s ? (0, qe.List)(['content', this.state.responseContentType, 'schema']) : w)); + } else ((Se = u.get('schema')), (xe = u.has('schema') ? w.push('schema') : w)); + let ze, + We, + He = !1, + Ye = { includeReadOnly: !0 }; + if (Y) + if (((We = Re.get('schema')?.toJS()), qe.Map.isMap($e) && !$e.isEmpty())) { + const s = this.getTargetExamplesKey(), + getMediaTypeExample = (s) => s.get('value'); + ((ze = getMediaTypeExample($e.get(s, (0, qe.Map)({})))), + void 0 === ze && (ze = getMediaTypeExample($e.values().next().value)), + (He = !0)); + } else void 0 !== Re.get('example') && ((ze = Re.get('example')), (He = !0)); + else { + ((We = Se), (Ye = { ...Ye, includeWriteOnly: !0 })); + const s = u.getIn(['examples', Te]); + s && ((ze = s), (He = !0)); + } + const Xe = ((s, o) => { + if (null == s) return null; + const i = getKnownSyntaxHighlighterLanguage(s) ? 'json' : null; + return Pe.createElement( + 'div', + null, + Pe.createElement(o, { className: 'example', language: i }, stringify(s)) + ); + })(z(We, Te, Ye, He ? ze : void 0), pe); + return Pe.createElement( + 'tr', + { className: 'response ' + (_ || ''), 'data-code': i }, + Pe.createElement('td', { className: 'response-col_status' }, i), + Pe.createElement( + 'td', + { className: 'response-col_description' }, + Pe.createElement( + 'div', + { className: 'response-col_description__inner' }, + Pe.createElement(fe, { source: u.get('description') }) + ), + Z && ee.size + ? ee + .entrySeq() + .map(([s, o]) => Pe.createElement(le, { key: `${s}-${o}`, xKey: s, xVal: o })) + : null, + Y && u.get('content') + ? Pe.createElement( + 'section', + { className: 'response-controls' }, + Pe.createElement( + 'div', + { + className: Hn()('response-control-media-type', { + 'response-control-media-type--accept-controller': $ + }) + }, + Pe.createElement( + 'small', + { className: 'response-control-media-type__title' }, + 'Media type' + ), + Pe.createElement(be, { + value: this.state.responseContentType, + contentTypes: u.get('content') + ? u.get('content').keySeq() + : (0, qe.Seq)(), + onChange: this._onContentTypeChange, + ariaLabel: 'Media Type' + }), + $ + ? Pe.createElement( + 'small', + { className: 'response-control-media-type__accept-message' }, + 'Controls ', + Pe.createElement('code', null, 'Accept'), + ' header.' + ) + : null + ), + qe.Map.isMap($e) && !$e.isEmpty() + ? Pe.createElement( + 'div', + { className: 'response-control-examples' }, + Pe.createElement( + 'small', + { className: 'response-control-examples__title' }, + 'Examples' + ), + Pe.createElement(_e, { + examples: $e, + currentExampleKey: this.getTargetExamplesKey(), + onSelect: (u) => + V.setActiveExamplesMember({ + name: u, + pathMethod: [s, o], + contextType: 'responses', + contextName: i + }), + showLabels: !1 + }) + ) + : null + ) + : null, + Xe || Se + ? Pe.createElement(de, { + specPath: xe, + getComponent: C, + getConfigs: j, + specSelectors: L, + schema: fromJSOrdered(Se), + example: Xe, + includeReadOnly: !0 + }) + : null, + Y && $e + ? Pe.createElement(we, { + example: $e.get(this.getTargetExamplesKey(), (0, qe.Map)({})), + getComponent: C, + getConfigs: j, + omitValue: !0 + }) + : null, + ie ? Pe.createElement(ce, { headers: ie, getComponent: C }) : null + ), + Y + ? Pe.createElement( + 'td', + { className: 'response-col_links' }, + ae + ? ae + .toSeq() + .entrySeq() + .map(([s, o]) => + Pe.createElement(ye, { key: s, name: s, link: o, getComponent: C }) + ) + : Pe.createElement('i', null, 'No links') + ) + : null + ); + } + } + const response_extension = ({ xKey: s, xVal: o }) => + Pe.createElement('div', { className: 'response__extension' }, s, ': ', String(o)); + var vk = __webpack_require__(26657), + _k = __webpack_require__.n(vk), + wk = __webpack_require__(80218), + xk = __webpack_require__.n(wk); + class ResponseBody extends Pe.PureComponent { + state = { parsedContent: null }; + updateParsedContent = (s) => { + const { content: o } = this.props; + if (s !== o) + if (o && o instanceof Blob) { + var i = new FileReader(); + ((i.onload = () => { + this.setState({ parsedContent: i.result }); + }), + i.readAsText(o)); + } else this.setState({ parsedContent: o.toString() }); + }; + componentDidMount() { + this.updateParsedContent(null); + } + componentDidUpdate(s) { + this.updateParsedContent(s.content); + } + render() { + let { + content: s, + contentType: o, + url: i, + headers: u = {}, + getComponent: _ + } = this.props; + const { parsedContent: w } = this.state, + x = _('HighlightCode', !0), + C = 'response_' + new Date().getTime(); + let j, L; + if ( + ((i = i || ''), + (/^application\/octet-stream/i.test(o) || + (u['Content-Disposition'] && /attachment/i.test(u['Content-Disposition'])) || + (u['content-disposition'] && /attachment/i.test(u['content-disposition'])) || + (u['Content-Description'] && /File Transfer/i.test(u['Content-Description'])) || + (u['content-description'] && /File Transfer/i.test(u['content-description']))) && + (s.size > 0 || s.length > 0)) + ) + if ('Blob' in window) { + let _ = o || 'text/html', + w = s instanceof Blob ? s : new Blob([s], { type: _ }), + x = window.URL.createObjectURL(w), + C = [_, i.substr(i.lastIndexOf('/') + 1), x].join(':'), + j = u['content-disposition'] || u['Content-Disposition']; + if (void 0 !== j) { + let s = (function extractFileNameFromContentDispositionHeader(s) { + let o; + if ( + ([ + /filename\*=[^']+'\w*'"([^"]+)";?/i, + /filename\*=[^']+'\w*'([^;]+);?/i, + /filename="([^;]*);?"/i, + /filename=([^;]*);?/i + ].some((i) => ((o = i.exec(s)), null !== o)), + null !== o && o.length > 1) + ) + try { + return decodeURIComponent(o[1]); + } catch (s) { + console.error(s); + } + return null; + })(j); + null !== s && (C = s); + } + L = + at.navigator && at.navigator.msSaveOrOpenBlob + ? Pe.createElement( + 'div', + null, + Pe.createElement( + 'a', + { href: x, onClick: () => at.navigator.msSaveOrOpenBlob(w, C) }, + 'Download file' + ) + ) + : Pe.createElement( + 'div', + null, + Pe.createElement('a', { href: x, download: C }, 'Download file') + ); + } else + L = Pe.createElement( + 'pre', + { className: 'microlight' }, + 'Download headers detected but your browser does not support downloading binary via XHR (Blob).' + ); + else if (/json/i.test(o)) { + let o = null; + getKnownSyntaxHighlighterLanguage(s) && (o = 'json'); + try { + j = JSON.stringify(JSON.parse(s), null, ' '); + } catch (o) { + j = "can't parse JSON. Raw result:\n\n" + s; + } + L = Pe.createElement( + x, + { language: o, downloadable: !0, fileName: `${C}.json`, canCopy: !0 }, + j + ); + } else + /xml/i.test(o) + ? ((j = _k()(s, { textNodesOnSameLine: !0, indentor: ' ' })), + (L = Pe.createElement( + x, + { downloadable: !0, fileName: `${C}.xml`, canCopy: !0 }, + j + ))) + : (L = + 'text/html' === xk()(o) || /text\/plain/.test(o) + ? Pe.createElement( + x, + { downloadable: !0, fileName: `${C}.html`, canCopy: !0 }, + s + ) + : 'text/csv' === xk()(o) || /text\/csv/.test(o) + ? Pe.createElement( + x, + { downloadable: !0, fileName: `${C}.csv`, canCopy: !0 }, + s + ) + : /^image\//i.test(o) + ? o.includes('svg') + ? Pe.createElement('div', null, ' ', s, ' ') + : Pe.createElement('img', { src: window.URL.createObjectURL(s) }) + : /^audio\//i.test(o) + ? Pe.createElement( + 'pre', + { className: 'microlight' }, + Pe.createElement( + 'audio', + { controls: !0, key: i }, + Pe.createElement('source', { src: i, type: o }) + ) + ) + : 'string' == typeof s + ? Pe.createElement( + x, + { downloadable: !0, fileName: `${C}.txt`, canCopy: !0 }, + s + ) + : s.size > 0 + ? w + ? Pe.createElement( + 'div', + null, + Pe.createElement( + 'p', + { className: 'i' }, + 'Unrecognized response type; displaying content as text.' + ), + Pe.createElement( + x, + { downloadable: !0, fileName: `${C}.txt`, canCopy: !0 }, + w + ) + ) + : Pe.createElement( + 'p', + { className: 'i' }, + 'Unrecognized response type; unable to display.' + ) + : null); + return L + ? Pe.createElement('div', null, Pe.createElement('h5', null, 'Response body'), L) + : null; + } + } + class Parameters extends Pe.Component { + constructor(s) { + (super(s), (this.state = { callbackVisible: !1, parametersVisible: !0 })); + } + static defaultProps = { + onTryoutClick: Function.prototype, + onCancelClick: Function.prototype, + tryItOutEnabled: !1, + allowTryItOut: !0, + onChangeKey: [], + specPath: [] + }; + onChange = (s, o, i) => { + let { + specActions: { changeParamByIdentity: u }, + onChangeKey: _ + } = this.props; + u(_, s, o, i); + }; + onChangeConsumesWrapper = (s) => { + let { + specActions: { changeConsumesValue: o }, + onChangeKey: i + } = this.props; + o(i, s); + }; + toggleTab = (s) => + 'parameters' === s + ? this.setState({ parametersVisible: !0, callbackVisible: !1 }) + : 'callbacks' === s + ? this.setState({ callbackVisible: !0, parametersVisible: !1 }) + : void 0; + onChangeMediaType = ({ value: s, pathMethod: o }) => { + let { specActions: i, oas3Selectors: u, oas3Actions: _ } = this.props; + const w = u.hasUserEditedBody(...o), + x = u.shouldRetainRequestBodyValue(...o); + (_.setRequestContentType({ value: s, pathMethod: o }), + _.initRequestBodyValidateError({ pathMethod: o }), + w || + (x || _.setRequestBodyValue({ value: void 0, pathMethod: o }), + i.clearResponse(...o), + i.clearRequest(...o), + i.clearValidateParams(o))); + }; + render() { + let { + onTryoutClick: s, + onResetClick: o, + parameters: i, + allowTryItOut: u, + tryItOutEnabled: _, + specPath: w, + fn: x, + getComponent: C, + getConfigs: j, + specSelectors: L, + specActions: B, + pathMethod: $, + oas3Actions: V, + oas3Selectors: U, + operation: z + } = this.props; + const Y = C('parameterRow'), + Z = C('TryItOutButton'), + ee = C('contentType'), + ie = C('Callbacks', !0), + ae = C('RequestBody', !0), + le = _ && u, + ce = L.isOAS3(), + pe = `${createHtmlReadyId(`${$[1]}${$[0]}_requests`)}_select`, + de = z.get('requestBody'), + fe = Object.values( + i.reduce((s, o) => { + const i = o.get('in'); + return ((s[i] ??= []), s[i].push(o), s); + }, {}) + ).reduce((s, o) => s.concat(o), []); + return Pe.createElement( + 'div', + { className: 'opblock-section' }, + Pe.createElement( + 'div', + { className: 'opblock-section-header' }, + ce + ? Pe.createElement( + 'div', + { className: 'tab-header' }, + Pe.createElement( + 'div', + { + onClick: () => this.toggleTab('parameters'), + className: `tab-item ${this.state.parametersVisible && 'active'}` + }, + Pe.createElement( + 'h4', + { className: 'opblock-title' }, + Pe.createElement('span', null, 'Parameters') + ) + ), + z.get('callbacks') + ? Pe.createElement( + 'div', + { + onClick: () => this.toggleTab('callbacks'), + className: `tab-item ${this.state.callbackVisible && 'active'}` + }, + Pe.createElement( + 'h4', + { className: 'opblock-title' }, + Pe.createElement('span', null, 'Callbacks') + ) + ) + : null + ) + : Pe.createElement( + 'div', + { className: 'tab-header' }, + Pe.createElement('h4', { className: 'opblock-title' }, 'Parameters') + ), + u + ? Pe.createElement(Z, { + isOAS3: L.isOAS3(), + hasUserEditedBody: U.hasUserEditedBody(...$), + enabled: _, + onCancelClick: this.props.onCancelClick, + onTryoutClick: s, + onResetClick: () => o($) + }) + : null + ), + this.state.parametersVisible + ? Pe.createElement( + 'div', + { className: 'parameters-container' }, + fe.length + ? Pe.createElement( + 'div', + { className: 'table-container' }, + Pe.createElement( + 'table', + { className: 'parameters' }, + Pe.createElement( + 'thead', + null, + Pe.createElement( + 'tr', + null, + Pe.createElement( + 'th', + { className: 'col_header parameters-col_name' }, + 'Name' + ), + Pe.createElement( + 'th', + { className: 'col_header parameters-col_description' }, + 'Description' + ) + ) + ), + Pe.createElement( + 'tbody', + null, + fe.map((s, o) => + Pe.createElement(Y, { + fn: x, + specPath: w.push(o.toString()), + getComponent: C, + getConfigs: j, + rawParam: s, + param: L.parameterWithMetaByIdentity($, s), + key: `${s.get('in')}.${s.get('name')}`, + onChange: this.onChange, + onChangeConsumes: this.onChangeConsumesWrapper, + specSelectors: L, + specActions: B, + oas3Actions: V, + oas3Selectors: U, + pathMethod: $, + isExecute: le + }) + ) + ) + ) + ) + : Pe.createElement( + 'div', + { className: 'opblock-description-wrapper' }, + Pe.createElement('p', null, 'No parameters') + ) + ) + : null, + this.state.callbackVisible + ? Pe.createElement( + 'div', + { className: 'callbacks-container opblock-description-wrapper' }, + Pe.createElement(ie, { + callbacks: (0, qe.Map)(z.get('callbacks')), + specPath: w.slice(0, -1).push('callbacks') + }) + ) + : null, + ce && + de && + this.state.parametersVisible && + Pe.createElement( + 'div', + { className: 'opblock-section opblock-section-request-body' }, + Pe.createElement( + 'div', + { className: 'opblock-section-header' }, + Pe.createElement( + 'h4', + { + className: `opblock-title parameter__name ${de.get('required') && 'required'}` + }, + 'Request body' + ), + Pe.createElement( + 'label', + { id: pe }, + Pe.createElement(ee, { + value: U.requestContentType(...$), + contentTypes: de.get('content', (0, qe.List)()).keySeq(), + onChange: (s) => { + this.onChangeMediaType({ value: s, pathMethod: $ }); + }, + className: 'body-param-content-type', + ariaLabel: 'Request content type', + controlId: pe + }) + ) + ), + Pe.createElement( + 'div', + { className: 'opblock-description-wrapper' }, + Pe.createElement(ae, { + setRetainRequestBodyValueFlag: (s) => + V.setRetainRequestBodyValueFlag({ value: s, pathMethod: $ }), + userHasEditedBody: U.hasUserEditedBody(...$), + specPath: w.slice(0, -1).push('requestBody'), + requestBody: de, + requestBodyValue: U.requestBodyValue(...$), + requestBodyInclusionSetting: U.requestBodyInclusionSetting(...$), + requestBodyErrors: U.requestBodyErrors(...$), + isExecute: le, + getConfigs: j, + activeExamplesKey: U.activeExamplesMember(...$, 'requestBody', 'requestBody'), + updateActiveExamplesKey: (s) => { + this.props.oas3Actions.setActiveExamplesMember({ + name: s, + pathMethod: this.props.pathMethod, + contextType: 'requestBody', + contextName: 'requestBody' + }); + }, + onChange: (s, o) => { + if (o) { + const i = U.requestBodyValue(...$), + u = qe.Map.isMap(i) ? i : (0, qe.Map)(); + return V.setRequestBodyValue({ pathMethod: $, value: u.setIn(o, s) }); + } + V.setRequestBodyValue({ value: s, pathMethod: $ }); + }, + onChangeIncludeEmpty: (s, o) => { + V.setRequestBodyInclusion({ pathMethod: $, value: o, name: s }); + }, + contentType: U.requestContentType(...$) + }) + ) + ) + ); + } + } + const parameter_extension = ({ xKey: s, xVal: o }) => + Pe.createElement('div', { className: 'parameter__extension' }, s, ': ', String(o)), + Ak = { onChange: () => {}, isIncludedOptions: {} }; + class ParameterIncludeEmpty extends Pe.Component { + static defaultProps = Ak; + componentDidMount() { + const { isIncludedOptions: s, onChange: o } = this.props, + { shouldDispatchInit: i, defaultValue: u } = s; + i && o(u); + } + onCheckboxChange = (s) => { + const { onChange: o } = this.props; + o(s.target.checked); + }; + render() { + let { isIncluded: s, isDisabled: o } = this.props; + return Pe.createElement( + 'div', + null, + Pe.createElement( + 'label', + { + htmlFor: 'include_empty_value', + className: Hn()('parameter__empty_value_toggle', { disabled: o }) + }, + Pe.createElement('input', { + id: 'include_empty_value', + type: 'checkbox', + disabled: o, + checked: !o && s, + onChange: this.onCheckboxChange + }), + 'Send empty value' + ) + ); + } + } + class ParameterRow extends Pe.Component { + constructor(s, o) { + (super(s, o), this.setDefaultValue()); + } + UNSAFE_componentWillReceiveProps(s) { + let o, + { specSelectors: i, pathMethod: u, rawParam: _ } = s, + w = i.isOAS3(), + x = i.parameterWithMetaByIdentity(u, _) || new qe.Map(); + if (((x = x.isEmpty() ? _ : x), w)) { + let { schema: s } = getParameterSchema(x, { isOAS3: w }); + o = s ? s.get('enum') : void 0; + } else o = x ? x.get('enum') : void 0; + let C, + j = x ? x.get('value') : void 0; + (void 0 !== j ? (C = j) : _.get('required') && o && o.size && (C = o.first()), + void 0 !== C && + C !== j && + this.onChangeWrapper( + (function numberToString(s) { + return 'number' == typeof s ? s.toString() : s; + })(C) + ), + this.setDefaultValue()); + } + onChangeWrapper = (s, o = !1) => { + let i, + { onChange: u, rawParam: _ } = this.props; + return ((i = '' === s || (s && 0 === s.size) ? null : s), u(_, i, o)); + }; + _onExampleSelect = (s) => { + this.props.oas3Actions.setActiveExamplesMember({ + name: s, + pathMethod: this.props.pathMethod, + contextType: 'parameters', + contextName: this.getParamKey() + }); + }; + onChangeIncludeEmpty = (s) => { + let { specActions: o, param: i, pathMethod: u } = this.props; + const _ = i.get('name'), + w = i.get('in'); + return o.updateEmptyParamInclusion(u, _, w, s); + }; + setDefaultValue = () => { + let { + specSelectors: s, + pathMethod: o, + rawParam: i, + oas3Selectors: u, + fn: _ + } = this.props; + const w = s.parameterWithMetaByIdentity(o, i) || (0, qe.Map)(); + let { schema: x } = getParameterSchema(w, { isOAS3: s.isOAS3() }); + const C = w + .get('content', (0, qe.Map)()) + .keySeq() + .first(), + j = x ? _.getSampleSchema(x.toJS(), C, { includeWriteOnly: !0 }) : null; + if (w && void 0 === w.get('value') && 'body' !== w.get('in')) { + let i; + if (s.isSwagger2()) + i = + void 0 !== w.get('x-example') + ? w.get('x-example') + : void 0 !== w.getIn(['schema', 'example']) + ? w.getIn(['schema', 'example']) + : x && x.getIn(['default']); + else if (s.isOAS3()) { + x = this.composeJsonSchema(x); + const s = u.activeExamplesMember(...o, 'parameters', this.getParamKey()); + i = + void 0 !== w.getIn(['examples', s, 'value']) + ? w.getIn(['examples', s, 'value']) + : void 0 !== w.getIn(['content', C, 'example']) + ? w.getIn(['content', C, 'example']) + : void 0 !== w.get('example') + ? w.get('example') + : void 0 !== (x && x.get('example')) + ? x && x.get('example') + : void 0 !== (x && x.get('default')) + ? x && x.get('default') + : w.get('default'); + } + (void 0 === i || qe.List.isList(i) || (i = stringify(i)), + void 0 !== i + ? this.onChangeWrapper(i) + : x && + 'object' === x.get('type') && + j && + !w.get('examples') && + this.onChangeWrapper(qe.List.isList(j) ? j : stringify(j))); + } + }; + getParamKey() { + const { param: s } = this.props; + return s ? `${s.get('name')}-${s.get('in')}` : null; + } + composeJsonSchema(s) { + const { fn: o } = this.props, + i = s.get('oneOf')?.get(0)?.toJS(), + u = s.get('anyOf')?.get(0)?.toJS(); + return (0, qe.fromJS)(o.mergeJsonSchema(s.toJS(), i ?? u ?? {})); + } + render() { + let { + param: s, + rawParam: o, + getComponent: i, + getConfigs: u, + isExecute: _, + fn: w, + onChangeConsumes: x, + specSelectors: C, + pathMethod: j, + specPath: L, + oas3Selectors: B + } = this.props, + $ = C.isOAS3(); + const { showExtensions: V, showCommonExtensions: U } = u(); + if ((s || (s = o), !o)) return null; + const z = i('JsonSchemaForm'), + Y = i('ParamBody'); + let Z = s.get('in'), + ee = + 'body' !== Z + ? null + : Pe.createElement(Y, { + getComponent: i, + getConfigs: u, + fn: w, + param: s, + consumes: C.consumesOptionsFor(j), + consumesValue: C.contentTypeValues(j).get('requestContentType'), + onChange: this.onChangeWrapper, + onChangeConsumes: x, + isExecute: _, + specSelectors: C, + pathMethod: j + }); + const ie = i('modelExample'), + ae = i('Markdown', !0), + le = i('ParameterExt'), + ce = i('ParameterIncludeEmpty'), + pe = i('ExamplesSelectValueRetainer'), + de = i('Example'); + let { schema: fe } = getParameterSchema(s, { isOAS3: $ }), + ye = C.parameterWithMetaByIdentity(j, o) || (0, qe.Map)(); + $ && (fe = this.composeJsonSchema(fe)); + let be, + _e, + we, + Se, + xe = fe ? fe.get('format') : null, + Te = fe ? fe.get('type') : null, + Re = fe ? fe.getIn(['items', 'type']) : null, + $e = 'formData' === Z, + ze = 'FormData' in at, + We = s.get('required'), + He = ye ? ye.get('value') : '', + Ye = U ? getCommonExtensions(fe) : null, + Xe = V ? getExtensions(s) : null, + Qe = !1; + return ( + void 0 !== s && fe && (be = fe.get('items')), + void 0 !== be + ? ((_e = be.get('enum')), (we = be.get('default'))) + : fe && (_e = fe.get('enum')), + _e && _e.size && _e.size > 0 && (Qe = !0), + void 0 !== s && + (fe && (we = fe.get('default')), + void 0 === we && (we = s.get('default')), + (Se = s.get('example')), + void 0 === Se && (Se = s.get('x-example'))), + Pe.createElement( + 'tr', + { 'data-param-name': s.get('name'), 'data-param-in': s.get('in') }, + Pe.createElement( + 'td', + { className: 'parameters-col_name' }, + Pe.createElement( + 'div', + { className: We ? 'parameter__name required' : 'parameter__name' }, + s.get('name'), + We ? Pe.createElement('span', null, ' *') : null + ), + Pe.createElement( + 'div', + { className: 'parameter__type' }, + Te, + Re && `[${Re}]`, + xe && Pe.createElement('span', { className: 'prop-format' }, '($', xe, ')') + ), + Pe.createElement( + 'div', + { className: 'parameter__deprecated' }, + $ && s.get('deprecated') ? 'deprecated' : null + ), + Pe.createElement('div', { className: 'parameter__in' }, '(', s.get('in'), ')') + ), + Pe.createElement( + 'td', + { className: 'parameters-col_description' }, + s.get('description') + ? Pe.createElement(ae, { source: s.get('description') }) + : null, + (!ee && _) || !Qe + ? null + : Pe.createElement(ae, { + className: 'parameter__enum', + source: + 'Available values : ' + + _e + .map(function (s) { + return s; + }) + .toArray() + .map(String) + .join(', ') + }), + (!ee && _) || void 0 === we + ? null + : Pe.createElement(ae, { + className: 'parameter__default', + source: 'Default value : ' + we + }), + (!ee && _) || void 0 === Se + ? null + : Pe.createElement(ae, { source: 'Example : ' + Se }), + $e && + !ze && + Pe.createElement('div', null, 'Error: your browser does not support FormData'), + $ && s.get('examples') + ? Pe.createElement( + 'section', + { className: 'parameter-controls' }, + Pe.createElement(pe, { + examples: s.get('examples'), + onSelect: this._onExampleSelect, + updateValue: this.onChangeWrapper, + getComponent: i, + defaultToFirstExample: !0, + currentKey: B.activeExamplesMember( + ...j, + 'parameters', + this.getParamKey() + ), + currentUserInputValue: He + }) + ) + : null, + ee + ? null + : Pe.createElement(z, { + fn: w, + getComponent: i, + value: He, + required: We, + disabled: !_, + description: s.get('name'), + onChange: this.onChangeWrapper, + errors: ye.get('errors'), + schema: fe + }), + ee && fe + ? Pe.createElement(ie, { + getComponent: i, + specPath: L.push('schema'), + getConfigs: u, + isExecute: _, + specSelectors: C, + schema: fe, + example: ee, + includeWriteOnly: !0 + }) + : null, + !ee && _ && s.get('allowEmptyValue') + ? Pe.createElement(ce, { + onChange: this.onChangeIncludeEmpty, + isIncluded: C.parameterInclusionSettingFor(j, s.get('name'), s.get('in')), + isDisabled: !isEmptyValue(He) + }) + : null, + $ && s.get('examples') + ? Pe.createElement(de, { + example: s.getIn([ + 'examples', + B.activeExamplesMember(...j, 'parameters', this.getParamKey()) + ]), + getComponent: i, + getConfigs: u + }) + : null, + U && Ye.size + ? Ye.entrySeq().map(([s, o]) => + Pe.createElement(le, { key: `${s}-${o}`, xKey: s, xVal: o }) + ) + : null, + V && Xe.size + ? Xe.entrySeq().map(([s, o]) => + Pe.createElement(le, { key: `${s}-${o}`, xKey: s, xVal: o }) + ) + : null + ) + ) + ); + } + } + class Execute extends Pe.Component { + handleValidateParameters = () => { + let { specSelectors: s, specActions: o, path: i, method: u } = this.props; + return (o.validateParams([i, u]), s.validateBeforeExecute([i, u])); + }; + handleValidateRequestBody = () => { + let { + path: s, + method: o, + specSelectors: i, + oas3Selectors: u, + oas3Actions: _ + } = this.props, + w = { missingBodyValue: !1, missingRequiredKeys: [] }; + _.clearRequestBodyValidateError({ path: s, method: o }); + let x = i.getOAS3RequiredRequestBodyContentType([s, o]), + C = u.requestBodyValue(s, o), + j = u.validateBeforeExecute([s, o]), + L = u.requestContentType(s, o); + if (!j) + return ( + (w.missingBodyValue = !0), + _.setRequestBodyValidateError({ path: s, method: o, validationErrors: w }), + !1 + ); + if (!x) return !0; + let B = u.validateShallowRequired({ + oas3RequiredRequestBodyContentType: x, + oas3RequestContentType: L, + oas3RequestBodyValue: C + }); + return ( + !B || + B.length < 1 || + (B.forEach((s) => { + w.missingRequiredKeys.push(s); + }), + _.setRequestBodyValidateError({ path: s, method: o, validationErrors: w }), + !1) + ); + }; + handleValidationResultPass = () => { + let { specActions: s, operation: o, path: i, method: u } = this.props; + (this.props.onExecute && this.props.onExecute(), + s.execute({ operation: o, path: i, method: u })); + }; + handleValidationResultFail = () => { + let { specActions: s, path: o, method: i } = this.props; + (s.clearValidateParams([o, i]), + setTimeout(() => { + s.validateParams([o, i]); + }, 40)); + }; + handleValidationResult = (s) => { + s ? this.handleValidationResultPass() : this.handleValidationResultFail(); + }; + onClick = () => { + let s = this.handleValidateParameters(), + o = this.handleValidateRequestBody(), + i = s && o; + this.handleValidationResult(i); + }; + onChangeProducesWrapper = (s) => + this.props.specActions.changeProducesValue([this.props.path, this.props.method], s); + render() { + const { disabled: s } = this.props; + return Pe.createElement( + 'button', + { className: 'btn execute opblock-control__btn', onClick: this.onClick, disabled: s }, + 'Execute' + ); + } + } + class headers_Headers extends Pe.Component { + render() { + let { headers: s, getComponent: o } = this.props; + const i = o('Property'), + u = o('Markdown', !0); + return s && s.size + ? Pe.createElement( + 'div', + { className: 'headers-wrapper' }, + Pe.createElement('h4', { className: 'headers__title' }, 'Headers:'), + Pe.createElement( + 'table', + { className: 'headers' }, + Pe.createElement( + 'thead', + null, + Pe.createElement( + 'tr', + { className: 'header-row' }, + Pe.createElement('th', { className: 'header-col' }, 'Name'), + Pe.createElement('th', { className: 'header-col' }, 'Description'), + Pe.createElement('th', { className: 'header-col' }, 'Type') + ) + ), + Pe.createElement( + 'tbody', + null, + s + .entrySeq() + .map(([s, o]) => { + if (!$e().Map.isMap(o)) return null; + const _ = o.get('description'), + w = o.getIn(['schema']) + ? o.getIn(['schema', 'type']) + : o.getIn(['type']), + x = o.getIn(['schema', 'example']); + return Pe.createElement( + 'tr', + { key: s }, + Pe.createElement('td', { className: 'header-col' }, s), + Pe.createElement( + 'td', + { className: 'header-col' }, + _ ? Pe.createElement(u, { source: _ }) : null + ), + Pe.createElement( + 'td', + { className: 'header-col' }, + w, + ' ', + x + ? Pe.createElement(i, { + propKey: 'Example', + propVal: x, + propClass: 'header-example' + }) + : null + ) + ); + }) + .toArray() + ) + ) + ) + : null; + } + } + class Errors extends Pe.Component { + render() { + let { + editorActions: s, + errSelectors: o, + layoutSelectors: i, + layoutActions: u, + getComponent: _ + } = this.props; + const w = _('Collapse'); + if (s && s.jumpToLine) var x = s.jumpToLine; + let C = o + .allErrors() + .filter((s) => 'thrown' === s.get('type') || 'error' === s.get('level')); + if (!C || C.count() < 1) return null; + let j = i.isShown(['errorPane'], !0), + L = C.sortBy((s) => s.get('line')); + return Pe.createElement( + 'pre', + { className: 'errors-wrapper' }, + Pe.createElement( + 'hgroup', + { className: 'error' }, + Pe.createElement('h4', { className: 'errors__title' }, 'Errors'), + Pe.createElement( + 'button', + { className: 'btn errors__clear-btn', onClick: () => u.show(['errorPane'], !j) }, + j ? 'Hide' : 'Show' + ) + ), + Pe.createElement( + w, + { isOpened: j, animated: !0 }, + Pe.createElement( + 'div', + { className: 'errors' }, + L.map((s, o) => { + let i = s.get('type'); + return 'thrown' === i || 'auth' === i + ? Pe.createElement(ThrownErrorItem, { + key: o, + error: s.get('error') || s, + jumpToLine: x + }) + : 'spec' === i + ? Pe.createElement(SpecErrorItem, { key: o, error: s, jumpToLine: x }) + : void 0; + }) + ) + ) + ); + } + } + const ThrownErrorItem = ({ error: s, jumpToLine: o }) => { + if (!s) return null; + let i = s.get('line'); + return Pe.createElement( + 'div', + { className: 'error-wrapper' }, + s + ? Pe.createElement( + 'div', + null, + Pe.createElement( + 'h4', + null, + s.get('source') && s.get('level') + ? toTitleCase(s.get('source')) + ' ' + s.get('level') + : '', + s.get('path') ? Pe.createElement('small', null, ' at ', s.get('path')) : null + ), + Pe.createElement('span', { className: 'message thrown' }, s.get('message')), + Pe.createElement( + 'div', + { className: 'error-line' }, + i && o + ? Pe.createElement('a', { onClick: o.bind(null, i) }, 'Jump to line ', i) + : null + ) + ) + : null + ); + }, + SpecErrorItem = ({ error: s, jumpToLine: o = null }) => { + let i = null; + return ( + s.get('path') + ? (i = qe.List.isList(s.get('path')) + ? Pe.createElement('small', null, 'at ', s.get('path').join('.')) + : Pe.createElement('small', null, 'at ', s.get('path'))) + : s.get('line') && + !o && + (i = Pe.createElement('small', null, 'on line ', s.get('line'))), + Pe.createElement( + 'div', + { className: 'error-wrapper' }, + s + ? Pe.createElement( + 'div', + null, + Pe.createElement( + 'h4', + null, + toTitleCase(s.get('source')) + ' ' + s.get('level'), + ' ', + i + ), + Pe.createElement('span', { className: 'message' }, s.get('message')), + Pe.createElement( + 'div', + { className: 'error-line' }, + o + ? Pe.createElement( + 'a', + { onClick: o.bind(null, s.get('line')) }, + 'Jump to line ', + s.get('line') + ) + : null + ) + ) + : null + ) + ); + }; + function toTitleCase(s) { + return (s || '') + .split(' ') + .map((s) => s[0].toUpperCase() + s.slice(1)) + .join(' '); + } + const content_type_noop = () => {}; + class ContentType extends Pe.Component { + static defaultProps = { + onChange: content_type_noop, + value: null, + contentTypes: (0, qe.fromJS)(['application/json']) + }; + componentDidMount() { + this.props.contentTypes && this.props.onChange(this.props.contentTypes.first()); + } + UNSAFE_componentWillReceiveProps(s) { + s.contentTypes && + s.contentTypes.size && + (s.contentTypes.includes(s.value) || s.onChange(s.contentTypes.first())); + } + onChangeWrapper = (s) => this.props.onChange(s.target.value); + render() { + let { + ariaControls: s, + ariaLabel: o, + className: i, + contentTypes: u, + controlId: _, + value: w + } = this.props; + return u && u.size + ? Pe.createElement( + 'div', + { className: 'content-type-wrapper ' + (i || '') }, + Pe.createElement( + 'select', + { + 'aria-controls': s, + 'aria-label': o, + className: 'content-type', + id: _, + onChange: this.onChangeWrapper, + value: w || '' + }, + u.map((s) => Pe.createElement('option', { key: s, value: s }, s)).toArray() + ) + ) + : null; + } + } + function xclass(...s) { + return s + .filter((s) => !!s) + .join(' ') + .trim(); + } + class Container extends Pe.Component { + render() { + let { fullscreen: s, full: o, ...i } = this.props; + if (s) return Pe.createElement('section', i); + let u = 'swagger-container' + (o ? '-full' : ''); + return Pe.createElement('section', Rn()({}, i, { className: xclass(i.className, u) })); + } + } + const Bk = { mobile: '', tablet: '-tablet', desktop: '-desktop', large: '-hd' }; + class Col extends Pe.Component { + render() { + const { + hide: s, + keepContents: o, + mobile: i, + tablet: u, + desktop: _, + large: w, + ...x + } = this.props; + if (s && !o) return Pe.createElement('span', null); + let C = []; + for (let s in Bk) { + if (!Object.prototype.hasOwnProperty.call(Bk, s)) continue; + let o = Bk[s]; + if (s in this.props) { + let i = this.props[s]; + if (i < 1) { + C.push('none' + o); + continue; + } + (C.push('block' + o), C.push('col-' + i + o)); + } + } + s && C.push('hidden'); + let j = xclass(x.className, ...C); + return Pe.createElement('section', Rn()({}, x, { className: j })); + } + } + class Row extends Pe.Component { + render() { + return Pe.createElement( + 'div', + Rn()({}, this.props, { className: xclass(this.props.className, 'wrapper') }) + ); + } + } + class Button extends Pe.Component { + static defaultProps = { className: '' }; + render() { + return Pe.createElement( + 'button', + Rn()({}, this.props, { className: xclass(this.props.className, 'button') }) + ); + } + } + const TextArea = (s) => Pe.createElement('textarea', s), + Input = (s) => Pe.createElement('input', s); + class Select extends Pe.Component { + static defaultProps = { multiple: !1, allowEmptyValue: !0 }; + constructor(s, o) { + let i; + (super(s, o), + (i = s.value ? s.value : s.multiple ? [''] : ''), + (this.state = { value: i })); + } + onChange = (s) => { + let o, + { onChange: i, multiple: u } = this.props, + _ = [].slice.call(s.target.options); + ((o = u + ? _.filter(function (s) { + return s.selected; + }).map(function (s) { + return s.value; + }) + : s.target.value), + this.setState({ value: o }), + i && i(o)); + }; + UNSAFE_componentWillReceiveProps(s) { + s.value !== this.props.value && this.setState({ value: s.value }); + } + render() { + let { allowedValues: s, multiple: o, allowEmptyValue: i, disabled: u } = this.props, + _ = this.state.value?.toJS?.() || this.state.value; + return Pe.createElement( + 'select', + { + className: this.props.className, + multiple: o, + value: _, + onChange: this.onChange, + disabled: u + }, + i ? Pe.createElement('option', { value: '' }, '--') : null, + s.map(function (s, o) { + return Pe.createElement('option', { key: o, value: String(s) }, String(s)); + }) + ); + } + } + class layout_utils_Link extends Pe.Component { + render() { + return Pe.createElement( + 'a', + Rn()({}, this.props, { + rel: 'noopener noreferrer', + className: xclass(this.props.className, 'link') + }) + ); + } + } + const NoMargin = ({ children: s }) => + Pe.createElement('div', { className: 'no-margin' }, ' ', s, ' '); + class Collapse extends Pe.Component { + static defaultProps = { isOpened: !1, animated: !1 }; + renderNotAnimated() { + return this.props.isOpened + ? Pe.createElement(NoMargin, null, this.props.children) + : Pe.createElement('noscript', null); + } + render() { + let { animated: s, isOpened: o, children: i } = this.props; + return s + ? ((i = o ? i : null), Pe.createElement(NoMargin, null, i)) + : this.renderNotAnimated(); + } + } + class Overview extends Pe.Component { + constructor(...s) { + (super(...s), (this.setTagShown = this._setTagShown.bind(this))); + } + _setTagShown(s, o) { + this.props.layoutActions.show(s, o); + } + showOp(s, o) { + let { layoutActions: i } = this.props; + i.show(s, o); + } + render() { + let { + specSelectors: s, + layoutSelectors: o, + layoutActions: i, + getComponent: u + } = this.props, + _ = s.taggedOperations(); + const w = u('Collapse'); + return Pe.createElement( + 'div', + null, + Pe.createElement('h4', { className: 'overview-title' }, 'Overview'), + _.map((s, u) => { + let _ = s.get('operations'), + x = ['overview-tags', u], + C = o.isShown(x, !0); + return Pe.createElement( + 'div', + { key: 'overview-' + u }, + Pe.createElement( + 'h4', + { onClick: () => i.show(x, !C), className: 'link overview-tag' }, + ' ', + C ? '-' : '+', + u + ), + Pe.createElement( + w, + { isOpened: C, animated: !0 }, + _.map((s) => { + let { path: u, method: _, id: w } = s.toObject(), + x = 'operations', + C = w, + j = o.isShown([x, C]); + return Pe.createElement(OperationLink, { + key: w, + path: u, + method: _, + id: u + '-' + _, + shown: j, + showOpId: C, + showOpIdPrefix: x, + href: `#operation-${C}`, + onClick: i.show + }); + }).toArray() + ) + ); + }).toArray(), + _.size < 1 && Pe.createElement('h3', null, ' No operations defined in spec! ') + ); + } + } + class OperationLink extends Pe.Component { + constructor(s) { + (super(s), (this.onClick = this._onClick.bind(this))); + } + _onClick() { + let { showOpId: s, showOpIdPrefix: o, onClick: i, shown: u } = this.props; + i([o, s], !u); + } + render() { + let { id: s, method: o, shown: i, href: u } = this.props; + return Pe.createElement( + layout_utils_Link, + { + href: u, + onClick: this.onClick, + className: 'block opblock-link ' + (i ? 'shown' : '') + }, + Pe.createElement( + 'div', + null, + Pe.createElement('small', { className: `bold-label-${o}` }, o.toUpperCase()), + Pe.createElement('span', { className: 'bold-label' }, s) + ) + ); + } + } + class InitializedInput extends Pe.Component { + componentDidMount() { + this.props.initialValue && (this.inputRef.value = this.props.initialValue); + } + render() { + const { value: s, defaultValue: o, initialValue: i, ...u } = this.props; + return Pe.createElement('input', Rn()({}, u, { ref: (s) => (this.inputRef = s) })); + } + } + class InfoBasePath extends Pe.Component { + render() { + const { host: s, basePath: o } = this.props; + return Pe.createElement('pre', { className: 'base-url' }, '[ Base URL: ', s, o, ' ]'); + } + } + class InfoUrl extends Pe.PureComponent { + render() { + const { url: s, getComponent: o } = this.props, + i = o('Link'); + return Pe.createElement( + i, + { target: '_blank', href: sanitizeUrl(s) }, + Pe.createElement('span', { className: 'url' }, ' ', s) + ); + } + } + class info_Info extends Pe.Component { + render() { + const { + info: s, + url: o, + host: i, + basePath: u, + getComponent: _, + externalDocs: w, + selectedServer: x, + url: C + } = this.props, + j = s.get('version'), + L = s.get('description'), + B = s.get('title'), + $ = safeBuildUrl(s.get('termsOfService'), C, { selectedServer: x }), + V = s.get('contact'), + U = s.get('license'), + z = safeBuildUrl(w && w.get('url'), C, { selectedServer: x }), + Y = w && w.get('description'), + Z = _('Markdown', !0), + ee = _('Link'), + ie = _('VersionStamp'), + ae = _('OpenAPIVersion'), + le = _('InfoUrl'), + ce = _('InfoBasePath'), + pe = _('License'), + de = _('Contact'); + return Pe.createElement( + 'div', + { className: 'info' }, + Pe.createElement( + 'hgroup', + { className: 'main' }, + Pe.createElement( + 'h2', + { className: 'title' }, + B, + Pe.createElement( + 'span', + null, + j && Pe.createElement(ie, { version: j }), + Pe.createElement(ae, { oasVersion: '2.0' }) + ) + ), + i || u ? Pe.createElement(ce, { host: i, basePath: u }) : null, + o && Pe.createElement(le, { getComponent: _, url: o }) + ), + Pe.createElement( + 'div', + { className: 'description' }, + Pe.createElement(Z, { source: L }) + ), + $ && + Pe.createElement( + 'div', + { className: 'info__tos' }, + Pe.createElement( + ee, + { target: '_blank', href: sanitizeUrl($) }, + 'Terms of service' + ) + ), + V?.size > 0 && + Pe.createElement(de, { getComponent: _, data: V, selectedServer: x, url: o }), + U?.size > 0 && + Pe.createElement(pe, { getComponent: _, license: U, selectedServer: x, url: o }), + z + ? Pe.createElement( + ee, + { className: 'info__extdocs', target: '_blank', href: sanitizeUrl(z) }, + Y || z + ) + : null + ); + } + } + const qk = info_Info; + class InfoContainer extends Pe.Component { + render() { + const { specSelectors: s, getComponent: o, oas3Selectors: i } = this.props, + u = s.info(), + _ = s.url(), + w = s.basePath(), + x = s.host(), + C = s.externalDocs(), + j = i.selectedServer(), + L = o('info'); + return Pe.createElement( + 'div', + null, + u && u.count() + ? Pe.createElement(L, { + info: u, + url: _, + host: x, + basePath: w, + externalDocs: C, + getComponent: o, + selectedServer: j + }) + : null + ); + } + } + class contact_Contact extends Pe.Component { + render() { + const { data: s, getComponent: o, selectedServer: i, url: u } = this.props, + _ = s.get('name', 'the developer'), + w = safeBuildUrl(s.get('url'), u, { selectedServer: i }), + x = s.get('email'), + C = o('Link'); + return Pe.createElement( + 'div', + { className: 'info__contact' }, + w && + Pe.createElement( + 'div', + null, + Pe.createElement(C, { href: sanitizeUrl(w), target: '_blank' }, _, ' - Website') + ), + x && + Pe.createElement( + C, + { href: sanitizeUrl(`mailto:${x}`) }, + w ? `Send email to ${_}` : `Contact ${_}` + ) + ); + } + } + const Vk = contact_Contact; + class license_License extends Pe.Component { + render() { + const { license: s, getComponent: o, selectedServer: i, url: u } = this.props, + _ = s.get('name', 'License'), + w = safeBuildUrl(s.get('url'), u, { selectedServer: i }), + x = o('Link'); + return Pe.createElement( + 'div', + { className: 'info__license' }, + w + ? Pe.createElement( + 'div', + { className: 'info__license__url' }, + Pe.createElement(x, { target: '_blank', href: sanitizeUrl(w) }, _) + ) + : Pe.createElement('span', null, _) + ); + } + } + const zk = license_License; + class JumpToPath extends Pe.Component { + render() { + return null; + } + } + class CopyToClipboardBtn extends Pe.Component { + render() { + let { getComponent: s } = this.props; + const o = s('CopyIcon'); + return Pe.createElement( + 'div', + { className: 'view-line-link copy-to-clipboard', title: 'Copy to clipboard' }, + Pe.createElement( + Jn.CopyToClipboard, + { text: this.props.textToCopy }, + Pe.createElement(o, null) + ) + ); + } + } + class Footer extends Pe.Component { + render() { + return Pe.createElement('div', { className: 'footer' }); + } + } + class FilterContainer extends Pe.Component { + onFilterChange = (s) => { + const { + target: { value: o } + } = s; + this.props.layoutActions.updateFilter(o); + }; + render() { + const { specSelectors: s, layoutSelectors: o, getComponent: i } = this.props, + u = i('Col'), + _ = 'loading' === s.loadingStatus(), + w = 'failed' === s.loadingStatus(), + x = o.currentFilter(), + C = ['operation-filter-input']; + return ( + w && C.push('failed'), + _ && C.push('loading'), + Pe.createElement( + 'div', + null, + !1 === x + ? null + : Pe.createElement( + 'div', + { className: 'filter-container' }, + Pe.createElement( + u, + { className: 'filter wrapper', mobile: 12 }, + Pe.createElement('input', { + className: C.join(' '), + placeholder: 'Filter by tag', + type: 'text', + onChange: this.onFilterChange, + value: 'string' == typeof x ? x : '', + disabled: _ + }) + ) + ) + ) + ); + } + } + const eC = Function.prototype; + class ParamBody extends Pe.PureComponent { + static defaultProp = { + consumes: (0, qe.fromJS)(['application/json']), + param: (0, qe.fromJS)({}), + onChange: eC, + onChangeConsumes: eC + }; + constructor(s, o) { + (super(s, o), (this.state = { isEditBox: !1, value: '' })); + } + componentDidMount() { + this.updateValues.call(this, this.props); + } + UNSAFE_componentWillReceiveProps(s) { + this.updateValues.call(this, s); + } + updateValues = (s) => { + let { param: o, isExecute: i, consumesValue: u = '' } = s, + _ = /xml/i.test(u), + w = /json/i.test(u), + x = _ ? o.get('value_xml') : o.get('value'); + if (void 0 !== x) { + let s = !x && w ? '{}' : x; + (this.setState({ value: s }), this.onChange(s, { isXml: _, isEditBox: i })); + } else + _ + ? this.onChange(this.sample('xml'), { isXml: _, isEditBox: i }) + : this.onChange(this.sample(), { isEditBox: i }); + }; + sample = (s) => { + let { param: o, fn: i } = this.props, + u = i.inferSchema(o.toJS()); + return i.getSampleSchema(u, s, { includeWriteOnly: !0 }); + }; + onChange = (s, { isEditBox: o, isXml: i }) => { + (this.setState({ value: s, isEditBox: o }), this._onChange(s, i)); + }; + _onChange = (s, o) => { + (this.props.onChange || eC)(s, o); + }; + handleOnChange = (s) => { + const { consumesValue: o } = this.props, + i = /xml/i.test(o), + u = s.target.value; + this.onChange(u, { isXml: i, isEditBox: this.state.isEditBox }); + }; + toggleIsEditBox = () => this.setState((s) => ({ isEditBox: !s.isEditBox })); + render() { + let { + onChangeConsumes: s, + param: o, + isExecute: i, + specSelectors: u, + pathMethod: _, + getComponent: w + } = this.props; + const x = w('Button'), + C = w('TextArea'), + j = w('HighlightCode', !0), + L = w('contentType'); + let B = (u ? u.parameterWithMetaByIdentity(_, o) : o).get('errors', (0, qe.List)()), + $ = u.contentTypeValues(_).get('requestContentType'), + V = + this.props.consumes && this.props.consumes.size + ? this.props.consumes + : ParamBody.defaultProp.consumes, + { value: U, isEditBox: z } = this.state, + Y = null; + getKnownSyntaxHighlighterLanguage(U) && (Y = 'json'); + const Z = `${createHtmlReadyId(`${_[1]}${_[0]}_parameters`)}_select`; + return Pe.createElement( + 'div', + { + className: 'body-param', + 'data-param-name': o.get('name'), + 'data-param-in': o.get('in') + }, + z && i + ? Pe.createElement(C, { + className: 'body-param__text' + (B.count() ? ' invalid' : ''), + value: U, + onChange: this.handleOnChange + }) + : U && Pe.createElement(j, { className: 'body-param__example', language: Y }, U), + Pe.createElement( + 'div', + { className: 'body-param-options' }, + i + ? Pe.createElement( + 'div', + { className: 'body-param-edit' }, + Pe.createElement( + x, + { + className: z + ? 'btn cancel body-param__example-edit' + : 'btn edit body-param__example-edit', + onClick: this.toggleIsEditBox + }, + z ? 'Cancel' : 'Edit' + ) + ) + : null, + Pe.createElement( + 'label', + { htmlFor: Z }, + Pe.createElement('span', null, 'Parameter content type'), + Pe.createElement(L, { + value: $, + contentTypes: V, + onChange: s, + className: 'body-param-content-type', + ariaLabel: 'Parameter content type', + controlId: Z + }) + ) + ) + ); + } + } + class Curl extends Pe.Component { + render() { + const { request: s, getComponent: o } = this.props, + i = requestSnippetGenerator_curl_bash(s), + u = o('SyntaxHighlighter', !0); + return Pe.createElement( + 'div', + { className: 'curl-command' }, + Pe.createElement('h4', null, 'Curl'), + Pe.createElement( + 'div', + { className: 'copy-to-clipboard' }, + Pe.createElement(Jn.CopyToClipboard, { text: i }, Pe.createElement('button', null)) + ), + Pe.createElement( + 'div', + null, + Pe.createElement( + u, + { + language: 'bash', + className: 'curl microlight', + renderPlainText: ({ children: s, PlainTextViewer: o }) => + Pe.createElement(o, { className: 'curl' }, s) + }, + i + ) + ) + ); + } + } + const property = ({ propKey: s, propVal: o, propClass: i }) => + Pe.createElement( + 'span', + { className: i }, + Pe.createElement('br', null), + s, + ': ', + String(o) + ); + class TryItOutButton extends Pe.Component { + static defaultProps = { + onTryoutClick: Function.prototype, + onCancelClick: Function.prototype, + onResetClick: Function.prototype, + enabled: !1, + hasUserEditedBody: !1, + isOAS3: !1 + }; + render() { + const { + onTryoutClick: s, + onCancelClick: o, + onResetClick: i, + enabled: u, + hasUserEditedBody: _, + isOAS3: w + } = this.props, + x = w && _; + return Pe.createElement( + 'div', + { className: x ? 'try-out btn-group' : 'try-out' }, + u + ? Pe.createElement( + 'button', + { className: 'btn try-out__btn cancel', onClick: o }, + 'Cancel' + ) + : Pe.createElement( + 'button', + { className: 'btn try-out__btn', onClick: s }, + 'Try it out ' + ), + x && + Pe.createElement( + 'button', + { className: 'btn try-out__btn reset', onClick: i }, + 'Reset' + ) + ); + } + } + class VersionPragmaFilter extends Pe.PureComponent { + static defaultProps = { alsoShow: null, children: null, bypass: !1 }; + render() { + const { bypass: s, isSwagger2: o, isOAS3: i, alsoShow: u } = this.props; + return s + ? Pe.createElement('div', null, this.props.children) + : o && i + ? Pe.createElement( + 'div', + { className: 'version-pragma' }, + u, + Pe.createElement( + 'div', + { className: 'version-pragma__message version-pragma__message--ambiguous' }, + Pe.createElement( + 'div', + null, + Pe.createElement('h3', null, 'Unable to render this definition'), + Pe.createElement( + 'p', + null, + Pe.createElement('code', null, 'swagger'), + ' and ', + Pe.createElement('code', null, 'openapi'), + ' fields cannot be present in the same Swagger or OpenAPI definition. Please remove one of the fields.' + ), + Pe.createElement( + 'p', + null, + 'Supported version fields are ', + Pe.createElement('code', null, 'swagger: ', '"2.0"'), + ' and those that match ', + Pe.createElement('code', null, 'openapi: 3.0.n'), + ' (for example, ', + Pe.createElement('code', null, 'openapi: 3.0.0'), + ').' + ) + ) + ) + ) + : o || i + ? Pe.createElement('div', null, this.props.children) + : Pe.createElement( + 'div', + { className: 'version-pragma' }, + u, + Pe.createElement( + 'div', + { className: 'version-pragma__message version-pragma__message--missing' }, + Pe.createElement( + 'div', + null, + Pe.createElement('h3', null, 'Unable to render this definition'), + Pe.createElement( + 'p', + null, + 'The provided definition does not specify a valid version field.' + ), + Pe.createElement( + 'p', + null, + 'Please indicate a valid Swagger or OpenAPI version field. Supported version fields are ', + Pe.createElement('code', null, 'swagger: ', '"2.0"'), + ' and those that match ', + Pe.createElement('code', null, 'openapi: 3.0.n'), + ' (for example, ', + Pe.createElement('code', null, 'openapi: 3.0.0'), + ').' + ) + ) + ) + ); + } + } + const version_stamp = ({ version: s }) => + Pe.createElement( + 'small', + null, + Pe.createElement('pre', { className: 'version' }, ' ', s, ' ') + ), + openapi_version = ({ oasVersion: s }) => + Pe.createElement( + 'small', + { className: 'version-stamp' }, + Pe.createElement('pre', { className: 'version' }, 'OAS ', s) + ), + deep_link = ({ enabled: s, path: o, text: i }) => + Pe.createElement( + 'a', + { + className: 'nostyle', + onClick: s ? (s) => s.preventDefault() : null, + href: s ? `#/${o}` : null + }, + Pe.createElement('span', null, i) + ), + svg_assets = () => + Pe.createElement( + 'div', + null, + Pe.createElement( + 'svg', + { + xmlns: 'http://www.w3.org/2000/svg', + xmlnsXlink: 'http://www.w3.org/1999/xlink', + className: 'svg-assets' + }, + Pe.createElement( + 'defs', + null, + Pe.createElement( + 'symbol', + { viewBox: '0 0 20 20', id: 'unlocked' }, + Pe.createElement('path', { + d: 'M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z' + }) + ), + Pe.createElement( + 'symbol', + { viewBox: '0 0 20 20', id: 'locked' }, + Pe.createElement('path', { + d: 'M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z' + }) + ), + Pe.createElement( + 'symbol', + { viewBox: '0 0 20 20', id: 'close' }, + Pe.createElement('path', { + d: 'M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z' + }) + ), + Pe.createElement( + 'symbol', + { viewBox: '0 0 20 20', id: 'large-arrow' }, + Pe.createElement('path', { + d: 'M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z' + }) + ), + Pe.createElement( + 'symbol', + { viewBox: '0 0 20 20', id: 'large-arrow-down' }, + Pe.createElement('path', { + d: 'M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z' + }) + ), + Pe.createElement( + 'symbol', + { viewBox: '0 0 20 20', id: 'large-arrow-up' }, + Pe.createElement('path', { + d: 'M 17.418 14.908 C 17.69 15.176 18.127 15.176 18.397 14.908 C 18.667 14.64 18.668 14.207 18.397 13.939 L 10.489 6.109 C 10.219 5.841 9.782 5.841 9.51 6.109 L 1.602 13.939 C 1.332 14.207 1.332 14.64 1.602 14.908 C 1.873 15.176 2.311 15.176 2.581 14.908 L 10 7.767 L 17.418 14.908 Z' + }) + ), + Pe.createElement( + 'symbol', + { viewBox: '0 0 24 24', id: 'jump-to' }, + Pe.createElement('path', { + d: 'M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z' + }) + ), + Pe.createElement( + 'symbol', + { viewBox: '0 0 24 24', id: 'expand' }, + Pe.createElement('path', { + d: 'M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z' + }) + ), + Pe.createElement( + 'symbol', + { viewBox: '0 0 15 16', id: 'copy' }, + Pe.createElement( + 'g', + { transform: 'translate(2, -1)' }, + Pe.createElement('path', { + fill: '#ffffff', + fillRule: 'evenodd', + d: 'M2 13h4v1H2v-1zm5-6H2v1h5V7zm2 3V8l-3 3 3 3v-2h5v-2H9zM4.5 9H2v1h2.5V9zM2 12h2.5v-1H2v1zm9 1h1v2c-.02.28-.11.52-.3.7-.19.18-.42.28-.7.3H1c-.55 0-1-.45-1-1V4c0-.55.45-1 1-1h3c0-1.11.89-2 2-2 1.11 0 2 .89 2 2h3c.55 0 1 .45 1 1v5h-1V6H1v9h10v-2zM2 5h8c0-.55-.45-1-1-1H8c-.55 0-1-.45-1-1s-.45-1-1-1-1 .45-1 1-.45 1-1 1H3c-.55 0-1 .45-1 1z' + }) + ) + ) + ) + ) + ); + var tC; + function decodeEntity(s) { + return ( + ((tC = tC || document.createElement('textarea')).innerHTML = '&' + s + ';'), + tC.value + ); + } + var rC = Object.prototype.hasOwnProperty; + function index_browser_has(s, o) { + return !!s && rC.call(s, o); + } + function index_browser_assign(s) { + return ( + [].slice.call(arguments, 1).forEach(function (o) { + if (o) { + if ('object' != typeof o) throw new TypeError(o + 'must be object'); + Object.keys(o).forEach(function (i) { + s[i] = o[i]; + }); + } + }), + s + ); + } + var nC = /\\([\\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g; + function unescapeMd(s) { + return s.indexOf('\\') < 0 ? s : s.replace(nC, '$1'); + } + function isValidEntityCode(s) { + return ( + !(s >= 55296 && s <= 57343) && + !(s >= 64976 && s <= 65007) && + !!(65535 & ~s && 65534 != (65535 & s)) && + !(s >= 0 && s <= 8) && + 11 !== s && + !(s >= 14 && s <= 31) && + !(s >= 127 && s <= 159) && + !(s > 1114111) + ); + } + function fromCodePoint(s) { + if (s > 65535) { + var o = 55296 + ((s -= 65536) >> 10), + i = 56320 + (1023 & s); + return String.fromCharCode(o, i); + } + return String.fromCharCode(s); + } + var sC = /&([a-z#][a-z0-9]{1,31});/gi, + oC = /^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))/i; + function replaceEntityPattern(s, o) { + var i = 0, + u = decodeEntity(o); + return o !== u + ? u + : 35 === o.charCodeAt(0) && + oC.test(o) && + isValidEntityCode( + (i = + 'x' === o[1].toLowerCase() + ? parseInt(o.slice(2), 16) + : parseInt(o.slice(1), 10)) + ) + ? fromCodePoint(i) + : s; + } + function replaceEntities(s) { + return s.indexOf('&') < 0 ? s : s.replace(sC, replaceEntityPattern); + } + var iC = /[&<>"]/, + aC = /[&<>"]/g, + lC = { '&': '&', '<': '<', '>': '>', '"': '"' }; + function replaceUnsafeChar(s) { + return lC[s]; + } + function escapeHtml(s) { + return iC.test(s) ? s.replace(aC, replaceUnsafeChar) : s; + } + var cC = {}; + function nextToken(s, o) { + return ++o >= s.length - 2 + ? o + : 'paragraph_open' === s[o].type && + s[o].tight && + 'inline' === s[o + 1].type && + 0 === s[o + 1].content.length && + 'paragraph_close' === s[o + 2].type && + s[o + 2].tight + ? nextToken(s, o + 2) + : o; + } + ((cC.blockquote_open = function () { + return '
      \n'; + }), + (cC.blockquote_close = function (s, o) { + return '
      ' + uC(s, o); + }), + (cC.code = function (s, o) { + return s[o].block + ? '
      ' + escapeHtml(s[o].content) + '
      ' + uC(s, o) + : '' + escapeHtml(s[o].content) + ''; + }), + (cC.fence = function (s, o, i, u, _) { + var w, + x, + C = s[o], + j = '', + L = i.langPrefix; + if (C.params) { + if ( + ((x = (w = C.params.split(/\s+/g)).join(' ')), + index_browser_has(_.rules.fence_custom, w[0])) + ) + return _.rules.fence_custom[w[0]](s, o, i, u, _); + j = ' class="' + L + escapeHtml(replaceEntities(unescapeMd(x))) + '"'; + } + return ( + '
      ' +
      +							((i.highlight && i.highlight.apply(i.highlight, [C.content].concat(w))) ||
      +								escapeHtml(C.content)) +
      +							'
      ' + + uC(s, o) + ); + }), + (cC.fence_custom = {}), + (cC.heading_open = function (s, o) { + return ''; + }), + (cC.heading_close = function (s, o) { + return '\n'; + }), + (cC.hr = function (s, o, i) { + return (i.xhtmlOut ? '
      ' : '
      ') + uC(s, o); + }), + (cC.bullet_list_open = function () { + return '
        \n'; + }), + (cC.bullet_list_close = function (s, o) { + return '
      ' + uC(s, o); + }), + (cC.list_item_open = function () { + return '
    • '; + }), + (cC.list_item_close = function () { + return '
    • \n'; + }), + (cC.ordered_list_open = function (s, o) { + var i = s[o]; + return ' 1 ? ' start="' + i.order + '"' : '') + '>\n'; + }), + (cC.ordered_list_close = function (s, o) { + return '' + uC(s, o); + }), + (cC.paragraph_open = function (s, o) { + return s[o].tight ? '' : '

      '; + }), + (cC.paragraph_close = function (s, o) { + var i = !(s[o].tight && o && 'inline' === s[o - 1].type && !s[o - 1].content); + return (s[o].tight ? '' : '

      ') + (i ? uC(s, o) : ''); + }), + (cC.link_open = function (s, o, i) { + var u = s[o].title ? ' title="' + escapeHtml(replaceEntities(s[o].title)) + '"' : '', + _ = i.linkTarget ? ' target="' + i.linkTarget + '"' : ''; + return ''; + }), + (cC.link_close = function () { + return ''; + }), + (cC.image = function (s, o, i) { + var u = ' src="' + escapeHtml(s[o].src) + '"', + _ = s[o].title ? ' title="' + escapeHtml(replaceEntities(s[o].title)) + '"' : ''; + return ( + '' + ); + }), + (cC.table_open = function () { + return '\n'; + }), + (cC.table_close = function () { + return '
      \n'; + }), + (cC.thead_open = function () { + return '\n'; + }), + (cC.thead_close = function () { + return '\n'; + }), + (cC.tbody_open = function () { + return '\n'; + }), + (cC.tbody_close = function () { + return '\n'; + }), + (cC.tr_open = function () { + return ''; + }), + (cC.tr_close = function () { + return '\n'; + }), + (cC.th_open = function (s, o) { + var i = s[o]; + return ''; + }), + (cC.th_close = function () { + return ''; + }), + (cC.td_open = function (s, o) { + var i = s[o]; + return ''; + }), + (cC.td_close = function () { + return ''; + }), + (cC.strong_open = function () { + return ''; + }), + (cC.strong_close = function () { + return ''; + }), + (cC.em_open = function () { + return ''; + }), + (cC.em_close = function () { + return ''; + }), + (cC.del_open = function () { + return ''; + }), + (cC.del_close = function () { + return ''; + }), + (cC.ins_open = function () { + return ''; + }), + (cC.ins_close = function () { + return ''; + }), + (cC.mark_open = function () { + return ''; + }), + (cC.mark_close = function () { + return ''; + }), + (cC.sub = function (s, o) { + return '' + escapeHtml(s[o].content) + ''; + }), + (cC.sup = function (s, o) { + return '' + escapeHtml(s[o].content) + ''; + }), + (cC.hardbreak = function (s, o, i) { + return i.xhtmlOut ? '
      \n' : '
      \n'; + }), + (cC.softbreak = function (s, o, i) { + return i.breaks ? (i.xhtmlOut ? '
      \n' : '
      \n') : '\n'; + }), + (cC.text = function (s, o) { + return escapeHtml(s[o].content); + }), + (cC.htmlblock = function (s, o) { + return s[o].content; + }), + (cC.htmltag = function (s, o) { + return s[o].content; + }), + (cC.abbr_open = function (s, o) { + return ''; + }), + (cC.abbr_close = function () { + return ''; + }), + (cC.footnote_ref = function (s, o) { + var i = Number(s[o].id + 1).toString(), + u = 'fnref' + i; + return ( + s[o].subId > 0 && (u += ':' + s[o].subId), + '[' + + i + + ']' + ); + }), + (cC.footnote_block_open = function (s, o, i) { + return ( + (i.xhtmlOut ? '
      \n' : '
      \n') + + '
      \n
        \n' + ); + }), + (cC.footnote_block_close = function () { + return '
      \n
      \n'; + }), + (cC.footnote_open = function (s, o) { + return '
    • '; + }), + (cC.footnote_close = function () { + return '
    • \n'; + }), + (cC.footnote_anchor = function (s, o) { + var i = 'fnref' + Number(s[o].id + 1).toString(); + return ( + s[o].subId > 0 && (i += ':' + s[o].subId), + ' ' + ); + }), + (cC.dl_open = function () { + return '
      \n'; + }), + (cC.dt_open = function () { + return '
      '; + }), + (cC.dd_open = function () { + return '
      '; + }), + (cC.dl_close = function () { + return '
      \n'; + }), + (cC.dt_close = function () { + return '\n'; + }), + (cC.dd_close = function () { + return '\n'; + })); + var uC = (cC.getBreak = function getBreak(s, o) { + return (o = nextToken(s, o)) < s.length && 'list_item_close' === s[o].type ? '' : '\n'; + }); + function Renderer() { + ((this.rules = index_browser_assign({}, cC)), (this.getBreak = cC.getBreak)); + } + function Ruler() { + ((this.__rules__ = []), (this.__cache__ = null)); + } + function StateInline(s, o, i, u, _) { + ((this.src = s), + (this.env = u), + (this.options = i), + (this.parser = o), + (this.tokens = _), + (this.pos = 0), + (this.posMax = this.src.length), + (this.level = 0), + (this.pending = ''), + (this.pendingLevel = 0), + (this.cache = []), + (this.isInLabel = !1), + (this.linkLevel = 0), + (this.linkContent = ''), + (this.labelUnmatchedScopes = 0)); + } + function parseLinkLabel(s, o) { + var i, + u, + _, + w = -1, + x = s.posMax, + C = s.pos, + j = s.isInLabel; + if (s.isInLabel) return -1; + if (s.labelUnmatchedScopes) return (s.labelUnmatchedScopes--, -1); + for (s.pos = o + 1, s.isInLabel = !0, i = 1; s.pos < x; ) { + if (91 === (_ = s.src.charCodeAt(s.pos))) i++; + else if (93 === _ && 0 === --i) { + u = !0; + break; + } + s.parser.skipToken(s); + } + return ( + u ? ((w = s.pos), (s.labelUnmatchedScopes = 0)) : (s.labelUnmatchedScopes = i - 1), + (s.pos = C), + (s.isInLabel = j), + w + ); + } + function parseAbbr(s, o, i, u) { + var _, w, x, C, j, L; + if (42 !== s.charCodeAt(0)) return -1; + if (91 !== s.charCodeAt(1)) return -1; + if (-1 === s.indexOf(']:')) return -1; + if ( + (w = parseLinkLabel((_ = new StateInline(s, o, i, u, [])), 1)) < 0 || + 58 !== s.charCodeAt(w + 1) + ) + return -1; + for (C = _.posMax, x = w + 2; x < C && 10 !== _.src.charCodeAt(x); x++); + return ( + (j = s.slice(2, w)), + 0 === (L = s.slice(w + 2, x).trim()).length + ? -1 + : (u.abbreviations || (u.abbreviations = {}), + void 0 === u.abbreviations[':' + j] && (u.abbreviations[':' + j] = L), + x) + ); + } + function normalizeLink(s) { + var o = replaceEntities(s); + try { + o = decodeURI(o); + } catch (s) {} + return encodeURI(o); + } + function parseLinkDestination(s, o) { + var i, + u, + _, + w = o, + x = s.posMax; + if (60 === s.src.charCodeAt(o)) { + for (o++; o < x; ) { + if (10 === (i = s.src.charCodeAt(o))) return !1; + if (62 === i) + return ( + (_ = normalizeLink(unescapeMd(s.src.slice(w + 1, o)))), + !!s.parser.validateLink(_) && ((s.pos = o + 1), (s.linkContent = _), !0) + ); + 92 === i && o + 1 < x ? (o += 2) : o++; + } + return !1; + } + for (u = 0; o < x && 32 !== (i = s.src.charCodeAt(o)) && !(i < 32 || 127 === i); ) + if (92 === i && o + 1 < x) o += 2; + else { + if (40 === i && ++u > 1) break; + if (41 === i && --u < 0) break; + o++; + } + return ( + w !== o && + ((_ = unescapeMd(s.src.slice(w, o))), + !!s.parser.validateLink(_) && ((s.linkContent = _), (s.pos = o), !0)) + ); + } + function parseLinkTitle(s, o) { + var i, + u = o, + _ = s.posMax, + w = s.src.charCodeAt(o); + if (34 !== w && 39 !== w && 40 !== w) return !1; + for (o++, 40 === w && (w = 41); o < _; ) { + if ((i = s.src.charCodeAt(o)) === w) + return ((s.pos = o + 1), (s.linkContent = unescapeMd(s.src.slice(u + 1, o))), !0); + 92 === i && o + 1 < _ ? (o += 2) : o++; + } + return !1; + } + function normalizeReference(s) { + return s.trim().replace(/\s+/g, ' ').toUpperCase(); + } + function parseReference(s, o, i, u) { + var _, w, x, C, j, L, B, $, V; + if (91 !== s.charCodeAt(0)) return -1; + if (-1 === s.indexOf(']:')) return -1; + if ( + (w = parseLinkLabel((_ = new StateInline(s, o, i, u, [])), 0)) < 0 || + 58 !== s.charCodeAt(w + 1) + ) + return -1; + for ( + C = _.posMax, x = w + 2; + x < C && (32 === (j = _.src.charCodeAt(x)) || 10 === j); + x++ + ); + if (!parseLinkDestination(_, x)) return -1; + for ( + B = _.linkContent, L = x = _.pos, x += 1; + x < C && (32 === (j = _.src.charCodeAt(x)) || 10 === j); + x++ + ); + for ( + x < C && L !== x && parseLinkTitle(_, x) + ? (($ = _.linkContent), (x = _.pos)) + : (($ = ''), (x = L)); + x < C && 32 === _.src.charCodeAt(x); + ) + x++; + return x < C && 10 !== _.src.charCodeAt(x) + ? -1 + : ((V = normalizeReference(s.slice(1, w))), + void 0 === u.references[V] && (u.references[V] = { title: $, href: B }), + x); + } + ((Renderer.prototype.renderInline = function (s, o, i) { + for (var u = this.rules, _ = s.length, w = 0, x = ''; _--; ) + x += u[s[w].type](s, w++, o, i, this); + return x; + }), + (Renderer.prototype.render = function (s, o, i) { + for (var u = this.rules, _ = s.length, w = -1, x = ''; ++w < _; ) + 'inline' === s[w].type + ? (x += this.renderInline(s[w].children, o, i)) + : (x += u[s[w].type](s, w, o, i, this)); + return x; + }), + (Ruler.prototype.__find__ = function (s) { + for (var o = this.__rules__.length, i = -1; o--; ) + if (this.__rules__[++i].name === s) return i; + return -1; + }), + (Ruler.prototype.__compile__ = function () { + var s = this, + o = ['']; + (s.__rules__.forEach(function (s) { + s.enabled && + s.alt.forEach(function (s) { + o.indexOf(s) < 0 && o.push(s); + }); + }), + (s.__cache__ = {}), + o.forEach(function (o) { + ((s.__cache__[o] = []), + s.__rules__.forEach(function (i) { + i.enabled && ((o && i.alt.indexOf(o) < 0) || s.__cache__[o].push(i.fn)); + })); + })); + }), + (Ruler.prototype.at = function (s, o, i) { + var u = this.__find__(s), + _ = i || {}; + if (-1 === u) throw new Error('Parser rule not found: ' + s); + ((this.__rules__[u].fn = o), + (this.__rules__[u].alt = _.alt || []), + (this.__cache__ = null)); + }), + (Ruler.prototype.before = function (s, o, i, u) { + var _ = this.__find__(s), + w = u || {}; + if (-1 === _) throw new Error('Parser rule not found: ' + s); + (this.__rules__.splice(_, 0, { name: o, enabled: !0, fn: i, alt: w.alt || [] }), + (this.__cache__ = null)); + }), + (Ruler.prototype.after = function (s, o, i, u) { + var _ = this.__find__(s), + w = u || {}; + if (-1 === _) throw new Error('Parser rule not found: ' + s); + (this.__rules__.splice(_ + 1, 0, { name: o, enabled: !0, fn: i, alt: w.alt || [] }), + (this.__cache__ = null)); + }), + (Ruler.prototype.push = function (s, o, i) { + var u = i || {}; + (this.__rules__.push({ name: s, enabled: !0, fn: o, alt: u.alt || [] }), + (this.__cache__ = null)); + }), + (Ruler.prototype.enable = function (s, o) { + ((s = Array.isArray(s) ? s : [s]), + o && + this.__rules__.forEach(function (s) { + s.enabled = !1; + }), + s.forEach(function (s) { + var o = this.__find__(s); + if (o < 0) throw new Error('Rules manager: invalid rule name ' + s); + this.__rules__[o].enabled = !0; + }, this), + (this.__cache__ = null)); + }), + (Ruler.prototype.disable = function (s) { + ((s = Array.isArray(s) ? s : [s]).forEach(function (s) { + var o = this.__find__(s); + if (o < 0) throw new Error('Rules manager: invalid rule name ' + s); + this.__rules__[o].enabled = !1; + }, this), + (this.__cache__ = null)); + }), + (Ruler.prototype.getRules = function (s) { + return (null === this.__cache__ && this.__compile__(), this.__cache__[s] || []); + }), + (StateInline.prototype.pushPending = function () { + (this.tokens.push({ type: 'text', content: this.pending, level: this.pendingLevel }), + (this.pending = '')); + }), + (StateInline.prototype.push = function (s) { + (this.pending && this.pushPending(), + this.tokens.push(s), + (this.pendingLevel = this.level)); + }), + (StateInline.prototype.cacheSet = function (s, o) { + for (var i = this.cache.length; i <= s; i++) this.cache.push(0); + this.cache[s] = o; + }), + (StateInline.prototype.cacheGet = function (s) { + return s < this.cache.length ? this.cache[s] : 0; + })); + var pC = ' \n()[]\'".,!?-'; + function regEscape(s) { + return s.replace(/([-()\[\]{}+?*.$\^|,:#= s.length) && !yC.test(s[o]); + } + function replaceAt(s, o, i) { + return s.substr(0, o) + i + s.substr(o + 1); + } + var vC = [ + [ + 'block', + function block(s) { + s.inlineMode + ? s.tokens.push({ + type: 'inline', + content: s.src.replace(/\n/g, ' ').trim(), + level: 0, + lines: [0, 1], + children: [] + }) + : s.block.parse(s.src, s.options, s.env, s.tokens); + } + ], + [ + 'abbr', + function abbr(s) { + var o, + i, + u, + _, + w = s.tokens; + if (!s.inlineMode) + for (o = 1, i = w.length - 1; o < i; o++) + if ( + 'paragraph_open' === w[o - 1].type && + 'inline' === w[o].type && + 'paragraph_close' === w[o + 1].type + ) { + for ( + u = w[o].content; + u.length && !((_ = parseAbbr(u, s.inline, s.options, s.env)) < 0); + ) + u = u.slice(_).trim(); + ((w[o].content = u), + u.length || ((w[o - 1].tight = !0), (w[o + 1].tight = !0))); + } + } + ], + [ + 'references', + function references(s) { + var o, + i, + u, + _, + w = s.tokens; + if (((s.env.references = s.env.references || {}), !s.inlineMode)) + for (o = 1, i = w.length - 1; o < i; o++) + if ( + 'inline' === w[o].type && + 'paragraph_open' === w[o - 1].type && + 'paragraph_close' === w[o + 1].type + ) { + for ( + u = w[o].content; + u.length && !((_ = parseReference(u, s.inline, s.options, s.env)) < 0); + ) + u = u.slice(_).trim(); + ((w[o].content = u), + u.length || ((w[o - 1].tight = !0), (w[o + 1].tight = !0))); + } + } + ], + [ + 'inline', + function inline(s) { + var o, + i, + u, + _ = s.tokens; + for (i = 0, u = _.length; i < u; i++) + 'inline' === (o = _[i]).type && + s.inline.parse(o.content, s.options, s.env, o.children); + } + ], + [ + 'footnote_tail', + function footnote_block(s) { + var o, + i, + u, + _, + w, + x, + C, + j, + L, + B = 0, + $ = !1, + V = {}; + if ( + s.env.footnotes && + ((s.tokens = s.tokens.filter(function (s) { + return 'footnote_reference_open' === s.type + ? (($ = !0), (j = []), (L = s.label), !1) + : 'footnote_reference_close' === s.type + ? (($ = !1), (V[':' + L] = j), !1) + : ($ && j.push(s), !$); + })), + s.env.footnotes.list) + ) { + for ( + x = s.env.footnotes.list, + s.tokens.push({ type: 'footnote_block_open', level: B++ }), + o = 0, + i = x.length; + o < i; + o++ + ) { + for ( + s.tokens.push({ type: 'footnote_open', id: o, level: B++ }), + x[o].tokens + ? ((C = []).push({ type: 'paragraph_open', tight: !1, level: B++ }), + C.push({ type: 'inline', content: '', level: B, children: x[o].tokens }), + C.push({ type: 'paragraph_close', tight: !1, level: --B })) + : x[o].label && (C = V[':' + x[o].label]), + s.tokens = s.tokens.concat(C), + w = + 'paragraph_close' === s.tokens[s.tokens.length - 1].type + ? s.tokens.pop() + : null, + _ = x[o].count > 0 ? x[o].count : 1, + u = 0; + u < _; + u++ + ) + s.tokens.push({ type: 'footnote_anchor', id: o, subId: u, level: B }); + (w && s.tokens.push(w), s.tokens.push({ type: 'footnote_close', level: --B })); + } + s.tokens.push({ type: 'footnote_block_close', level: --B }); + } + } + ], + [ + 'abbr2', + function abbr2(s) { + var o, + i, + u, + _, + w, + x, + C, + j, + L, + B, + $, + V, + U = s.tokens; + if (s.env.abbreviations) + for ( + s.env.abbrRegExp || + ((V = + '(^|[' + + pC.split('').map(regEscape).join('') + + '])(' + + Object.keys(s.env.abbreviations) + .map(function (s) { + return s.substr(1); + }) + .sort(function (s, o) { + return o.length - s.length; + }) + .map(regEscape) + .join('|') + + ')($|[' + + pC.split('').map(regEscape).join('') + + '])'), + (s.env.abbrRegExp = new RegExp(V, 'g'))), + B = s.env.abbrRegExp, + i = 0, + u = U.length; + i < u; + i++ + ) + if ('inline' === U[i].type) + for (o = (_ = U[i].children).length - 1; o >= 0; o--) + if ('text' === (w = _[o]).type) { + for ( + j = 0, x = w.content, B.lastIndex = 0, L = w.level, C = []; + ($ = B.exec(x)); + ) + (B.lastIndex > j && + C.push({ + type: 'text', + content: x.slice(j, $.index + $[1].length), + level: L + }), + C.push({ + type: 'abbr_open', + title: s.env.abbreviations[':' + $[2]], + level: L++ + }), + C.push({ type: 'text', content: $[2], level: L }), + C.push({ type: 'abbr_close', level: --L }), + (j = B.lastIndex - $[3].length)); + C.length && + (j < x.length && C.push({ type: 'text', content: x.slice(j), level: L }), + (U[i].children = _ = [].concat(_.slice(0, o), C, _.slice(o + 1)))); + } + } + ], + [ + 'replacements', + function index_browser_replace(s) { + var o, i, u, _, w; + if (s.options.typographer) + for (w = s.tokens.length - 1; w >= 0; w--) + if ('inline' === s.tokens[w].type) + for (o = (_ = s.tokens[w].children).length - 1; o >= 0; o--) + 'text' === (i = _[o]).type && + ((u = replaceScopedAbbr((u = i.content))), + hC.test(u) && + (u = u + .replace(/\+-/g, '±') + .replace(/\.{2,}/g, '…') + .replace(/([?!])…/g, '$1..') + .replace(/([?!]){4,}/g, '$1$1$1') + .replace(/,{2,}/g, ',') + .replace(/(^|[^-])---([^-]|$)/gm, '$1—$2') + .replace(/(^|\s)--(\s|$)/gm, '$1–$2') + .replace(/(^|[^-\s])--([^-\s]|$)/gm, '$1–$2')), + (i.content = u)); + } + ], + [ + 'smartquotes', + function smartquotes(s) { + var o, i, u, _, w, x, C, j, L, B, $, V, U, z, Y, Z, ee; + if (s.options.typographer) + for (ee = [], Y = s.tokens.length - 1; Y >= 0; Y--) + if ('inline' === s.tokens[Y].type) + for (Z = s.tokens[Y].children, ee.length = 0, o = 0; o < Z.length; o++) + if ('text' === (i = Z[o]).type && !mC.test(i.text)) { + for (C = Z[o].level, U = ee.length - 1; U >= 0 && !(ee[U].level <= C); U--); + ((ee.length = U + 1), (w = 0), (x = (u = i.content).length)); + e: for (; w < x && ((gC.lastIndex = w), (_ = gC.exec(u))); ) + if ( + ((j = !isLetter(u, _.index - 1)), + (w = _.index + 1), + (z = "'" === _[0]), + (L = !isLetter(u, w)) || j) + ) { + if ((($ = !L), (V = !j))) + for ( + U = ee.length - 1; + U >= 0 && ((B = ee[U]), !(ee[U].level < C)); + U-- + ) + if (B.single === z && ee[U].level === C) { + ((B = ee[U]), + z + ? ((Z[B.token].content = replaceAt( + Z[B.token].content, + B.pos, + s.options.quotes[2] + )), + (i.content = replaceAt( + i.content, + _.index, + s.options.quotes[3] + ))) + : ((Z[B.token].content = replaceAt( + Z[B.token].content, + B.pos, + s.options.quotes[0] + )), + (i.content = replaceAt( + i.content, + _.index, + s.options.quotes[1] + ))), + (ee.length = U)); + continue e; + } + $ + ? ee.push({ token: o, pos: _.index, single: z, level: C }) + : V && z && (i.content = replaceAt(i.content, _.index, '’')); + } else z && (i.content = replaceAt(i.content, _.index, '’')); + } + } + ] + ]; + function Core() { + ((this.options = {}), (this.ruler = new Ruler())); + for (var s = 0; s < vC.length; s++) this.ruler.push(vC[s][0], vC[s][1]); + } + function StateBlock(s, o, i, u, _) { + var w, x, C, j, L, B, $; + for ( + this.src = s, + this.parser = o, + this.options = i, + this.env = u, + this.tokens = _, + this.bMarks = [], + this.eMarks = [], + this.tShift = [], + this.blkIndent = 0, + this.line = 0, + this.lineMax = 0, + this.tight = !1, + this.parentType = 'root', + this.ddIndent = -1, + this.level = 0, + this.result = '', + B = 0, + $ = !1, + C = j = B = 0, + L = (x = this.src).length; + j < L; + j++ + ) { + if (((w = x.charCodeAt(j)), !$)) { + if (32 === w) { + B++; + continue; + } + $ = !0; + } + (10 !== w && j !== L - 1) || + (10 !== w && j++, + this.bMarks.push(C), + this.eMarks.push(j), + this.tShift.push(B), + ($ = !1), + (B = 0), + (C = j + 1)); + } + (this.bMarks.push(x.length), + this.eMarks.push(x.length), + this.tShift.push(0), + (this.lineMax = this.bMarks.length - 1)); + } + function skipBulletListMarker(s, o) { + var i, u, _; + return (u = s.bMarks[o] + s.tShift[o]) >= (_ = s.eMarks[o]) || + (42 !== (i = s.src.charCodeAt(u++)) && 45 !== i && 43 !== i) || + (u < _ && 32 !== s.src.charCodeAt(u)) + ? -1 + : u; + } + function skipOrderedListMarker(s, o) { + var i, + u = s.bMarks[o] + s.tShift[o], + _ = s.eMarks[o]; + if (u + 1 >= _) return -1; + if ((i = s.src.charCodeAt(u++)) < 48 || i > 57) return -1; + for (;;) { + if (u >= _) return -1; + if (!((i = s.src.charCodeAt(u++)) >= 48 && i <= 57)) { + if (41 === i || 46 === i) break; + return -1; + } + } + return u < _ && 32 !== s.src.charCodeAt(u) ? -1 : u; + } + ((Core.prototype.process = function (s) { + var o, i, u; + for (o = 0, i = (u = this.ruler.getRules('')).length; o < i; o++) u[o](s); + }), + (StateBlock.prototype.isEmpty = function isEmpty(s) { + return this.bMarks[s] + this.tShift[s] >= this.eMarks[s]; + }), + (StateBlock.prototype.skipEmptyLines = function skipEmptyLines(s) { + for ( + var o = this.lineMax; + s < o && !(this.bMarks[s] + this.tShift[s] < this.eMarks[s]); + s++ + ); + return s; + }), + (StateBlock.prototype.skipSpaces = function skipSpaces(s) { + for (var o = this.src.length; s < o && 32 === this.src.charCodeAt(s); s++); + return s; + }), + (StateBlock.prototype.skipChars = function skipChars(s, o) { + for (var i = this.src.length; s < i && this.src.charCodeAt(s) === o; s++); + return s; + }), + (StateBlock.prototype.skipCharsBack = function skipCharsBack(s, o, i) { + if (s <= i) return s; + for (; s > i; ) if (o !== this.src.charCodeAt(--s)) return s + 1; + return s; + }), + (StateBlock.prototype.getLines = function getLines(s, o, i, u) { + var _, + w, + x, + C, + j, + L = s; + if (s >= o) return ''; + if (L + 1 === o) + return ( + (w = this.bMarks[L] + Math.min(this.tShift[L], i)), + (x = u ? this.eMarks[L] + 1 : this.eMarks[L]), + this.src.slice(w, x) + ); + for (C = new Array(o - s), _ = 0; L < o; L++, _++) + ((j = this.tShift[L]) > i && (j = i), + j < 0 && (j = 0), + (w = this.bMarks[L] + j), + (x = L + 1 < o || u ? this.eMarks[L] + 1 : this.eMarks[L]), + (C[_] = this.src.slice(w, x))); + return C.join(''); + })); + var bC = {}; + [ + 'article', + 'aside', + 'button', + 'blockquote', + 'body', + 'canvas', + 'caption', + 'col', + 'colgroup', + 'dd', + 'div', + 'dl', + 'dt', + 'embed', + 'fieldset', + 'figcaption', + 'figure', + 'footer', + 'form', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'header', + 'hgroup', + 'hr', + 'iframe', + 'li', + 'map', + 'object', + 'ol', + 'output', + 'p', + 'pre', + 'progress', + 'script', + 'section', + 'style', + 'table', + 'tbody', + 'td', + 'textarea', + 'tfoot', + 'th', + 'tr', + 'thead', + 'ul', + 'video' + ].forEach(function (s) { + bC[s] = !0; + }); + var _C = /^<([a-zA-Z]{1,15})[\s\/>]/, + EC = /^<\/([a-zA-Z]{1,15})[\s>]/; + function index_browser_getLine(s, o) { + var i = s.bMarks[o] + s.blkIndent, + u = s.eMarks[o]; + return s.src.substr(i, u - i); + } + function skipMarker(s, o) { + var i, + u, + _ = s.bMarks[o] + s.tShift[o], + w = s.eMarks[o]; + return _ >= w || + (126 !== (u = s.src.charCodeAt(_++)) && 58 !== u) || + _ === (i = s.skipSpaces(_)) || + i >= w + ? -1 + : i; + } + var wC = [ + [ + 'code', + function code(s, o, i) { + var u, _; + if (s.tShift[o] - s.blkIndent < 4) return !1; + for (_ = u = o + 1; u < i; ) + if (s.isEmpty(u)) u++; + else { + if (!(s.tShift[u] - s.blkIndent >= 4)) break; + _ = ++u; + } + return ( + (s.line = u), + s.tokens.push({ + type: 'code', + content: s.getLines(o, _, 4 + s.blkIndent, !0), + block: !0, + lines: [o, s.line], + level: s.level + }), + !0 + ); + } + ], + [ + 'fences', + function fences(s, o, i, u) { + var _, + w, + x, + C, + j, + L = !1, + B = s.bMarks[o] + s.tShift[o], + $ = s.eMarks[o]; + if (B + 3 > $) return !1; + if (126 !== (_ = s.src.charCodeAt(B)) && 96 !== _) return !1; + if (((j = B), (w = (B = s.skipChars(B, _)) - j) < 3)) return !1; + if ((x = s.src.slice(B, $).trim()).indexOf('`') >= 0) return !1; + if (u) return !0; + for ( + C = o; + !(++C >= i) && + !( + (B = j = s.bMarks[C] + s.tShift[C]) < ($ = s.eMarks[C]) && + s.tShift[C] < s.blkIndent + ); + ) + if ( + s.src.charCodeAt(B) === _ && + !( + s.tShift[C] - s.blkIndent >= 4 || + (B = s.skipChars(B, _)) - j < w || + (B = s.skipSpaces(B)) < $ + ) + ) { + L = !0; + break; + } + return ( + (w = s.tShift[o]), + (s.line = C + (L ? 1 : 0)), + s.tokens.push({ + type: 'fence', + params: x, + content: s.getLines(o + 1, C, w, !0), + lines: [o, s.line], + level: s.level + }), + !0 + ); + }, + ['paragraph', 'blockquote', 'list'] + ], + [ + 'blockquote', + function blockquote(s, o, i, u) { + var _, + w, + x, + C, + j, + L, + B, + $, + V, + U, + z, + Y = s.bMarks[o] + s.tShift[o], + Z = s.eMarks[o]; + if (Y > Z) return !1; + if (62 !== s.src.charCodeAt(Y++)) return !1; + if (s.level >= s.options.maxNesting) return !1; + if (u) return !0; + for ( + 32 === s.src.charCodeAt(Y) && Y++, + j = s.blkIndent, + s.blkIndent = 0, + C = [s.bMarks[o]], + s.bMarks[o] = Y, + w = (Y = Y < Z ? s.skipSpaces(Y) : Y) >= Z, + x = [s.tShift[o]], + s.tShift[o] = Y - s.bMarks[o], + $ = s.parser.ruler.getRules('blockquote'), + _ = o + 1; + _ < i && !((Y = s.bMarks[_] + s.tShift[_]) >= (Z = s.eMarks[_])); + _++ + ) + if (62 !== s.src.charCodeAt(Y++)) { + if (w) break; + for (z = !1, V = 0, U = $.length; V < U; V++) + if ($[V](s, _, i, !0)) { + z = !0; + break; + } + if (z) break; + (C.push(s.bMarks[_]), x.push(s.tShift[_]), (s.tShift[_] = -1337)); + } else + (32 === s.src.charCodeAt(Y) && Y++, + C.push(s.bMarks[_]), + (s.bMarks[_] = Y), + (w = (Y = Y < Z ? s.skipSpaces(Y) : Y) >= Z), + x.push(s.tShift[_]), + (s.tShift[_] = Y - s.bMarks[_])); + for ( + L = s.parentType, + s.parentType = 'blockquote', + s.tokens.push({ type: 'blockquote_open', lines: (B = [o, 0]), level: s.level++ }), + s.parser.tokenize(s, o, _), + s.tokens.push({ type: 'blockquote_close', level: --s.level }), + s.parentType = L, + B[1] = s.line, + V = 0; + V < x.length; + V++ + ) + ((s.bMarks[V + o] = C[V]), (s.tShift[V + o] = x[V])); + return ((s.blkIndent = j), !0); + }, + ['paragraph', 'blockquote', 'list'] + ], + [ + 'hr', + function hr(s, o, i, u) { + var _, + w, + x, + C = s.bMarks[o], + j = s.eMarks[o]; + if ((C += s.tShift[o]) > j) return !1; + if (42 !== (_ = s.src.charCodeAt(C++)) && 45 !== _ && 95 !== _) return !1; + for (w = 1; C < j; ) { + if ((x = s.src.charCodeAt(C++)) !== _ && 32 !== x) return !1; + x === _ && w++; + } + return ( + !(w < 3) && + (u || + ((s.line = o + 1), + s.tokens.push({ type: 'hr', lines: [o, s.line], level: s.level })), + !0) + ); + }, + ['paragraph', 'blockquote', 'list'] + ], + [ + 'list', + function index_browser_list(s, o, i, u) { + var _, + w, + x, + C, + j, + L, + B, + $, + V, + U, + z, + Y, + Z, + ee, + ie, + ae, + le, + ce, + pe, + de, + fe, + ye = !0; + if (($ = skipOrderedListMarker(s, o)) >= 0) Y = !0; + else { + if (!(($ = skipBulletListMarker(s, o)) >= 0)) return !1; + Y = !1; + } + if (s.level >= s.options.maxNesting) return !1; + if (((z = s.src.charCodeAt($ - 1)), u)) return !0; + for ( + ee = s.tokens.length, + Y + ? ((B = s.bMarks[o] + s.tShift[o]), + (U = Number(s.src.substr(B, $ - B - 1))), + s.tokens.push({ + type: 'ordered_list_open', + order: U, + lines: (ae = [o, 0]), + level: s.level++ + })) + : s.tokens.push({ + type: 'bullet_list_open', + lines: (ae = [o, 0]), + level: s.level++ + }), + _ = o, + ie = !1, + ce = s.parser.ruler.getRules('list'); + !( + !(_ < i) || + ((V = (Z = s.skipSpaces($)) >= s.eMarks[_] ? 1 : Z - $) > 4 && (V = 1), + V < 1 && (V = 1), + (w = $ - s.bMarks[_] + V), + s.tokens.push({ type: 'list_item_open', lines: (le = [o, 0]), level: s.level++ }), + (C = s.blkIndent), + (j = s.tight), + (x = s.tShift[o]), + (L = s.parentType), + (s.tShift[o] = Z - s.bMarks[o]), + (s.blkIndent = w), + (s.tight = !0), + (s.parentType = 'list'), + s.parser.tokenize(s, o, i, !0), + (s.tight && !ie) || (ye = !1), + (ie = s.line - o > 1 && s.isEmpty(s.line - 1)), + (s.blkIndent = C), + (s.tShift[o] = x), + (s.tight = j), + (s.parentType = L), + s.tokens.push({ type: 'list_item_close', level: --s.level }), + (_ = o = s.line), + (le[1] = _), + (Z = s.bMarks[o]), + _ >= i) || + s.isEmpty(_) || + s.tShift[_] < s.blkIndent + ); + ) { + for (fe = !1, pe = 0, de = ce.length; pe < de; pe++) + if (ce[pe](s, _, i, !0)) { + fe = !0; + break; + } + if (fe) break; + if (Y) { + if (($ = skipOrderedListMarker(s, _)) < 0) break; + } else if (($ = skipBulletListMarker(s, _)) < 0) break; + if (z !== s.src.charCodeAt($ - 1)) break; + } + return ( + s.tokens.push({ + type: Y ? 'ordered_list_close' : 'bullet_list_close', + level: --s.level + }), + (ae[1] = _), + (s.line = _), + ye && + (function markTightParagraphs(s, o) { + var i, + u, + _ = s.level + 2; + for (i = o + 2, u = s.tokens.length - 2; i < u; i++) + s.tokens[i].level === _ && + 'paragraph_open' === s.tokens[i].type && + ((s.tokens[i + 2].tight = !0), (s.tokens[i].tight = !0), (i += 2)); + })(s, ee), + !0 + ); + }, + ['paragraph', 'blockquote'] + ], + [ + 'footnote', + function footnote(s, o, i, u) { + var _, + w, + x, + C, + j, + L = s.bMarks[o] + s.tShift[o], + B = s.eMarks[o]; + if (L + 4 > B) return !1; + if (91 !== s.src.charCodeAt(L)) return !1; + if (94 !== s.src.charCodeAt(L + 1)) return !1; + if (s.level >= s.options.maxNesting) return !1; + for (C = L + 2; C < B; C++) { + if (32 === s.src.charCodeAt(C)) return !1; + if (93 === s.src.charCodeAt(C)) break; + } + return ( + C !== L + 2 && + !(C + 1 >= B || 58 !== s.src.charCodeAt(++C)) && + (u || + (C++, + s.env.footnotes || (s.env.footnotes = {}), + s.env.footnotes.refs || (s.env.footnotes.refs = {}), + (j = s.src.slice(L + 2, C - 2)), + (s.env.footnotes.refs[':' + j] = -1), + s.tokens.push({ type: 'footnote_reference_open', label: j, level: s.level++ }), + (_ = s.bMarks[o]), + (w = s.tShift[o]), + (x = s.parentType), + (s.tShift[o] = s.skipSpaces(C) - C), + (s.bMarks[o] = C), + (s.blkIndent += 4), + (s.parentType = 'footnote'), + s.tShift[o] < s.blkIndent && + ((s.tShift[o] += s.blkIndent), (s.bMarks[o] -= s.blkIndent)), + s.parser.tokenize(s, o, i, !0), + (s.parentType = x), + (s.blkIndent -= 4), + (s.tShift[o] = w), + (s.bMarks[o] = _), + s.tokens.push({ type: 'footnote_reference_close', level: --s.level })), + !0) + ); + }, + ['paragraph'] + ], + [ + 'heading', + function heading(s, o, i, u) { + var _, + w, + x, + C = s.bMarks[o] + s.tShift[o], + j = s.eMarks[o]; + if (C >= j) return !1; + if (35 !== (_ = s.src.charCodeAt(C)) || C >= j) return !1; + for (w = 1, _ = s.src.charCodeAt(++C); 35 === _ && C < j && w <= 6; ) + (w++, (_ = s.src.charCodeAt(++C))); + return ( + !(w > 6 || (C < j && 32 !== _)) && + (u || + ((j = s.skipCharsBack(j, 32, C)), + (x = s.skipCharsBack(j, 35, C)) > C && 32 === s.src.charCodeAt(x - 1) && (j = x), + (s.line = o + 1), + s.tokens.push({ + type: 'heading_open', + hLevel: w, + lines: [o, s.line], + level: s.level + }), + C < j && + s.tokens.push({ + type: 'inline', + content: s.src.slice(C, j).trim(), + level: s.level + 1, + lines: [o, s.line], + children: [] + }), + s.tokens.push({ type: 'heading_close', hLevel: w, level: s.level })), + !0) + ); + }, + ['paragraph', 'blockquote'] + ], + [ + 'lheading', + function lheading(s, o, i) { + var u, + _, + w, + x = o + 1; + return ( + !(x >= i) && + !(s.tShift[x] < s.blkIndent) && + !(s.tShift[x] - s.blkIndent > 3) && + !((_ = s.bMarks[x] + s.tShift[x]) >= (w = s.eMarks[x])) && + (45 === (u = s.src.charCodeAt(_)) || 61 === u) && + ((_ = s.skipChars(_, u)), + !((_ = s.skipSpaces(_)) < w) && + ((_ = s.bMarks[o] + s.tShift[o]), + (s.line = x + 1), + s.tokens.push({ + type: 'heading_open', + hLevel: 61 === u ? 1 : 2, + lines: [o, s.line], + level: s.level + }), + s.tokens.push({ + type: 'inline', + content: s.src.slice(_, s.eMarks[o]).trim(), + level: s.level + 1, + lines: [o, s.line - 1], + children: [] + }), + s.tokens.push({ + type: 'heading_close', + hLevel: 61 === u ? 1 : 2, + level: s.level + }), + !0)) + ); + } + ], + [ + 'htmlblock', + function htmlblock(s, o, i, u) { + var _, + w, + x, + C = s.bMarks[o], + j = s.eMarks[o], + L = s.tShift[o]; + if (((C += L), !s.options.html)) return !1; + if (L > 3 || C + 2 >= j) return !1; + if (60 !== s.src.charCodeAt(C)) return !1; + if (33 === (_ = s.src.charCodeAt(C + 1)) || 63 === _) { + if (u) return !0; + } else { + if ( + 47 !== _ && + !(function isLetter$1(s) { + var o = 32 | s; + return o >= 97 && o <= 122; + })(_) + ) + return !1; + if (47 === _) { + if (!(w = s.src.slice(C, j).match(EC))) return !1; + } else if (!(w = s.src.slice(C, j).match(_C))) return !1; + if (!0 !== bC[w[1].toLowerCase()]) return !1; + if (u) return !0; + } + for (x = o + 1; x < s.lineMax && !s.isEmpty(x); ) x++; + return ( + (s.line = x), + s.tokens.push({ + type: 'htmlblock', + level: s.level, + lines: [o, s.line], + content: s.getLines(o, x, 0, !0) + }), + !0 + ); + }, + ['paragraph', 'blockquote'] + ], + [ + 'table', + function table(s, o, i, u) { + var _, w, x, C, j, L, B, $, V, U, z; + if (o + 2 > i) return !1; + if (((j = o + 1), s.tShift[j] < s.blkIndent)) return !1; + if ((x = s.bMarks[j] + s.tShift[j]) >= s.eMarks[j]) return !1; + if (124 !== (_ = s.src.charCodeAt(x)) && 45 !== _ && 58 !== _) return !1; + if (((w = index_browser_getLine(s, o + 1)), !/^[-:| ]+$/.test(w))) return !1; + if ((L = w.split('|')) <= 2) return !1; + for ($ = [], C = 0; C < L.length; C++) { + if (!(V = L[C].trim())) { + if (0 === C || C === L.length - 1) continue; + return !1; + } + if (!/^:?-+:?$/.test(V)) return !1; + 58 === V.charCodeAt(V.length - 1) + ? $.push(58 === V.charCodeAt(0) ? 'center' : 'right') + : 58 === V.charCodeAt(0) + ? $.push('left') + : $.push(''); + } + if (-1 === (w = index_browser_getLine(s, o).trim()).indexOf('|')) return !1; + if (((L = w.replace(/^\||\|$/g, '').split('|')), $.length !== L.length)) return !1; + if (u) return !0; + for ( + s.tokens.push({ type: 'table_open', lines: (U = [o, 0]), level: s.level++ }), + s.tokens.push({ type: 'thead_open', lines: [o, o + 1], level: s.level++ }), + s.tokens.push({ type: 'tr_open', lines: [o, o + 1], level: s.level++ }), + C = 0; + C < L.length; + C++ + ) + (s.tokens.push({ + type: 'th_open', + align: $[C], + lines: [o, o + 1], + level: s.level++ + }), + s.tokens.push({ + type: 'inline', + content: L[C].trim(), + lines: [o, o + 1], + level: s.level, + children: [] + }), + s.tokens.push({ type: 'th_close', level: --s.level })); + for ( + s.tokens.push({ type: 'tr_close', level: --s.level }), + s.tokens.push({ type: 'thead_close', level: --s.level }), + s.tokens.push({ type: 'tbody_open', lines: (z = [o + 2, 0]), level: s.level++ }), + j = o + 2; + j < i && + !(s.tShift[j] < s.blkIndent) && + -1 !== (w = index_browser_getLine(s, j).trim()).indexOf('|'); + j++ + ) { + for ( + L = w.replace(/^\||\|$/g, '').split('|'), + s.tokens.push({ type: 'tr_open', level: s.level++ }), + C = 0; + C < L.length; + C++ + ) + (s.tokens.push({ type: 'td_open', align: $[C], level: s.level++ }), + (B = L[C].substring( + 124 === L[C].charCodeAt(0) ? 1 : 0, + 124 === L[C].charCodeAt(L[C].length - 1) ? L[C].length - 1 : L[C].length + ).trim()), + s.tokens.push({ type: 'inline', content: B, level: s.level, children: [] }), + s.tokens.push({ type: 'td_close', level: --s.level })); + s.tokens.push({ type: 'tr_close', level: --s.level }); + } + return ( + s.tokens.push({ type: 'tbody_close', level: --s.level }), + s.tokens.push({ type: 'table_close', level: --s.level }), + (U[1] = z[1] = j), + (s.line = j), + !0 + ); + }, + ['paragraph'] + ], + [ + 'deflist', + function deflist(s, o, i, u) { + var _, w, x, C, j, L, B, $, V, U, z, Y, Z, ee; + if (u) return !(s.ddIndent < 0) && skipMarker(s, o) >= 0; + if (((B = o + 1), s.isEmpty(B) && ++B > i)) return !1; + if (s.tShift[B] < s.blkIndent) return !1; + if ((_ = skipMarker(s, B)) < 0) return !1; + if (s.level >= s.options.maxNesting) return !1; + ((L = s.tokens.length), + s.tokens.push({ type: 'dl_open', lines: (j = [o, 0]), level: s.level++ }), + (x = o), + (w = B)); + e: for (;;) { + for ( + ee = !0, + Z = !1, + s.tokens.push({ type: 'dt_open', lines: [x, x], level: s.level++ }), + s.tokens.push({ + type: 'inline', + content: s.getLines(x, x + 1, s.blkIndent, !1).trim(), + level: s.level + 1, + lines: [x, x], + children: [] + }), + s.tokens.push({ type: 'dt_close', level: --s.level }); + ; + ) { + if ( + (s.tokens.push({ type: 'dd_open', lines: (C = [B, 0]), level: s.level++ }), + (Y = s.tight), + (V = s.ddIndent), + ($ = s.blkIndent), + (z = s.tShift[w]), + (U = s.parentType), + (s.blkIndent = s.ddIndent = s.tShift[w] + 2), + (s.tShift[w] = _ - s.bMarks[w]), + (s.tight = !0), + (s.parentType = 'deflist'), + s.parser.tokenize(s, w, i, !0), + (s.tight && !Z) || (ee = !1), + (Z = s.line - w > 1 && s.isEmpty(s.line - 1)), + (s.tShift[w] = z), + (s.tight = Y), + (s.parentType = U), + (s.blkIndent = $), + (s.ddIndent = V), + s.tokens.push({ type: 'dd_close', level: --s.level }), + (C[1] = B = s.line), + B >= i) + ) + break e; + if (s.tShift[B] < s.blkIndent) break e; + if ((_ = skipMarker(s, B)) < 0) break; + w = B; + } + if (B >= i) break; + if (((x = B), s.isEmpty(x))) break; + if (s.tShift[x] < s.blkIndent) break; + if ((w = x + 1) >= i) break; + if ((s.isEmpty(w) && w++, w >= i)) break; + if (s.tShift[w] < s.blkIndent) break; + if ((_ = skipMarker(s, w)) < 0) break; + } + return ( + s.tokens.push({ type: 'dl_close', level: --s.level }), + (j[1] = B), + (s.line = B), + ee && + (function markTightParagraphs$1(s, o) { + var i, + u, + _ = s.level + 2; + for (i = o + 2, u = s.tokens.length - 2; i < u; i++) + s.tokens[i].level === _ && + 'paragraph_open' === s.tokens[i].type && + ((s.tokens[i + 2].tight = !0), (s.tokens[i].tight = !0), (i += 2)); + })(s, L), + !0 + ); + }, + ['paragraph'] + ], + [ + 'paragraph', + function paragraph(s, o) { + var i, + u, + _, + w, + x, + C, + j = o + 1; + if (j < (i = s.lineMax) && !s.isEmpty(j)) + for (C = s.parser.ruler.getRules('paragraph'); j < i && !s.isEmpty(j); j++) + if (!(s.tShift[j] - s.blkIndent > 3)) { + for (_ = !1, w = 0, x = C.length; w < x; w++) + if (C[w](s, j, i, !0)) { + _ = !0; + break; + } + if (_) break; + } + return ( + (u = s.getLines(o, j, s.blkIndent, !1).trim()), + (s.line = j), + u.length && + (s.tokens.push({ + type: 'paragraph_open', + tight: !1, + lines: [o, s.line], + level: s.level + }), + s.tokens.push({ + type: 'inline', + content: u, + level: s.level + 1, + lines: [o, s.line], + children: [] + }), + s.tokens.push({ type: 'paragraph_close', tight: !1, level: s.level })), + !0 + ); + } + ] + ]; + function ParserBlock() { + this.ruler = new Ruler(); + for (var s = 0; s < wC.length; s++) + this.ruler.push(wC[s][0], wC[s][1], { alt: (wC[s][2] || []).slice() }); + } + ParserBlock.prototype.tokenize = function (s, o, i) { + for ( + var u, _ = this.ruler.getRules(''), w = _.length, x = o, C = !1; + x < i && + ((s.line = x = s.skipEmptyLines(x)), !(x >= i)) && + !(s.tShift[x] < s.blkIndent); + ) { + for (u = 0; u < w && !_[u](s, x, i, !1); u++); + if ( + ((s.tight = !C), s.isEmpty(s.line - 1) && (C = !0), (x = s.line) < i && s.isEmpty(x)) + ) { + if (((C = !0), ++x < i && 'list' === s.parentType && s.isEmpty(x))) break; + s.line = x; + } + } + }; + var SC = /[\n\t]/g, + xC = /\r[\n\u0085]|[\u2424\u2028\u0085]/g, + kC = /\u00a0/g; + function isTerminatorChar(s) { + switch (s) { + case 10: + case 92: + case 96: + case 42: + case 95: + case 94: + case 91: + case 93: + case 33: + case 38: + case 60: + case 62: + case 123: + case 125: + case 36: + case 37: + case 64: + case 126: + case 43: + case 61: + case 58: + return !0; + default: + return !1; + } + } + ParserBlock.prototype.parse = function (s, o, i, u) { + var _, + w = 0, + x = 0; + if (!s) return []; + ((s = (s = s.replace(kC, ' ')).replace(xC, '\n')).indexOf('\t') >= 0 && + (s = s.replace(SC, function (o, i) { + var u; + return 10 === s.charCodeAt(i) + ? ((w = i + 1), (x = 0), o) + : ((u = ' '.slice((i - w - x) % 4)), (x = i - w + 1), u); + })), + (_ = new StateBlock(s, this, o, i, u)), + this.tokenize(_, _.line, _.lineMax)); + }; + for (var CC = [], OC = 0; OC < 256; OC++) CC.push(0); + function isAlphaNum(s) { + return (s >= 48 && s <= 57) || (s >= 65 && s <= 90) || (s >= 97 && s <= 122); + } + function scanDelims(s, o) { + var i, + u, + _, + w = o, + x = !0, + C = !0, + j = s.posMax, + L = s.src.charCodeAt(o); + for (i = o > 0 ? s.src.charCodeAt(o - 1) : -1; w < j && s.src.charCodeAt(w) === L; ) w++; + return ( + w >= j && (x = !1), + (_ = w - o) >= 4 + ? (x = C = !1) + : ((32 !== (u = w < j ? s.src.charCodeAt(w) : -1) && 10 !== u) || (x = !1), + (32 !== i && 10 !== i) || (C = !1), + 95 === L && (isAlphaNum(i) && (x = !1), isAlphaNum(u) && (C = !1))), + { can_open: x, can_close: C, delims: _ } + ); + } + '\\!"#$%&\'()*+,./:;<=>?@[]^_`{|}~-'.split('').forEach(function (s) { + CC[s.charCodeAt(0)] = 1; + }); + var AC = /\\([ \\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g; + var jC = /\\([ \\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g; + var IC = [ + 'coap', + 'doi', + 'javascript', + 'aaa', + 'aaas', + 'about', + 'acap', + 'cap', + 'cid', + 'crid', + 'data', + 'dav', + 'dict', + 'dns', + 'file', + 'ftp', + 'geo', + 'go', + 'gopher', + 'h323', + 'http', + 'https', + 'iax', + 'icap', + 'im', + 'imap', + 'info', + 'ipp', + 'iris', + 'iris.beep', + 'iris.xpc', + 'iris.xpcs', + 'iris.lwz', + 'ldap', + 'mailto', + 'mid', + 'msrp', + 'msrps', + 'mtqp', + 'mupdate', + 'news', + 'nfs', + 'ni', + 'nih', + 'nntp', + 'opaquelocktoken', + 'pop', + 'pres', + 'rtsp', + 'service', + 'session', + 'shttp', + 'sieve', + 'sip', + 'sips', + 'sms', + 'snmp', + 'soap.beep', + 'soap.beeps', + 'tag', + 'tel', + 'telnet', + 'tftp', + 'thismessage', + 'tn3270', + 'tip', + 'tv', + 'urn', + 'vemmi', + 'ws', + 'wss', + 'xcon', + 'xcon-userid', + 'xmlrpc.beep', + 'xmlrpc.beeps', + 'xmpp', + 'z39.50r', + 'z39.50s', + 'adiumxtra', + 'afp', + 'afs', + 'aim', + 'apt', + 'attachment', + 'aw', + 'beshare', + 'bitcoin', + 'bolo', + 'callto', + 'chrome', + 'chrome-extension', + 'com-eventbrite-attendee', + 'content', + 'cvs', + 'dlna-playsingle', + 'dlna-playcontainer', + 'dtn', + 'dvb', + 'ed2k', + 'facetime', + 'feed', + 'finger', + 'fish', + 'gg', + 'git', + 'gizmoproject', + 'gtalk', + 'hcp', + 'icon', + 'ipn', + 'irc', + 'irc6', + 'ircs', + 'itms', + 'jar', + 'jms', + 'keyparc', + 'lastfm', + 'ldaps', + 'magnet', + 'maps', + 'market', + 'message', + 'mms', + 'ms-help', + 'msnim', + 'mumble', + 'mvn', + 'notes', + 'oid', + 'palm', + 'paparazzi', + 'platform', + 'proxy', + 'psyc', + 'query', + 'res', + 'resource', + 'rmi', + 'rsync', + 'rtmp', + 'secondlife', + 'sftp', + 'sgn', + 'skype', + 'smb', + 'soldat', + 'spotify', + 'ssh', + 'steam', + 'svn', + 'teamspeak', + 'things', + 'udp', + 'unreal', + 'ut2004', + 'ventrilo', + 'view-source', + 'webcal', + 'wtai', + 'wyciwyg', + 'xfire', + 'xri', + 'ymsgr' + ], + PC = + /^<([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/, + MC = /^<([a-zA-Z.\-]{1,25}):([^<>\x00-\x20]*)>/; + function replace$1(s, o) { + return ( + (s = s.source), + (o = o || ''), + function self(i, u) { + return i ? ((u = u.source || u), (s = s.replace(i, u)), self) : new RegExp(s, o); + } + ); + } + var TC = replace$1(/(?:unquoted|single_quoted|double_quoted)/)( + 'unquoted', + /[^"'=<>`\x00-\x20]+/ + )('single_quoted', /'[^']*'/)('double_quoted', /"[^"]*"/)(), + NC = replace$1(/(?:\s+attr_name(?:\s*=\s*attr_value)?)/)( + 'attr_name', + /[a-zA-Z_:][a-zA-Z0-9:._-]*/ + )('attr_value', TC)(), + RC = replace$1(/<[A-Za-z][A-Za-z0-9]*attribute*\s*\/?>/)('attribute', NC)(), + DC = replace$1(/^(?:open_tag|close_tag|comment|processing|declaration|cdata)/)( + 'open_tag', + RC + )('close_tag', /<\/[A-Za-z][A-Za-z0-9]*\s*>/)( + 'comment', + /|/ + )('processing', /<[?].*?[?]>/)('declaration', /]*>/)( + 'cdata', + // + )(); + var LC = /^&#((?:x[a-f0-9]{1,8}|[0-9]{1,8}));/i, + BC = /^&([a-z][a-z0-9]{1,31});/i; + var FC = [ + [ + 'text', + function index_browser_text(s, o) { + for (var i = s.pos; i < s.posMax && !isTerminatorChar(s.src.charCodeAt(i)); ) i++; + return i !== s.pos && (o || (s.pending += s.src.slice(s.pos, i)), (s.pos = i), !0); + } + ], + [ + 'newline', + function newline(s, o) { + var i, + u, + _ = s.pos; + if (10 !== s.src.charCodeAt(_)) return !1; + if (((i = s.pending.length - 1), (u = s.posMax), !o)) + if (i >= 0 && 32 === s.pending.charCodeAt(i)) + if (i >= 1 && 32 === s.pending.charCodeAt(i - 1)) { + for (var w = i - 2; w >= 0; w--) + if (32 !== s.pending.charCodeAt(w)) { + s.pending = s.pending.substring(0, w + 1); + break; + } + s.push({ type: 'hardbreak', level: s.level }); + } else + ((s.pending = s.pending.slice(0, -1)), + s.push({ type: 'softbreak', level: s.level })); + else s.push({ type: 'softbreak', level: s.level }); + for (_++; _ < u && 32 === s.src.charCodeAt(_); ) _++; + return ((s.pos = _), !0); + } + ], + [ + 'escape', + function index_browser_escape(s, o) { + var i, + u = s.pos, + _ = s.posMax; + if (92 !== s.src.charCodeAt(u)) return !1; + if (++u < _) { + if ((i = s.src.charCodeAt(u)) < 256 && 0 !== CC[i]) + return (o || (s.pending += s.src[u]), (s.pos += 2), !0); + if (10 === i) { + for ( + o || s.push({ type: 'hardbreak', level: s.level }), u++; + u < _ && 32 === s.src.charCodeAt(u); + ) + u++; + return ((s.pos = u), !0); + } + } + return (o || (s.pending += '\\'), s.pos++, !0); + } + ], + [ + 'backticks', + function backticks(s, o) { + var i, + u, + _, + w, + x, + C = s.pos; + if (96 !== s.src.charCodeAt(C)) return !1; + for (i = C, C++, u = s.posMax; C < u && 96 === s.src.charCodeAt(C); ) C++; + for (_ = s.src.slice(i, C), w = x = C; -1 !== (w = s.src.indexOf('`', x)); ) { + for (x = w + 1; x < u && 96 === s.src.charCodeAt(x); ) x++; + if (x - w === _.length) + return ( + o || + s.push({ + type: 'code', + content: s.src + .slice(C, w) + .replace(/[ \n]+/g, ' ') + .trim(), + block: !1, + level: s.level + }), + (s.pos = x), + !0 + ); + } + return (o || (s.pending += _), (s.pos += _.length), !0); + } + ], + [ + 'del', + function del(s, o) { + var i, + u, + _, + w, + x, + C = s.posMax, + j = s.pos; + if (126 !== s.src.charCodeAt(j)) return !1; + if (o) return !1; + if (j + 4 >= C) return !1; + if (126 !== s.src.charCodeAt(j + 1)) return !1; + if (s.level >= s.options.maxNesting) return !1; + if ( + ((w = j > 0 ? s.src.charCodeAt(j - 1) : -1), + (x = s.src.charCodeAt(j + 2)), + 126 === w) + ) + return !1; + if (126 === x) return !1; + if (32 === x || 10 === x) return !1; + for (u = j + 2; u < C && 126 === s.src.charCodeAt(u); ) u++; + if (u > j + 3) return ((s.pos += u - j), o || (s.pending += s.src.slice(j, u)), !0); + for (s.pos = j + 2, _ = 1; s.pos + 1 < C; ) { + if ( + 126 === s.src.charCodeAt(s.pos) && + 126 === s.src.charCodeAt(s.pos + 1) && + ((w = s.src.charCodeAt(s.pos - 1)), + 126 !== (x = s.pos + 2 < C ? s.src.charCodeAt(s.pos + 2) : -1) && + 126 !== w && + (32 !== w && 10 !== w ? _-- : 32 !== x && 10 !== x && _++, _ <= 0)) + ) { + i = !0; + break; + } + s.parser.skipToken(s); + } + return i + ? ((s.posMax = s.pos), + (s.pos = j + 2), + o || + (s.push({ type: 'del_open', level: s.level++ }), + s.parser.tokenize(s), + s.push({ type: 'del_close', level: --s.level })), + (s.pos = s.posMax + 2), + (s.posMax = C), + !0) + : ((s.pos = j), !1); + } + ], + [ + 'ins', + function ins(s, o) { + var i, + u, + _, + w, + x, + C = s.posMax, + j = s.pos; + if (43 !== s.src.charCodeAt(j)) return !1; + if (o) return !1; + if (j + 4 >= C) return !1; + if (43 !== s.src.charCodeAt(j + 1)) return !1; + if (s.level >= s.options.maxNesting) return !1; + if ( + ((w = j > 0 ? s.src.charCodeAt(j - 1) : -1), + (x = s.src.charCodeAt(j + 2)), + 43 === w) + ) + return !1; + if (43 === x) return !1; + if (32 === x || 10 === x) return !1; + for (u = j + 2; u < C && 43 === s.src.charCodeAt(u); ) u++; + if (u !== j + 2) return ((s.pos += u - j), o || (s.pending += s.src.slice(j, u)), !0); + for (s.pos = j + 2, _ = 1; s.pos + 1 < C; ) { + if ( + 43 === s.src.charCodeAt(s.pos) && + 43 === s.src.charCodeAt(s.pos + 1) && + ((w = s.src.charCodeAt(s.pos - 1)), + 43 !== (x = s.pos + 2 < C ? s.src.charCodeAt(s.pos + 2) : -1) && + 43 !== w && + (32 !== w && 10 !== w ? _-- : 32 !== x && 10 !== x && _++, _ <= 0)) + ) { + i = !0; + break; + } + s.parser.skipToken(s); + } + return i + ? ((s.posMax = s.pos), + (s.pos = j + 2), + o || + (s.push({ type: 'ins_open', level: s.level++ }), + s.parser.tokenize(s), + s.push({ type: 'ins_close', level: --s.level })), + (s.pos = s.posMax + 2), + (s.posMax = C), + !0) + : ((s.pos = j), !1); + } + ], + [ + 'mark', + function mark(s, o) { + var i, + u, + _, + w, + x, + C = s.posMax, + j = s.pos; + if (61 !== s.src.charCodeAt(j)) return !1; + if (o) return !1; + if (j + 4 >= C) return !1; + if (61 !== s.src.charCodeAt(j + 1)) return !1; + if (s.level >= s.options.maxNesting) return !1; + if ( + ((w = j > 0 ? s.src.charCodeAt(j - 1) : -1), + (x = s.src.charCodeAt(j + 2)), + 61 === w) + ) + return !1; + if (61 === x) return !1; + if (32 === x || 10 === x) return !1; + for (u = j + 2; u < C && 61 === s.src.charCodeAt(u); ) u++; + if (u !== j + 2) return ((s.pos += u - j), o || (s.pending += s.src.slice(j, u)), !0); + for (s.pos = j + 2, _ = 1; s.pos + 1 < C; ) { + if ( + 61 === s.src.charCodeAt(s.pos) && + 61 === s.src.charCodeAt(s.pos + 1) && + ((w = s.src.charCodeAt(s.pos - 1)), + 61 !== (x = s.pos + 2 < C ? s.src.charCodeAt(s.pos + 2) : -1) && + 61 !== w && + (32 !== w && 10 !== w ? _-- : 32 !== x && 10 !== x && _++, _ <= 0)) + ) { + i = !0; + break; + } + s.parser.skipToken(s); + } + return i + ? ((s.posMax = s.pos), + (s.pos = j + 2), + o || + (s.push({ type: 'mark_open', level: s.level++ }), + s.parser.tokenize(s), + s.push({ type: 'mark_close', level: --s.level })), + (s.pos = s.posMax + 2), + (s.posMax = C), + !0) + : ((s.pos = j), !1); + } + ], + [ + 'emphasis', + function emphasis(s, o) { + var i, + u, + _, + w, + x, + C, + j, + L = s.posMax, + B = s.pos, + $ = s.src.charCodeAt(B); + if (95 !== $ && 42 !== $) return !1; + if (o) return !1; + if (((i = (j = scanDelims(s, B)).delims), !j.can_open)) + return ((s.pos += i), o || (s.pending += s.src.slice(B, s.pos)), !0); + if (s.level >= s.options.maxNesting) return !1; + for (s.pos = B + i, C = [i]; s.pos < L; ) + if (s.src.charCodeAt(s.pos) !== $) s.parser.skipToken(s); + else { + if (((u = (j = scanDelims(s, s.pos)).delims), j.can_close)) { + for (w = C.pop(), x = u; w !== x; ) { + if (x < w) { + C.push(w - x); + break; + } + if (((x -= w), 0 === C.length)) break; + ((s.pos += w), (w = C.pop())); + } + if (0 === C.length) { + ((i = w), (_ = !0)); + break; + } + s.pos += u; + continue; + } + (j.can_open && C.push(u), (s.pos += u)); + } + return _ + ? ((s.posMax = s.pos), + (s.pos = B + i), + o || + ((2 !== i && 3 !== i) || s.push({ type: 'strong_open', level: s.level++ }), + (1 !== i && 3 !== i) || s.push({ type: 'em_open', level: s.level++ }), + s.parser.tokenize(s), + (1 !== i && 3 !== i) || s.push({ type: 'em_close', level: --s.level }), + (2 !== i && 3 !== i) || s.push({ type: 'strong_close', level: --s.level })), + (s.pos = s.posMax + i), + (s.posMax = L), + !0) + : ((s.pos = B), !1); + } + ], + [ + 'sub', + function sub(s, o) { + var i, + u, + _ = s.posMax, + w = s.pos; + if (126 !== s.src.charCodeAt(w)) return !1; + if (o) return !1; + if (w + 2 >= _) return !1; + if (s.level >= s.options.maxNesting) return !1; + for (s.pos = w + 1; s.pos < _; ) { + if (126 === s.src.charCodeAt(s.pos)) { + i = !0; + break; + } + s.parser.skipToken(s); + } + return i && w + 1 !== s.pos + ? (u = s.src.slice(w + 1, s.pos)).match(/(^|[^\\])(\\\\)*\s/) + ? ((s.pos = w), !1) + : ((s.posMax = s.pos), + (s.pos = w + 1), + o || s.push({ type: 'sub', level: s.level, content: u.replace(AC, '$1') }), + (s.pos = s.posMax + 1), + (s.posMax = _), + !0) + : ((s.pos = w), !1); + } + ], + [ + 'sup', + function sup(s, o) { + var i, + u, + _ = s.posMax, + w = s.pos; + if (94 !== s.src.charCodeAt(w)) return !1; + if (o) return !1; + if (w + 2 >= _) return !1; + if (s.level >= s.options.maxNesting) return !1; + for (s.pos = w + 1; s.pos < _; ) { + if (94 === s.src.charCodeAt(s.pos)) { + i = !0; + break; + } + s.parser.skipToken(s); + } + return i && w + 1 !== s.pos + ? (u = s.src.slice(w + 1, s.pos)).match(/(^|[^\\])(\\\\)*\s/) + ? ((s.pos = w), !1) + : ((s.posMax = s.pos), + (s.pos = w + 1), + o || s.push({ type: 'sup', level: s.level, content: u.replace(jC, '$1') }), + (s.pos = s.posMax + 1), + (s.posMax = _), + !0) + : ((s.pos = w), !1); + } + ], + [ + 'links', + function links(s, o) { + var i, + u, + _, + w, + x, + C, + j, + L, + B = !1, + $ = s.pos, + V = s.posMax, + U = s.pos, + z = s.src.charCodeAt(U); + if ((33 === z && ((B = !0), (z = s.src.charCodeAt(++U))), 91 !== z)) return !1; + if (s.level >= s.options.maxNesting) return !1; + if (((i = U + 1), (u = parseLinkLabel(s, U)) < 0)) return !1; + if ((C = u + 1) < V && 40 === s.src.charCodeAt(C)) { + for (C++; C < V && (32 === (L = s.src.charCodeAt(C)) || 10 === L); C++); + if (C >= V) return !1; + for ( + U = C, + parseLinkDestination(s, C) ? ((w = s.linkContent), (C = s.pos)) : (w = ''), + U = C; + C < V && (32 === (L = s.src.charCodeAt(C)) || 10 === L); + C++ + ); + if (C < V && U !== C && parseLinkTitle(s, C)) + for ( + x = s.linkContent, C = s.pos; + C < V && (32 === (L = s.src.charCodeAt(C)) || 10 === L); + C++ + ); + else x = ''; + if (C >= V || 41 !== s.src.charCodeAt(C)) return ((s.pos = $), !1); + C++; + } else { + if (s.linkLevel > 0) return !1; + for (; C < V && (32 === (L = s.src.charCodeAt(C)) || 10 === L); C++); + if ( + (C < V && + 91 === s.src.charCodeAt(C) && + ((U = C + 1), + (C = parseLinkLabel(s, C)) >= 0 ? (_ = s.src.slice(U, C++)) : (C = U - 1)), + _ || (void 0 === _ && (C = u + 1), (_ = s.src.slice(i, u))), + !(j = s.env.references[normalizeReference(_)])) + ) + return ((s.pos = $), !1); + ((w = j.href), (x = j.title)); + } + return ( + o || + ((s.pos = i), + (s.posMax = u), + B + ? s.push({ + type: 'image', + src: w, + title: x, + alt: s.src.substr(i, u - i), + level: s.level + }) + : (s.push({ type: 'link_open', href: w, title: x, level: s.level++ }), + s.linkLevel++, + s.parser.tokenize(s), + s.linkLevel--, + s.push({ type: 'link_close', level: --s.level }))), + (s.pos = C), + (s.posMax = V), + !0 + ); + } + ], + [ + 'footnote_inline', + function footnote_inline(s, o) { + var i, + u, + _, + w, + x = s.posMax, + C = s.pos; + return ( + !(C + 2 >= x) && + 94 === s.src.charCodeAt(C) && + 91 === s.src.charCodeAt(C + 1) && + !(s.level >= s.options.maxNesting) && + ((i = C + 2), + !((u = parseLinkLabel(s, C + 1)) < 0) && + (o || + (s.env.footnotes || (s.env.footnotes = {}), + s.env.footnotes.list || (s.env.footnotes.list = []), + (_ = s.env.footnotes.list.length), + (s.pos = i), + (s.posMax = u), + s.push({ type: 'footnote_ref', id: _, level: s.level }), + s.linkLevel++, + (w = s.tokens.length), + s.parser.tokenize(s), + (s.env.footnotes.list[_] = { tokens: s.tokens.splice(w) }), + s.linkLevel--), + (s.pos = u + 1), + (s.posMax = x), + !0)) + ); + } + ], + [ + 'footnote_ref', + function footnote_ref(s, o) { + var i, + u, + _, + w, + x = s.posMax, + C = s.pos; + if (C + 3 > x) return !1; + if (!s.env.footnotes || !s.env.footnotes.refs) return !1; + if (91 !== s.src.charCodeAt(C)) return !1; + if (94 !== s.src.charCodeAt(C + 1)) return !1; + if (s.level >= s.options.maxNesting) return !1; + for (u = C + 2; u < x; u++) { + if (32 === s.src.charCodeAt(u)) return !1; + if (10 === s.src.charCodeAt(u)) return !1; + if (93 === s.src.charCodeAt(u)) break; + } + return ( + u !== C + 2 && + !(u >= x) && + (u++, + (i = s.src.slice(C + 2, u - 1)), + void 0 !== s.env.footnotes.refs[':' + i] && + (o || + (s.env.footnotes.list || (s.env.footnotes.list = []), + s.env.footnotes.refs[':' + i] < 0 + ? ((_ = s.env.footnotes.list.length), + (s.env.footnotes.list[_] = { label: i, count: 0 }), + (s.env.footnotes.refs[':' + i] = _)) + : (_ = s.env.footnotes.refs[':' + i]), + (w = s.env.footnotes.list[_].count), + s.env.footnotes.list[_].count++, + s.push({ type: 'footnote_ref', id: _, subId: w, level: s.level })), + (s.pos = u), + (s.posMax = x), + !0)) + ); + } + ], + [ + 'autolink', + function autolink(s, o) { + var i, + u, + _, + w, + x, + C = s.pos; + return ( + 60 === s.src.charCodeAt(C) && + !((i = s.src.slice(C)).indexOf('>') < 0) && + ((u = i.match(MC)) + ? !(IC.indexOf(u[1].toLowerCase()) < 0) && + ((x = normalizeLink((w = u[0].slice(1, -1)))), + !!s.parser.validateLink(w) && + (o || + (s.push({ type: 'link_open', href: x, level: s.level }), + s.push({ type: 'text', content: w, level: s.level + 1 }), + s.push({ type: 'link_close', level: s.level })), + (s.pos += u[0].length), + !0)) + : !!(_ = i.match(PC)) && + ((x = normalizeLink('mailto:' + (w = _[0].slice(1, -1)))), + !!s.parser.validateLink(x) && + (o || + (s.push({ type: 'link_open', href: x, level: s.level }), + s.push({ type: 'text', content: w, level: s.level + 1 }), + s.push({ type: 'link_close', level: s.level })), + (s.pos += _[0].length), + !0))) + ); + } + ], + [ + 'htmltag', + function htmltag(s, o) { + var i, + u, + _, + w = s.pos; + return ( + !!s.options.html && + ((_ = s.posMax), + !(60 !== s.src.charCodeAt(w) || w + 2 >= _) && + !( + 33 !== (i = s.src.charCodeAt(w + 1)) && + 63 !== i && + 47 !== i && + !(function isLetter$2(s) { + var o = 32 | s; + return o >= 97 && o <= 122; + })(i) + ) && + !!(u = s.src.slice(w).match(DC)) && + (o || + s.push({ + type: 'htmltag', + content: s.src.slice(w, w + u[0].length), + level: s.level + }), + (s.pos += u[0].length), + !0)) + ); + } + ], + [ + 'entity', + function entity(s, o) { + var i, + u, + _ = s.pos, + w = s.posMax; + if (38 !== s.src.charCodeAt(_)) return !1; + if (_ + 1 < w) + if (35 === s.src.charCodeAt(_ + 1)) { + if ((u = s.src.slice(_).match(LC))) + return ( + o || + ((i = + 'x' === u[1][0].toLowerCase() + ? parseInt(u[1].slice(1), 16) + : parseInt(u[1], 10)), + (s.pending += isValidEntityCode(i) + ? fromCodePoint(i) + : fromCodePoint(65533))), + (s.pos += u[0].length), + !0 + ); + } else if ((u = s.src.slice(_).match(BC))) { + var x = decodeEntity(u[1]); + if (u[1] !== x) return (o || (s.pending += x), (s.pos += u[0].length), !0); + } + return (o || (s.pending += '&'), s.pos++, !0); + } + ] + ]; + function ParserInline() { + this.ruler = new Ruler(); + for (var s = 0; s < FC.length; s++) this.ruler.push(FC[s][0], FC[s][1]); + this.validateLink = validateLink; + } + function validateLink(s) { + var o = s.trim().toLowerCase(); + return ( + -1 === (o = replaceEntities(o)).indexOf(':') || + -1 === ['vbscript', 'javascript', 'file', 'data'].indexOf(o.split(':')[0]) + ); + } + ((ParserInline.prototype.skipToken = function (s) { + var o, + i, + u = this.ruler.getRules(''), + _ = u.length, + w = s.pos; + if ((i = s.cacheGet(w)) > 0) s.pos = i; + else { + for (o = 0; o < _; o++) if (u[o](s, !0)) return void s.cacheSet(w, s.pos); + (s.pos++, s.cacheSet(w, s.pos)); + } + }), + (ParserInline.prototype.tokenize = function (s) { + for (var o, i, u = this.ruler.getRules(''), _ = u.length, w = s.posMax; s.pos < w; ) { + for (i = 0; i < _ && !(o = u[i](s, !1)); i++); + if (o) { + if (s.pos >= w) break; + } else s.pending += s.src[s.pos++]; + } + s.pending && s.pushPending(); + }), + (ParserInline.prototype.parse = function (s, o, i, u) { + var _ = new StateInline(s, this, o, i, u); + this.tokenize(_); + })); + var qC = { + default: { + options: { + html: !1, + xhtmlOut: !1, + breaks: !1, + langPrefix: 'language-', + linkTarget: '', + typographer: !1, + quotes: '“”‘’', + highlight: null, + maxNesting: 20 + }, + components: { + core: { + rules: [ + 'block', + 'inline', + 'references', + 'replacements', + 'smartquotes', + 'references', + 'abbr2', + 'footnote_tail' + ] + }, + block: { + rules: [ + 'blockquote', + 'code', + 'fences', + 'footnote', + 'heading', + 'hr', + 'htmlblock', + 'lheading', + 'list', + 'paragraph', + 'table' + ] + }, + inline: { + rules: [ + 'autolink', + 'backticks', + 'del', + 'emphasis', + 'entity', + 'escape', + 'footnote_ref', + 'htmltag', + 'links', + 'newline', + 'text' + ] + } + } + }, + full: { + options: { + html: !1, + xhtmlOut: !1, + breaks: !1, + langPrefix: 'language-', + linkTarget: '', + typographer: !1, + quotes: '“”‘’', + highlight: null, + maxNesting: 20 + }, + components: { core: {}, block: {}, inline: {} } + }, + commonmark: { + options: { + html: !0, + xhtmlOut: !0, + breaks: !1, + langPrefix: 'language-', + linkTarget: '', + typographer: !1, + quotes: '“”‘’', + highlight: null, + maxNesting: 20 + }, + components: { + core: { rules: ['block', 'inline', 'references', 'abbr2'] }, + block: { + rules: [ + 'blockquote', + 'code', + 'fences', + 'heading', + 'hr', + 'htmlblock', + 'lheading', + 'list', + 'paragraph' + ] + }, + inline: { + rules: [ + 'autolink', + 'backticks', + 'emphasis', + 'entity', + 'escape', + 'htmltag', + 'links', + 'newline', + 'text' + ] + } + } + } + }; + function StateCore(s, o, i) { + ((this.src = o), + (this.env = i), + (this.options = s.options), + (this.tokens = []), + (this.inlineMode = !1), + (this.inline = s.inline), + (this.block = s.block), + (this.renderer = s.renderer), + (this.typographer = s.typographer)); + } + function Remarkable(s, o) { + ('string' != typeof s && ((o = s), (s = 'default')), + o && + null != o.linkify && + console.warn( + "linkify option is removed. Use linkify plugin instead:\n\nimport Remarkable from 'remarkable';\nimport linkify from 'remarkable/linkify';\nnew Remarkable().use(linkify)\n" + ), + (this.inline = new ParserInline()), + (this.block = new ParserBlock()), + (this.core = new Core()), + (this.renderer = new Renderer()), + (this.ruler = new Ruler()), + (this.options = {}), + this.configure(qC[s]), + this.set(o || {})); + } + ((Remarkable.prototype.set = function (s) { + index_browser_assign(this.options, s); + }), + (Remarkable.prototype.configure = function (s) { + var o = this; + if (!s) throw new Error('Wrong `remarkable` preset, check name/content'); + (s.options && o.set(s.options), + s.components && + Object.keys(s.components).forEach(function (i) { + s.components[i].rules && o[i].ruler.enable(s.components[i].rules, !0); + })); + }), + (Remarkable.prototype.use = function (s, o) { + return (s(this, o), this); + }), + (Remarkable.prototype.parse = function (s, o) { + var i = new StateCore(this, s, o); + return (this.core.process(i), i.tokens); + }), + (Remarkable.prototype.render = function (s, o) { + return ((o = o || {}), this.renderer.render(this.parse(s, o), this.options, o)); + }), + (Remarkable.prototype.parseInline = function (s, o) { + var i = new StateCore(this, s, o); + return ((i.inlineMode = !0), this.core.process(i), i.tokens); + }), + (Remarkable.prototype.renderInline = function (s, o) { + return ((o = o || {}), this.renderer.render(this.parseInline(s, o), this.options, o)); + })); + function indexOf(s, o) { + if (Array.prototype.indexOf) return s.indexOf(o); + for (var i = 0, u = s.length; i < u; i++) if (s[i] === o) return i; + return -1; + } + function utils_remove(s, o) { + for (var i = s.length - 1; i >= 0; i--) !0 === o(s[i]) && s.splice(i, 1); + } + function throwUnhandledCaseError(s) { + throw new Error("Unhandled case for value: '".concat(s, "'")); + } + var $C = (function () { + function HtmlTag(s) { + (void 0 === s && (s = {}), + (this.tagName = ''), + (this.attrs = {}), + (this.innerHTML = ''), + (this.whitespaceRegex = /\s+/), + (this.tagName = s.tagName || ''), + (this.attrs = s.attrs || {}), + (this.innerHTML = s.innerHtml || s.innerHTML || '')); + } + return ( + (HtmlTag.prototype.setTagName = function (s) { + return ((this.tagName = s), this); + }), + (HtmlTag.prototype.getTagName = function () { + return this.tagName || ''; + }), + (HtmlTag.prototype.setAttr = function (s, o) { + return ((this.getAttrs()[s] = o), this); + }), + (HtmlTag.prototype.getAttr = function (s) { + return this.getAttrs()[s]; + }), + (HtmlTag.prototype.setAttrs = function (s) { + return (Object.assign(this.getAttrs(), s), this); + }), + (HtmlTag.prototype.getAttrs = function () { + return this.attrs || (this.attrs = {}); + }), + (HtmlTag.prototype.setClass = function (s) { + return this.setAttr('class', s); + }), + (HtmlTag.prototype.addClass = function (s) { + for ( + var o, + i = this.getClass(), + u = this.whitespaceRegex, + _ = i ? i.split(u) : [], + w = s.split(u); + (o = w.shift()); + ) + -1 === indexOf(_, o) && _.push(o); + return ((this.getAttrs().class = _.join(' ')), this); + }), + (HtmlTag.prototype.removeClass = function (s) { + for ( + var o, + i = this.getClass(), + u = this.whitespaceRegex, + _ = i ? i.split(u) : [], + w = s.split(u); + _.length && (o = w.shift()); + ) { + var x = indexOf(_, o); + -1 !== x && _.splice(x, 1); + } + return ((this.getAttrs().class = _.join(' ')), this); + }), + (HtmlTag.prototype.getClass = function () { + return this.getAttrs().class || ''; + }), + (HtmlTag.prototype.hasClass = function (s) { + return -1 !== (' ' + this.getClass() + ' ').indexOf(' ' + s + ' '); + }), + (HtmlTag.prototype.setInnerHTML = function (s) { + return ((this.innerHTML = s), this); + }), + (HtmlTag.prototype.setInnerHtml = function (s) { + return this.setInnerHTML(s); + }), + (HtmlTag.prototype.getInnerHTML = function () { + return this.innerHTML || ''; + }), + (HtmlTag.prototype.getInnerHtml = function () { + return this.getInnerHTML(); + }), + (HtmlTag.prototype.toAnchorString = function () { + var s = this.getTagName(), + o = this.buildAttrsStr(); + return ['<', s, (o = o ? ' ' + o : ''), '>', this.getInnerHtml(), ''].join( + '' + ); + }), + (HtmlTag.prototype.buildAttrsStr = function () { + if (!this.attrs) return ''; + var s = this.getAttrs(), + o = []; + for (var i in s) s.hasOwnProperty(i) && o.push(i + '="' + s[i] + '"'); + return o.join(' '); + }), + HtmlTag + ); + })(); + var VC = (function () { + function AnchorTagBuilder(s) { + (void 0 === s && (s = {}), + (this.newWindow = !1), + (this.truncate = {}), + (this.className = ''), + (this.newWindow = s.newWindow || !1), + (this.truncate = s.truncate || {}), + (this.className = s.className || '')); + } + return ( + (AnchorTagBuilder.prototype.build = function (s) { + return new $C({ + tagName: 'a', + attrs: this.createAttrs(s), + innerHtml: this.processAnchorText(s.getAnchorText()) + }); + }), + (AnchorTagBuilder.prototype.createAttrs = function (s) { + var o = { href: s.getAnchorHref() }, + i = this.createCssClass(s); + return ( + i && (o.class = i), + this.newWindow && ((o.target = '_blank'), (o.rel = 'noopener noreferrer')), + this.truncate && + this.truncate.length && + this.truncate.length < s.getAnchorText().length && + (o.title = s.getAnchorHref()), + o + ); + }), + (AnchorTagBuilder.prototype.createCssClass = function (s) { + var o = this.className; + if (o) { + for (var i = [o], u = s.getCssClassSuffixes(), _ = 0, w = u.length; _ < w; _++) + i.push(o + '-' + u[_]); + return i.join(' '); + } + return ''; + }), + (AnchorTagBuilder.prototype.processAnchorText = function (s) { + return (s = this.doTruncate(s)); + }), + (AnchorTagBuilder.prototype.doTruncate = function (s) { + var o = this.truncate; + if (!o || !o.length) return s; + var i = o.length, + u = o.location; + return 'smart' === u + ? (function truncateSmart(s, o, i) { + var u, _; + null == i + ? ((i = '…'), (_ = 3), (u = 8)) + : ((_ = i.length), (u = i.length)); + var buildUrl = function (s) { + var o = ''; + return ( + s.scheme && s.host && (o += s.scheme + '://'), + s.host && (o += s.host), + s.path && (o += '/' + s.path), + s.query && (o += '?' + s.query), + s.fragment && (o += '#' + s.fragment), + o + ); + }, + buildSegment = function (s, o) { + var u = o / 2, + _ = Math.ceil(u), + w = -1 * Math.floor(u), + x = ''; + return (w < 0 && (x = s.substr(w)), s.substr(0, _) + i + x); + }; + if (s.length <= o) return s; + var w = o - _, + x = (function (s) { + var o = {}, + i = s, + u = i.match(/^([a-z]+):\/\//i); + return ( + u && ((o.scheme = u[1]), (i = i.substr(u[0].length))), + (u = i.match(/^(.*?)(?=(\?|#|\/|$))/i)) && + ((o.host = u[1]), (i = i.substr(u[0].length))), + (u = i.match(/^\/(.*?)(?=(\?|#|$))/i)) && + ((o.path = u[1]), (i = i.substr(u[0].length))), + (u = i.match(/^\?(.*?)(?=(#|$))/i)) && + ((o.query = u[1]), (i = i.substr(u[0].length))), + (u = i.match(/^#(.*?)$/i)) && (o.fragment = u[1]), + o + ); + })(s); + if (x.query) { + var C = x.query.match(/^(.*?)(?=(\?|\#))(.*?)$/i); + C && ((x.query = x.query.substr(0, C[1].length)), (s = buildUrl(x))); + } + if (s.length <= o) return s; + if ( + (x.host && ((x.host = x.host.replace(/^www\./, '')), (s = buildUrl(x))), + s.length <= o) + ) + return s; + var j = ''; + if ((x.host && (j += x.host), j.length >= w)) + return x.host.length == o + ? (x.host.substr(0, o - _) + i).substr(0, w + u) + : buildSegment(j, w).substr(0, w + u); + var L = ''; + if ((x.path && (L += '/' + x.path), x.query && (L += '?' + x.query), L)) { + if ((j + L).length >= w) + return (j + L).length == o + ? (j + L).substr(0, o) + : (j + buildSegment(L, w - j.length)).substr(0, w + u); + j += L; + } + if (x.fragment) { + var B = '#' + x.fragment; + if ((j + B).length >= w) + return (j + B).length == o + ? (j + B).substr(0, o) + : (j + buildSegment(B, w - j.length)).substr(0, w + u); + j += B; + } + if (x.scheme && x.host) { + var $ = x.scheme + '://'; + if ((j + $).length < w) return ($ + j).substr(0, o); + } + if (j.length <= o) return j; + var V = ''; + return ( + w > 0 && (V = j.substr(-1 * Math.floor(w / 2))), + (j.substr(0, Math.ceil(w / 2)) + i + V).substr(0, w + u) + ); + })(s, i) + : 'middle' === u + ? (function truncateMiddle(s, o, i) { + if (s.length <= o) return s; + var u, _; + null == i + ? ((i = '…'), (u = 8), (_ = 3)) + : ((u = i.length), (_ = i.length)); + var w = o - _, + x = ''; + return ( + w > 0 && (x = s.substr(-1 * Math.floor(w / 2))), + (s.substr(0, Math.ceil(w / 2)) + i + x).substr(0, w + u) + ); + })(s, i) + : (function truncateEnd(s, o, i) { + return (function ellipsis(s, o, i) { + var u; + return ( + s.length > o && + (null == i ? ((i = '…'), (u = 3)) : (u = i.length), + (s = s.substring(0, o - u) + i)), + s + ); + })(s, o, i); + })(s, i); + }), + AnchorTagBuilder + ); + })(), + UC = (function () { + function Match(s) { + ((this.__jsduckDummyDocProp = null), + (this.matchedText = ''), + (this.offset = 0), + (this.tagBuilder = s.tagBuilder), + (this.matchedText = s.matchedText), + (this.offset = s.offset)); + } + return ( + (Match.prototype.getMatchedText = function () { + return this.matchedText; + }), + (Match.prototype.setOffset = function (s) { + this.offset = s; + }), + (Match.prototype.getOffset = function () { + return this.offset; + }), + (Match.prototype.getCssClassSuffixes = function () { + return [this.getType()]; + }), + (Match.prototype.buildTag = function () { + return this.tagBuilder.build(this); + }), + Match + ); + })(), + extendStatics = function (s, o) { + return ( + (extendStatics = + Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && + function (s, o) { + s.__proto__ = o; + }) || + function (s, o) { + for (var i in o) Object.prototype.hasOwnProperty.call(o, i) && (s[i] = o[i]); + }), + extendStatics(s, o) + ); + }; + function tslib_es6_extends(s, o) { + if ('function' != typeof o && null !== o) + throw new TypeError( + 'Class extends value ' + String(o) + ' is not a constructor or null' + ); + function __() { + this.constructor = s; + } + (extendStatics(s, o), + (s.prototype = + null === o ? Object.create(o) : ((__.prototype = o.prototype), new __()))); + } + var __assign = function () { + return ( + (__assign = + Object.assign || + function __assign(s) { + for (var o, i = 1, u = arguments.length; i < u; i++) + for (var _ in (o = arguments[i])) + Object.prototype.hasOwnProperty.call(o, _) && (s[_] = o[_]); + return s; + }), + __assign.apply(this, arguments) + ); + }; + Object.create; + Object.create; + 'function' == typeof SuppressedError && SuppressedError; + var zC, + WC = (function (s) { + function EmailMatch(o) { + var i = s.call(this, o) || this; + return ((i.email = ''), (i.email = o.email), i); + } + return ( + tslib_es6_extends(EmailMatch, s), + (EmailMatch.prototype.getType = function () { + return 'email'; + }), + (EmailMatch.prototype.getEmail = function () { + return this.email; + }), + (EmailMatch.prototype.getAnchorHref = function () { + return 'mailto:' + this.email; + }), + (EmailMatch.prototype.getAnchorText = function () { + return this.email; + }), + EmailMatch + ); + })(UC), + KC = (function (s) { + function HashtagMatch(o) { + var i = s.call(this, o) || this; + return ( + (i.serviceName = ''), + (i.hashtag = ''), + (i.serviceName = o.serviceName), + (i.hashtag = o.hashtag), + i + ); + } + return ( + tslib_es6_extends(HashtagMatch, s), + (HashtagMatch.prototype.getType = function () { + return 'hashtag'; + }), + (HashtagMatch.prototype.getServiceName = function () { + return this.serviceName; + }), + (HashtagMatch.prototype.getHashtag = function () { + return this.hashtag; + }), + (HashtagMatch.prototype.getAnchorHref = function () { + var s = this.serviceName, + o = this.hashtag; + switch (s) { + case 'twitter': + return 'https://twitter.com/hashtag/' + o; + case 'facebook': + return 'https://www.facebook.com/hashtag/' + o; + case 'instagram': + return 'https://instagram.com/explore/tags/' + o; + case 'tiktok': + return 'https://www.tiktok.com/tag/' + o; + default: + throw new Error('Unknown service name to point hashtag to: ' + s); + } + }), + (HashtagMatch.prototype.getAnchorText = function () { + return '#' + this.hashtag; + }), + HashtagMatch + ); + })(UC), + HC = (function (s) { + function MentionMatch(o) { + var i = s.call(this, o) || this; + return ( + (i.serviceName = 'twitter'), + (i.mention = ''), + (i.mention = o.mention), + (i.serviceName = o.serviceName), + i + ); + } + return ( + tslib_es6_extends(MentionMatch, s), + (MentionMatch.prototype.getType = function () { + return 'mention'; + }), + (MentionMatch.prototype.getMention = function () { + return this.mention; + }), + (MentionMatch.prototype.getServiceName = function () { + return this.serviceName; + }), + (MentionMatch.prototype.getAnchorHref = function () { + switch (this.serviceName) { + case 'twitter': + return 'https://twitter.com/' + this.mention; + case 'instagram': + return 'https://instagram.com/' + this.mention; + case 'soundcloud': + return 'https://soundcloud.com/' + this.mention; + case 'tiktok': + return 'https://www.tiktok.com/@' + this.mention; + default: + throw new Error( + 'Unknown service name to point mention to: ' + this.serviceName + ); + } + }), + (MentionMatch.prototype.getAnchorText = function () { + return '@' + this.mention; + }), + (MentionMatch.prototype.getCssClassSuffixes = function () { + var o = s.prototype.getCssClassSuffixes.call(this), + i = this.getServiceName(); + return (i && o.push(i), o); + }), + MentionMatch + ); + })(UC), + JC = (function (s) { + function PhoneMatch(o) { + var i = s.call(this, o) || this; + return ( + (i.number = ''), + (i.plusSign = !1), + (i.number = o.number), + (i.plusSign = o.plusSign), + i + ); + } + return ( + tslib_es6_extends(PhoneMatch, s), + (PhoneMatch.prototype.getType = function () { + return 'phone'; + }), + (PhoneMatch.prototype.getPhoneNumber = function () { + return this.number; + }), + (PhoneMatch.prototype.getNumber = function () { + return this.getPhoneNumber(); + }), + (PhoneMatch.prototype.getAnchorHref = function () { + return 'tel:' + (this.plusSign ? '+' : '') + this.number; + }), + (PhoneMatch.prototype.getAnchorText = function () { + return this.matchedText; + }), + PhoneMatch + ); + })(UC), + GC = (function (s) { + function UrlMatch(o) { + var i = s.call(this, o) || this; + return ( + (i.url = ''), + (i.urlMatchType = 'scheme'), + (i.protocolUrlMatch = !1), + (i.protocolRelativeMatch = !1), + (i.stripPrefix = { scheme: !0, www: !0 }), + (i.stripTrailingSlash = !0), + (i.decodePercentEncoding = !0), + (i.schemePrefixRegex = /^(https?:\/\/)?/i), + (i.wwwPrefixRegex = /^(https?:\/\/)?(www\.)?/i), + (i.protocolRelativeRegex = /^\/\//), + (i.protocolPrepended = !1), + (i.urlMatchType = o.urlMatchType), + (i.url = o.url), + (i.protocolUrlMatch = o.protocolUrlMatch), + (i.protocolRelativeMatch = o.protocolRelativeMatch), + (i.stripPrefix = o.stripPrefix), + (i.stripTrailingSlash = o.stripTrailingSlash), + (i.decodePercentEncoding = o.decodePercentEncoding), + i + ); + } + return ( + tslib_es6_extends(UrlMatch, s), + (UrlMatch.prototype.getType = function () { + return 'url'; + }), + (UrlMatch.prototype.getUrlMatchType = function () { + return this.urlMatchType; + }), + (UrlMatch.prototype.getUrl = function () { + var s = this.url; + return ( + this.protocolRelativeMatch || + this.protocolUrlMatch || + this.protocolPrepended || + ((s = this.url = 'http://' + s), (this.protocolPrepended = !0)), + s + ); + }), + (UrlMatch.prototype.getAnchorHref = function () { + return this.getUrl().replace(/&/g, '&'); + }), + (UrlMatch.prototype.getAnchorText = function () { + var s = this.getMatchedText(); + return ( + this.protocolRelativeMatch && (s = this.stripProtocolRelativePrefix(s)), + this.stripPrefix.scheme && (s = this.stripSchemePrefix(s)), + this.stripPrefix.www && (s = this.stripWwwPrefix(s)), + this.stripTrailingSlash && (s = this.removeTrailingSlash(s)), + this.decodePercentEncoding && (s = this.removePercentEncoding(s)), + s + ); + }), + (UrlMatch.prototype.stripSchemePrefix = function (s) { + return s.replace(this.schemePrefixRegex, ''); + }), + (UrlMatch.prototype.stripWwwPrefix = function (s) { + return s.replace(this.wwwPrefixRegex, '$1'); + }), + (UrlMatch.prototype.stripProtocolRelativePrefix = function (s) { + return s.replace(this.protocolRelativeRegex, ''); + }), + (UrlMatch.prototype.removeTrailingSlash = function (s) { + return ('/' === s.charAt(s.length - 1) && (s = s.slice(0, -1)), s); + }), + (UrlMatch.prototype.removePercentEncoding = function (s) { + var o = s + .replace(/%22/gi, '"') + .replace(/%26/gi, '&') + .replace(/%27/gi, ''') + .replace(/%3C/gi, '<') + .replace(/%3E/gi, '>'); + try { + return decodeURIComponent(o); + } catch (s) { + return o; + } + }), + UrlMatch + ); + })(UC), + YC = function YC(s) { + ((this.__jsduckDummyDocProp = null), (this.tagBuilder = s.tagBuilder)); + }, + XC = /[A-Za-z]/, + ZC = /[\d]/, + QC = /[\D]/, + eO = /\s/, + tO = /['"]/, + rO = /[\x00-\x1F\x7F]/, + nO = + /A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC/ + .source, + sO = + nO + + /\u2700-\u27bf\udde6-\uddff\ud800-\udbff\udc00-\udfff\ufe0e\ufe0f\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0\ud83c\udffb-\udfff\u200d\u3299\u3297\u303d\u3030\u24c2\ud83c\udd70-\udd71\udd7e-\udd7f\udd8e\udd91-\udd9a\udde6-\uddff\ude01-\ude02\ude1a\ude2f\ude32-\ude3a\ude50-\ude51\u203c\u2049\u25aa-\u25ab\u25b6\u25c0\u25fb-\u25fe\u00a9\u00ae\u2122\u2139\udc04\u2600-\u26FF\u2b05\u2b06\u2b07\u2b1b\u2b1c\u2b50\u2b55\u231a\u231b\u2328\u23cf\u23e9-\u23f3\u23f8-\u23fa\udccf\u2935\u2934\u2190-\u21ff/ + .source + + /\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D4-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D01-\u0D03\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF5\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F/ + .source, + oO = + /0-9\u0660-\u0669\u06F0-\u06F9\u07C0-\u07C9\u0966-\u096F\u09E6-\u09EF\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0BE6-\u0BEF\u0C66-\u0C6F\u0CE6-\u0CEF\u0D66-\u0D6F\u0DE6-\u0DEF\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F29\u1040-\u1049\u1090-\u1099\u17E0-\u17E9\u1810-\u1819\u1946-\u194F\u19D0-\u19D9\u1A80-\u1A89\u1A90-\u1A99\u1B50-\u1B59\u1BB0-\u1BB9\u1C40-\u1C49\u1C50-\u1C59\uA620-\uA629\uA8D0-\uA8D9\uA900-\uA909\uA9D0-\uA9D9\uA9F0-\uA9F9\uAA50-\uAA59\uABF0-\uABF9\uFF10-\uFF19/ + .source, + iO = sO + oO, + aO = sO + oO, + lO = new RegExp('['.concat(aO, ']')), + cO = '(?:[' + oO + ']{1,3}\\.){3}[' + oO + ']{1,3}', + uO = '[' + aO + '](?:[' + aO + '\\-_]{0,61}[' + aO + '])?', + getDomainLabelStr = function (s) { + return '(?=(' + uO + '))\\' + s; + }, + getDomainNameStr = function (s) { + return ( + '(?:' + + getDomainLabelStr(s) + + '(?:\\.' + + getDomainLabelStr(s + 1) + + '){0,126}|' + + cO + + ')' + ); + }, + pO = (new RegExp('[' + aO + '.\\-]*[' + aO + '\\-]'), lO), + hO = + /(?:xn--vermgensberatung-pwb|xn--vermgensberater-ctb|xn--clchc0ea0b2g2a9gcd|xn--w4r85el8fhu5dnra|northwesternmutual|travelersinsurance|vermögensberatung|xn--5su34j936bgsg|xn--bck1b9a5dre4c|xn--mgbah1a3hjkrd|xn--mgbai9azgqp6j|xn--mgberp4a5d4ar|xn--xkc2dl3a5ee0h|vermögensberater|xn--fzys8d69uvgm|xn--mgba7c0bbn0a|xn--mgbcpq6gpa1a|xn--xkc2al3hye2a|americanexpress|kerryproperties|sandvikcoromant|xn--i1b6b1a6a2e|xn--kcrx77d1x4a|xn--lgbbat1ad8j|xn--mgba3a4f16a|xn--mgbaakc7dvf|xn--mgbc0a9azcg|xn--nqv7fs00ema|americanfamily|bananarepublic|cancerresearch|cookingchannel|kerrylogistics|weatherchannel|xn--54b7fta0cc|xn--6qq986b3xl|xn--80aqecdr1a|xn--b4w605ferd|xn--fiq228c5hs|xn--h2breg3eve|xn--jlq480n2rg|xn--jlq61u9w7b|xn--mgba3a3ejt|xn--mgbaam7a8h|xn--mgbayh7gpa|xn--mgbbh1a71e|xn--mgbca7dzdo|xn--mgbi4ecexp|xn--mgbx4cd0ab|xn--rvc1e0am3e|international|lifeinsurance|travelchannel|wolterskluwer|xn--cckwcxetd|xn--eckvdtc9d|xn--fpcrj9c3d|xn--fzc2c9e2c|xn--h2brj9c8c|xn--tiq49xqyj|xn--yfro4i67o|xn--ygbi2ammx|construction|lplfinancial|scholarships|versicherung|xn--3e0b707e|xn--45br5cyl|xn--4dbrk0ce|xn--80adxhks|xn--80asehdb|xn--8y0a063a|xn--gckr3f0f|xn--mgb9awbf|xn--mgbab2bd|xn--mgbgu82a|xn--mgbpl2fh|xn--mgbt3dhd|xn--mk1bu44c|xn--ngbc5azd|xn--ngbe9e0a|xn--ogbpf8fl|xn--qcka1pmc|accountants|barclaycard|blackfriday|blockbuster|bridgestone|calvinklein|contractors|creditunion|engineering|enterprises|foodnetwork|investments|kerryhotels|lamborghini|motorcycles|olayangroup|photography|playstation|productions|progressive|redumbrella|williamhill|xn--11b4c3d|xn--1ck2e1b|xn--1qqw23a|xn--2scrj9c|xn--3bst00m|xn--3ds443g|xn--3hcrj9c|xn--42c2d9a|xn--45brj9c|xn--55qw42g|xn--6frz82g|xn--80ao21a|xn--9krt00a|xn--cck2b3b|xn--czr694b|xn--d1acj3b|xn--efvy88h|xn--fct429k|xn--fjq720a|xn--flw351e|xn--g2xx48c|xn--gecrj9c|xn--gk3at1e|xn--h2brj9c|xn--hxt814e|xn--imr513n|xn--j6w193g|xn--jvr189m|xn--kprw13d|xn--kpry57d|xn--mgbbh1a|xn--mgbtx2b|xn--mix891f|xn--nyqy26a|xn--otu796d|xn--pgbs0dh|xn--q9jyb4c|xn--rhqv96g|xn--rovu88b|xn--s9brj9c|xn--ses554g|xn--t60b56a|xn--vuq861b|xn--w4rs40l|xn--xhq521b|xn--zfr164b|சிங்கப்பூர்|accountant|apartments|associates|basketball|bnpparibas|boehringer|capitalone|consulting|creditcard|cuisinella|eurovision|extraspace|foundation|healthcare|immobilien|industries|management|mitsubishi|nextdirect|properties|protection|prudential|realestate|republican|restaurant|schaeffler|tatamotors|technology|university|vlaanderen|volkswagen|xn--30rr7y|xn--3pxu8k|xn--45q11c|xn--4gbrim|xn--55qx5d|xn--5tzm5g|xn--80aswg|xn--90a3ac|xn--9dbq2a|xn--9et52u|xn--c2br7g|xn--cg4bki|xn--czrs0t|xn--czru2d|xn--fiq64b|xn--fiqs8s|xn--fiqz9s|xn--io0a7i|xn--kput3i|xn--mxtq1m|xn--o3cw4h|xn--pssy2u|xn--q7ce6a|xn--unup4y|xn--wgbh1c|xn--wgbl6a|xn--y9a3aq|accenture|alfaromeo|allfinanz|amsterdam|analytics|aquarelle|barcelona|bloomberg|christmas|community|directory|education|equipment|fairwinds|financial|firestone|fresenius|frontdoor|furniture|goldpoint|hisamitsu|homedepot|homegoods|homesense|institute|insurance|kuokgroup|lancaster|landrover|lifestyle|marketing|marshalls|melbourne|microsoft|panasonic|passagens|pramerica|richardli|shangrila|solutions|statebank|statefarm|stockholm|travelers|vacations|xn--90ais|xn--c1avg|xn--d1alf|xn--e1a4c|xn--fhbei|xn--j1aef|xn--j1amh|xn--l1acc|xn--ngbrx|xn--nqv7f|xn--p1acf|xn--qxa6a|xn--tckwe|xn--vhquv|yodobashi|موريتانيا|abudhabi|airforce|allstate|attorney|barclays|barefoot|bargains|baseball|boutique|bradesco|broadway|brussels|builders|business|capetown|catering|catholic|cipriani|cityeats|cleaning|clinique|clothing|commbank|computer|delivery|deloitte|democrat|diamonds|discount|discover|download|engineer|ericsson|etisalat|exchange|feedback|fidelity|firmdale|football|frontier|goodyear|grainger|graphics|guardian|hdfcbank|helsinki|holdings|hospital|infiniti|ipiranga|istanbul|jpmorgan|lighting|lundbeck|marriott|maserati|mckinsey|memorial|merckmsd|mortgage|observer|partners|pharmacy|pictures|plumbing|property|redstone|reliance|saarland|samsclub|security|services|shopping|showtime|softbank|software|stcgroup|supplies|training|vanguard|ventures|verisign|woodside|xn--90ae|xn--node|xn--p1ai|xn--qxam|yokohama|السعودية|abogado|academy|agakhan|alibaba|android|athleta|auction|audible|auspost|avianca|banamex|bauhaus|bentley|bestbuy|booking|brother|bugatti|capital|caravan|careers|channel|charity|chintai|citadel|clubmed|college|cologne|comcast|company|compare|contact|cooking|corsica|country|coupons|courses|cricket|cruises|dentist|digital|domains|exposed|express|farmers|fashion|ferrari|ferrero|finance|fishing|fitness|flights|florist|flowers|forsale|frogans|fujitsu|gallery|genting|godaddy|grocery|guitars|hamburg|hangout|hitachi|holiday|hosting|hoteles|hotmail|hyundai|ismaili|jewelry|juniper|kitchen|komatsu|lacaixa|lanxess|lasalle|latrobe|leclerc|limited|lincoln|markets|monster|netbank|netflix|network|neustar|okinawa|oldnavy|organic|origins|philips|pioneer|politie|realtor|recipes|rentals|reviews|rexroth|samsung|sandvik|schmidt|schwarz|science|shiksha|singles|staples|storage|support|surgery|systems|temasek|theater|theatre|tickets|tiffany|toshiba|trading|walmart|wanggou|watches|weather|website|wedding|whoswho|windows|winners|xfinity|yamaxun|youtube|zuerich|католик|اتصالات|البحرين|الجزائر|العليان|پاکستان|كاثوليك|இந்தியா|abarth|abbott|abbvie|africa|agency|airbus|airtel|alipay|alsace|alstom|amazon|anquan|aramco|author|bayern|beauty|berlin|bharti|bostik|boston|broker|camera|career|casino|center|chanel|chrome|church|circle|claims|clinic|coffee|comsec|condos|coupon|credit|cruise|dating|datsun|dealer|degree|dental|design|direct|doctor|dunlop|dupont|durban|emerck|energy|estate|events|expert|family|flickr|futbol|gallup|garden|george|giving|global|google|gratis|health|hermes|hiphop|hockey|hotels|hughes|imamat|insure|intuit|jaguar|joburg|juegos|kaufen|kinder|kindle|kosher|lancia|latino|lawyer|lefrak|living|locker|london|luxury|madrid|maison|makeup|market|mattel|mobile|monash|mormon|moscow|museum|mutual|nagoya|natura|nissan|nissay|norton|nowruz|office|olayan|online|oracle|orange|otsuka|pfizer|photos|physio|pictet|quebec|racing|realty|reisen|repair|report|review|rocher|rogers|ryukyu|safety|sakura|sanofi|school|schule|search|secure|select|shouji|soccer|social|stream|studio|supply|suzuki|swatch|sydney|taipei|taobao|target|tattoo|tennis|tienda|tjmaxx|tkmaxx|toyota|travel|unicom|viajes|viking|villas|virgin|vision|voting|voyage|vuelos|walter|webcam|xihuan|yachts|yandex|zappos|москва|онлайн|ابوظبي|ارامكو|الاردن|المغرب|امارات|فلسطين|مليسيا|भारतम्|இலங்கை|ファッション|actor|adult|aetna|amfam|amica|apple|archi|audio|autos|azure|baidu|beats|bible|bingo|black|boats|bosch|build|canon|cards|chase|cheap|cisco|citic|click|cloud|coach|codes|crown|cymru|dabur|dance|deals|delta|drive|dubai|earth|edeka|email|epson|faith|fedex|final|forex|forum|gallo|games|gifts|gives|glass|globo|gmail|green|gripe|group|gucci|guide|homes|honda|horse|house|hyatt|ikano|irish|jetzt|koeln|kyoto|lamer|lease|legal|lexus|lilly|linde|lipsy|loans|locus|lotte|lotto|macys|mango|media|miami|money|movie|music|nexus|nikon|ninja|nokia|nowtv|omega|osaka|paris|parts|party|phone|photo|pizza|place|poker|praxi|press|prime|promo|quest|radio|rehab|reise|ricoh|rocks|rodeo|rugby|salon|sener|seven|sharp|shell|shoes|skype|sling|smart|smile|solar|space|sport|stada|store|study|style|sucks|swiss|tatar|tires|tirol|tmall|today|tokyo|tools|toray|total|tours|trade|trust|tunes|tushu|ubank|vegas|video|vodka|volvo|wales|watch|weber|weibo|works|world|xerox|yahoo|ישראל|ایران|بازار|بھارت|سودان|سورية|همراه|भारोत|संगठन|বাংলা|భారత్|ഭാരതം|嘉里大酒店|aarp|able|adac|aero|akdn|ally|amex|arab|army|arpa|arte|asda|asia|audi|auto|baby|band|bank|bbva|beer|best|bike|bing|blog|blue|bofa|bond|book|buzz|cafe|call|camp|care|cars|casa|case|cash|cbre|cern|chat|citi|city|club|cool|coop|cyou|data|date|dclk|deal|dell|desi|diet|dish|docs|dvag|erni|fage|fail|fans|farm|fast|fiat|fido|film|fire|fish|flir|food|ford|free|fund|game|gbiz|gent|ggee|gift|gmbh|gold|golf|goog|guge|guru|hair|haus|hdfc|help|here|hgtv|host|hsbc|icbc|ieee|imdb|immo|info|itau|java|jeep|jobs|jprs|kddi|kids|kiwi|kpmg|kred|land|lego|lgbt|lidl|life|like|limo|link|live|loan|loft|love|ltda|luxe|maif|meet|meme|menu|mini|mint|mobi|moda|moto|name|navy|news|next|nico|nike|ollo|open|page|pars|pccw|pics|ping|pink|play|plus|pohl|porn|post|prod|prof|qpon|read|reit|rent|rest|rich|room|rsvp|ruhr|safe|sale|sarl|save|saxo|scot|seat|seek|sexy|shaw|shia|shop|show|silk|sina|site|skin|sncf|sohu|song|sony|spot|star|surf|talk|taxi|team|tech|teva|tiaa|tips|town|toys|tube|vana|visa|viva|vivo|vote|voto|wang|weir|wien|wiki|wine|work|xbox|yoga|zara|zero|zone|дети|сайт|بارت|بيتك|ڀارت|تونس|شبكة|عراق|عمان|موقع|भारत|ভারত|ভাৰত|ਭਾਰਤ|ભારત|ଭାରତ|ಭಾರತ|ලංකා|アマゾン|グーグル|クラウド|ポイント|组织机构|電訊盈科|香格里拉|aaa|abb|abc|aco|ads|aeg|afl|aig|anz|aol|app|art|aws|axa|bar|bbc|bbt|bcg|bcn|bet|bid|bio|biz|bms|bmw|bom|boo|bot|box|buy|bzh|cab|cal|cam|car|cat|cba|cbn|cbs|ceo|cfa|cfd|com|cpa|crs|dad|day|dds|dev|dhl|diy|dnp|dog|dot|dtv|dvr|eat|eco|edu|esq|eus|fan|fit|fly|foo|fox|frl|ftr|fun|fyi|gal|gap|gay|gdn|gea|gle|gmo|gmx|goo|gop|got|gov|hbo|hiv|hkt|hot|how|ibm|ice|icu|ifm|inc|ing|ink|int|ist|itv|jcb|jio|jll|jmp|jnj|jot|joy|kfh|kia|kim|kpn|krd|lat|law|lds|llc|llp|lol|lpl|ltd|man|map|mba|med|men|mil|mit|mlb|mls|mma|moe|moi|mom|mov|msd|mtn|mtr|nab|nba|nec|net|new|nfl|ngo|nhk|now|nra|nrw|ntt|nyc|obi|one|ong|onl|ooo|org|ott|ovh|pay|pet|phd|pid|pin|pnc|pro|pru|pub|pwc|red|ren|ril|rio|rip|run|rwe|sap|sas|sbi|sbs|sca|scb|ses|sew|sex|sfr|ski|sky|soy|spa|srl|stc|tab|tax|tci|tdk|tel|thd|tjx|top|trv|tui|tvs|ubs|uno|uol|ups|vet|vig|vin|vip|wed|win|wme|wow|wtc|wtf|xin|xxx|xyz|you|yun|zip|бел|ком|қаз|мкд|мон|орг|рус|срб|укр|հայ|קום|عرب|قطر|كوم|مصر|कॉम|नेट|คอม|ไทย|ລາວ|ストア|セール|みんな|中文网|亚马逊|天主教|我爱你|新加坡|淡马锡|诺基亚|飞利浦|ac|ad|ae|af|ag|ai|al|am|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cw|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw|ελ|ευ|бг|ею|рф|გე|닷넷|닷컴|삼성|한국|コム|世界|中信|中国|中國|企业|佛山|信息|健康|八卦|公司|公益|台湾|台灣|商城|商店|商标|嘉里|在线|大拿|娱乐|家電|广东|微博|慈善|手机|招聘|政务|政府|新闻|时尚|書籍|机构|游戏|澳門|点看|移动|网址|网店|网站|网络|联通|谷歌|购物|通販|集团|食品|餐厅|香港)/, + dO = new RegExp('['.concat(aO, "!#$%&'*+/=?^_`{|}~-]")), + fO = new RegExp('^'.concat(hO.source, '$')), + mO = (function (s) { + function EmailMatcher() { + var o = (null !== s && s.apply(this, arguments)) || this; + return ((o.localPartCharRegex = dO), (o.strictTldRegex = fO), o); + } + return ( + tslib_es6_extends(EmailMatcher, s), + (EmailMatcher.prototype.parseMatches = function (s) { + for ( + var o = this.tagBuilder, + i = this.localPartCharRegex, + u = this.strictTldRegex, + _ = [], + w = s.length, + x = new gO(), + C = { m: 'a', a: 'i', i: 'l', l: 't', t: 'o', o: ':' }, + j = 0, + L = 0, + B = x; + j < w; + ) { + var $ = s.charAt(j); + switch (L) { + case 0: + stateNonEmailAddress($); + break; + case 1: + stateMailTo(s.charAt(j - 1), $); + break; + case 2: + stateLocalPart($); + break; + case 3: + stateLocalPartDot($); + break; + case 4: + stateAtSign($); + break; + case 5: + stateDomainChar($); + break; + case 6: + stateDomainHyphen($); + break; + case 7: + stateDomainDot($); + break; + default: + throwUnhandledCaseError(L); + } + j++; + } + return (captureMatchIfValidAndReset(), _); + function stateNonEmailAddress(s) { + 'm' === s ? beginEmailMatch(1) : i.test(s) && beginEmailMatch(); + } + function stateMailTo(s, o) { + ':' === s + ? i.test(o) + ? ((L = 2), (B = new gO(__assign(__assign({}, B), { hasMailtoPrefix: !0 })))) + : resetToNonEmailMatchState() + : C[s] === o || + (i.test(o) + ? (L = 2) + : '.' === o + ? (L = 3) + : '@' === o + ? (L = 4) + : resetToNonEmailMatchState()); + } + function stateLocalPart(s) { + '.' === s + ? (L = 3) + : '@' === s + ? (L = 4) + : i.test(s) || resetToNonEmailMatchState(); + } + function stateLocalPartDot(s) { + '.' === s || '@' === s + ? resetToNonEmailMatchState() + : i.test(s) + ? (L = 2) + : resetToNonEmailMatchState(); + } + function stateAtSign(s) { + pO.test(s) ? (L = 5) : resetToNonEmailMatchState(); + } + function stateDomainChar(s) { + '.' === s + ? (L = 7) + : '-' === s + ? (L = 6) + : pO.test(s) || captureMatchIfValidAndReset(); + } + function stateDomainHyphen(s) { + '-' === s || '.' === s + ? captureMatchIfValidAndReset() + : pO.test(s) + ? (L = 5) + : captureMatchIfValidAndReset(); + } + function stateDomainDot(s) { + '.' === s || '-' === s + ? captureMatchIfValidAndReset() + : pO.test(s) + ? ((L = 5), (B = new gO(__assign(__assign({}, B), { hasDomainDot: !0 })))) + : captureMatchIfValidAndReset(); + } + function beginEmailMatch(s) { + (void 0 === s && (s = 2), (L = s), (B = new gO({ idx: j }))); + } + function resetToNonEmailMatchState() { + ((L = 0), (B = x)); + } + function captureMatchIfValidAndReset() { + if (B.hasDomainDot) { + var i = s.slice(B.idx, j); + /[-.]$/.test(i) && (i = i.slice(0, -1)); + var w = B.hasMailtoPrefix ? i.slice(7) : i; + (function doesEmailHaveValidTld(s) { + var o = s.split('.').pop() || '', + i = o.toLowerCase(); + return u.test(i); + })(w) && + _.push(new WC({ tagBuilder: o, matchedText: i, offset: B.idx, email: w })); + } + resetToNonEmailMatchState(); + } + }), + EmailMatcher + ); + })(YC), + gO = function gO(s) { + (void 0 === s && (s = {}), + (this.idx = void 0 !== s.idx ? s.idx : -1), + (this.hasMailtoPrefix = !!s.hasMailtoPrefix), + (this.hasDomainDot = !!s.hasDomainDot)); + }, + yO = (function () { + function UrlMatchValidator() {} + return ( + (UrlMatchValidator.isValid = function (s, o) { + return !( + (o && !this.isValidUriScheme(o)) || + this.urlMatchDoesNotHaveProtocolOrDot(s, o) || + (this.urlMatchDoesNotHaveAtLeastOneWordChar(s, o) && !this.isValidIpAddress(s)) || + this.containsMultipleDots(s) + ); + }), + (UrlMatchValidator.isValidIpAddress = function (s) { + var o = new RegExp(this.hasFullProtocolRegex.source + this.ipRegex.source); + return null !== s.match(o); + }), + (UrlMatchValidator.containsMultipleDots = function (s) { + var o = s; + return ( + this.hasFullProtocolRegex.test(s) && (o = s.split('://')[1]), + o.split('/')[0].indexOf('..') > -1 + ); + }), + (UrlMatchValidator.isValidUriScheme = function (s) { + var o = s.match(this.uriSchemeRegex), + i = o && o[0].toLowerCase(); + return 'javascript:' !== i && 'vbscript:' !== i; + }), + (UrlMatchValidator.urlMatchDoesNotHaveProtocolOrDot = function (s, o) { + return !(!s || (o && this.hasFullProtocolRegex.test(o)) || -1 !== s.indexOf('.')); + }), + (UrlMatchValidator.urlMatchDoesNotHaveAtLeastOneWordChar = function (s, o) { + return ( + !(!s || !o) && + !this.hasFullProtocolRegex.test(o) && + !this.hasWordCharAfterProtocolRegex.test(s) + ); + }), + (UrlMatchValidator.hasFullProtocolRegex = /^[A-Za-z][-.+A-Za-z0-9]*:\/\//), + (UrlMatchValidator.uriSchemeRegex = /^[A-Za-z][-.+A-Za-z0-9]*:/), + (UrlMatchValidator.hasWordCharAfterProtocolRegex = new RegExp( + ':[^\\s]*?[' + nO + ']' + )), + (UrlMatchValidator.ipRegex = + /[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?(:[0-9]*)?\/?$/), + UrlMatchValidator + ); + })(), + vO = + ((zC = new RegExp( + '[/?#](?:[' + + aO + + "\\-+&@#/%=~_()|'$*\\[\\]{}?!:,.;^✓]*[" + + aO + + "\\-+&@#/%=~_()|'$*\\[\\]{}✓])?" + )), + new RegExp( + [ + '(?:', + '(', + /(?:[A-Za-z][-.+A-Za-z0-9]{0,63}:(?![A-Za-z][-.+A-Za-z0-9]{0,63}:\/\/)(?!\d+\/?)(?:\/\/)?)/ + .source, + getDomainNameStr(2), + ')', + '|', + '(', + '(//)?', + /(?:www\.)/.source, + getDomainNameStr(6), + ')', + '|', + '(', + '(//)?', + getDomainNameStr(10) + '\\.', + hO.source, + '(?![-' + iO + '])', + ')', + ')', + '(?::[0-9]+)?', + '(?:' + zC.source + ')?' + ].join(''), + 'gi' + )), + bO = new RegExp('[' + aO + ']'), + _O = (function (s) { + function UrlMatcher(o) { + var i = s.call(this, o) || this; + return ( + (i.stripPrefix = { scheme: !0, www: !0 }), + (i.stripTrailingSlash = !0), + (i.decodePercentEncoding = !0), + (i.matcherRegex = vO), + (i.wordCharRegExp = bO), + (i.stripPrefix = o.stripPrefix), + (i.stripTrailingSlash = o.stripTrailingSlash), + (i.decodePercentEncoding = o.decodePercentEncoding), + i + ); + } + return ( + tslib_es6_extends(UrlMatcher, s), + (UrlMatcher.prototype.parseMatches = function (s) { + for ( + var o, + i = this.matcherRegex, + u = this.stripPrefix, + _ = this.stripTrailingSlash, + w = this.decodePercentEncoding, + x = this.tagBuilder, + C = [], + _loop_1 = function () { + var i = o[0], + L = o[1], + B = o[4], + $ = o[5], + V = o[9], + U = o.index, + z = $ || V, + Y = s.charAt(U - 1); + if (!yO.isValid(i, L)) return 'continue'; + if (U > 0 && '@' === Y) return 'continue'; + if (U > 0 && z && j.wordCharRegExp.test(Y)) return 'continue'; + if ( + (/\?$/.test(i) && (i = i.substr(0, i.length - 1)), + j.matchHasUnbalancedClosingParen(i)) + ) + i = i.substr(0, i.length - 1); + else { + var Z = j.matchHasInvalidCharAfterTld(i, L); + Z > -1 && (i = i.substr(0, Z)); + } + var ee = ['http://', 'https://'].find(function (s) { + return !!L && -1 !== L.indexOf(s); + }); + if (ee) { + var ie = i.indexOf(ee); + ((i = i.substr(ie)), (L = L.substr(ie)), (U += ie)); + } + var ae = L ? 'scheme' : B ? 'www' : 'tld', + le = !!L; + C.push( + new GC({ + tagBuilder: x, + matchedText: i, + offset: U, + urlMatchType: ae, + url: i, + protocolUrlMatch: le, + protocolRelativeMatch: !!z, + stripPrefix: u, + stripTrailingSlash: _, + decodePercentEncoding: w + }) + ); + }, + j = this; + null !== (o = i.exec(s)); + ) + _loop_1(); + return C; + }), + (UrlMatcher.prototype.matchHasUnbalancedClosingParen = function (s) { + var o, + i = s.charAt(s.length - 1); + if (')' === i) o = '('; + else if (']' === i) o = '['; + else { + if ('}' !== i) return !1; + o = '{'; + } + for (var u = 0, _ = 0, w = s.length - 1; _ < w; _++) { + var x = s.charAt(_); + x === o ? u++ : x === i && (u = Math.max(u - 1, 0)); + } + return 0 === u; + }), + (UrlMatcher.prototype.matchHasInvalidCharAfterTld = function (s, o) { + if (!s) return -1; + var i = 0; + o && ((i = s.indexOf(':')), (s = s.slice(i))); + var u = new RegExp('^((.?//)?[-.' + aO + ']*[-' + aO + ']\\.[-' + aO + ']+)').exec( + s + ); + return null === u + ? -1 + : ((i += u[1].length), + (s = s.slice(u[1].length)), + /^[^-.A-Za-z0-9:\/?#]/.test(s) ? i : -1); + }), + UrlMatcher + ); + })(YC), + EO = new RegExp('[_'.concat(aO, ']')), + wO = (function (s) { + function HashtagMatcher(o) { + var i = s.call(this, o) || this; + return ((i.serviceName = 'twitter'), (i.serviceName = o.serviceName), i); + } + return ( + tslib_es6_extends(HashtagMatcher, s), + (HashtagMatcher.prototype.parseMatches = function (s) { + for ( + var o = this.tagBuilder, + i = this.serviceName, + u = [], + _ = s.length, + w = 0, + x = -1, + C = 0; + w < _; + ) { + var j = s.charAt(w); + switch (C) { + case 0: + stateNone(j); + break; + case 1: + stateNonHashtagWordChar(j); + break; + case 2: + stateHashtagHashChar(j); + break; + case 3: + stateHashtagTextChar(j); + break; + default: + throwUnhandledCaseError(C); + } + w++; + } + return (captureMatchIfValid(), u); + function stateNone(s) { + '#' === s ? ((C = 2), (x = w)) : lO.test(s) && (C = 1); + } + function stateNonHashtagWordChar(s) { + lO.test(s) || (C = 0); + } + function stateHashtagHashChar(s) { + C = EO.test(s) ? 3 : lO.test(s) ? 1 : 0; + } + function stateHashtagTextChar(s) { + EO.test(s) || (captureMatchIfValid(), (x = -1), (C = lO.test(s) ? 1 : 0)); + } + function captureMatchIfValid() { + if (x > -1 && w - x <= 140) { + var _ = s.slice(x, w), + C = new KC({ + tagBuilder: o, + matchedText: _, + offset: x, + serviceName: i, + hashtag: _.slice(1) + }); + u.push(C); + } + } + }), + HashtagMatcher + ); + })(YC), + SO = ['twitter', 'facebook', 'instagram', 'tiktok'], + xO = new RegExp( + '' + .concat( + /(?:(?:(?:(\+)?\d{1,3}[-\040.]?)?\(?\d{3}\)?[-\040.]?\d{3}[-\040.]?\d{4})|(?:(\+)(?:9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)[-\040.]?(?:\d[-\040.]?){6,12}\d+))([,;]+[0-9]+#?)*/ + .source, + '|' + ) + .concat( + /(0([1-9]{1}-?[1-9]\d{3}|[1-9]{2}-?\d{3}|[1-9]{2}\d{1}-?\d{2}|[1-9]{2}\d{2}-?\d{1})-?\d{4}|0[789]0-?\d{4}-?\d{4}|050-?\d{4}-?\d{4})/ + .source + ), + 'g' + ), + kO = (function (s) { + function PhoneMatcher() { + var o = (null !== s && s.apply(this, arguments)) || this; + return ((o.matcherRegex = xO), o); + } + return ( + tslib_es6_extends(PhoneMatcher, s), + (PhoneMatcher.prototype.parseMatches = function (s) { + for ( + var o, i = this.matcherRegex, u = this.tagBuilder, _ = []; + null !== (o = i.exec(s)); + ) { + var w = o[0], + x = w.replace(/[^0-9,;#]/g, ''), + C = !(!o[1] && !o[2]), + j = 0 == o.index ? '' : s.substr(o.index - 1, 1), + L = s.substr(o.index + w.length, 1), + B = !j.match(/\d/) && !L.match(/\d/); + this.testMatch(o[3]) && + this.testMatch(w) && + B && + _.push( + new JC({ + tagBuilder: u, + matchedText: w, + offset: o.index, + number: x, + plusSign: C + }) + ); + } + return _; + }), + (PhoneMatcher.prototype.testMatch = function (s) { + return QC.test(s); + }), + PhoneMatcher + ); + })(YC), + CO = new RegExp('@[_'.concat(aO, ']{1,50}(?![_').concat(aO, '])'), 'g'), + OO = new RegExp('@[_.'.concat(aO, ']{1,30}(?![_').concat(aO, '])'), 'g'), + AO = new RegExp('@[-_.'.concat(aO, ']{1,50}(?![-_').concat(aO, '])'), 'g'), + jO = new RegExp( + '@[_.'.concat(aO, ']{1,23}[_').concat(aO, '](?![_').concat(aO, '])'), + 'g' + ), + IO = new RegExp('[^' + aO + ']'), + PO = (function (s) { + function MentionMatcher(o) { + var i = s.call(this, o) || this; + return ( + (i.serviceName = 'twitter'), + (i.matcherRegexes = { twitter: CO, instagram: OO, soundcloud: AO, tiktok: jO }), + (i.nonWordCharRegex = IO), + (i.serviceName = o.serviceName), + i + ); + } + return ( + tslib_es6_extends(MentionMatcher, s), + (MentionMatcher.prototype.parseMatches = function (s) { + var o, + i = this.serviceName, + u = this.matcherRegexes[this.serviceName], + _ = this.nonWordCharRegex, + w = this.tagBuilder, + x = []; + if (!u) return x; + for (; null !== (o = u.exec(s)); ) { + var C = o.index, + j = s.charAt(C - 1); + if (0 === C || _.test(j)) { + var L = o[0].replace(/\.+$/g, ''), + B = L.slice(1); + x.push( + new HC({ + tagBuilder: w, + matchedText: L, + offset: C, + serviceName: i, + mention: B + }) + ); + } + } + return x; + }), + MentionMatcher + ); + })(YC); + function parseHtml(s, o) { + for ( + var i = o.onOpenTag, + u = o.onCloseTag, + _ = o.onText, + w = o.onComment, + x = o.onDoctype, + C = new MO(), + j = 0, + L = s.length, + B = 0, + $ = 0, + V = C; + j < L; + ) { + var U = s.charAt(j); + switch (B) { + case 0: + stateData(U); + break; + case 1: + stateTagOpen(U); + break; + case 2: + stateEndTagOpen(U); + break; + case 3: + stateTagName(U); + break; + case 4: + stateBeforeAttributeName(U); + break; + case 5: + stateAttributeName(U); + break; + case 6: + stateAfterAttributeName(U); + break; + case 7: + stateBeforeAttributeValue(U); + break; + case 8: + stateAttributeValueDoubleQuoted(U); + break; + case 9: + stateAttributeValueSingleQuoted(U); + break; + case 10: + stateAttributeValueUnquoted(U); + break; + case 11: + stateAfterAttributeValueQuoted(U); + break; + case 12: + stateSelfClosingStartTag(U); + break; + case 13: + stateMarkupDeclarationOpen(U); + break; + case 14: + stateCommentStart(U); + break; + case 15: + stateCommentStartDash(U); + break; + case 16: + stateComment(U); + break; + case 17: + stateCommentEndDash(U); + break; + case 18: + stateCommentEnd(U); + break; + case 19: + stateCommentEndBang(U); + break; + case 20: + stateDoctype(U); + break; + default: + throwUnhandledCaseError(B); + } + j++; + } + function stateData(s) { + '<' === s && startNewTag(); + } + function stateTagOpen(s) { + '!' === s + ? (B = 13) + : '/' === s + ? ((B = 2), (V = new MO(__assign(__assign({}, V), { isClosing: !0 })))) + : '<' === s + ? startNewTag() + : XC.test(s) + ? ((B = 3), (V = new MO(__assign(__assign({}, V), { isOpening: !0 })))) + : ((B = 0), (V = C)); + } + function stateTagName(s) { + eO.test(s) + ? ((V = new MO(__assign(__assign({}, V), { name: captureTagName() }))), (B = 4)) + : '<' === s + ? startNewTag() + : '/' === s + ? ((V = new MO(__assign(__assign({}, V), { name: captureTagName() }))), (B = 12)) + : '>' === s + ? ((V = new MO(__assign(__assign({}, V), { name: captureTagName() }))), + emitTagAndPreviousTextNode()) + : XC.test(s) || ZC.test(s) || ':' === s || resetToDataState(); + } + function stateEndTagOpen(s) { + '>' === s ? resetToDataState() : XC.test(s) ? (B = 3) : resetToDataState(); + } + function stateBeforeAttributeName(s) { + eO.test(s) || + ('/' === s + ? (B = 12) + : '>' === s + ? emitTagAndPreviousTextNode() + : '<' === s + ? startNewTag() + : '=' === s || tO.test(s) || rO.test(s) + ? resetToDataState() + : (B = 5)); + } + function stateAttributeName(s) { + eO.test(s) + ? (B = 6) + : '/' === s + ? (B = 12) + : '=' === s + ? (B = 7) + : '>' === s + ? emitTagAndPreviousTextNode() + : '<' === s + ? startNewTag() + : tO.test(s) && resetToDataState(); + } + function stateAfterAttributeName(s) { + eO.test(s) || + ('/' === s + ? (B = 12) + : '=' === s + ? (B = 7) + : '>' === s + ? emitTagAndPreviousTextNode() + : '<' === s + ? startNewTag() + : tO.test(s) + ? resetToDataState() + : (B = 5)); + } + function stateBeforeAttributeValue(s) { + eO.test(s) || + ('"' === s + ? (B = 8) + : "'" === s + ? (B = 9) + : /[>=`]/.test(s) + ? resetToDataState() + : '<' === s + ? startNewTag() + : (B = 10)); + } + function stateAttributeValueDoubleQuoted(s) { + '"' === s && (B = 11); + } + function stateAttributeValueSingleQuoted(s) { + "'" === s && (B = 11); + } + function stateAttributeValueUnquoted(s) { + eO.test(s) + ? (B = 4) + : '>' === s + ? emitTagAndPreviousTextNode() + : '<' === s && startNewTag(); + } + function stateAfterAttributeValueQuoted(s) { + eO.test(s) + ? (B = 4) + : '/' === s + ? (B = 12) + : '>' === s + ? emitTagAndPreviousTextNode() + : '<' === s + ? startNewTag() + : ((B = 4), + (function reconsumeCurrentCharacter() { + j--; + })()); + } + function stateSelfClosingStartTag(s) { + '>' === s + ? ((V = new MO(__assign(__assign({}, V), { isClosing: !0 }))), + emitTagAndPreviousTextNode()) + : (B = 4); + } + function stateMarkupDeclarationOpen(o) { + '--' === s.substr(j, 2) + ? ((j += 2), (V = new MO(__assign(__assign({}, V), { type: 'comment' }))), (B = 14)) + : 'DOCTYPE' === s.substr(j, 7).toUpperCase() + ? ((j += 7), (V = new MO(__assign(__assign({}, V), { type: 'doctype' }))), (B = 20)) + : resetToDataState(); + } + function stateCommentStart(s) { + '-' === s ? (B = 15) : '>' === s ? resetToDataState() : (B = 16); + } + function stateCommentStartDash(s) { + '-' === s ? (B = 18) : '>' === s ? resetToDataState() : (B = 16); + } + function stateComment(s) { + '-' === s && (B = 17); + } + function stateCommentEndDash(s) { + B = '-' === s ? 18 : 16; + } + function stateCommentEnd(s) { + '>' === s ? emitTagAndPreviousTextNode() : '!' === s ? (B = 19) : '-' === s || (B = 16); + } + function stateCommentEndBang(s) { + '-' === s ? (B = 17) : '>' === s ? emitTagAndPreviousTextNode() : (B = 16); + } + function stateDoctype(s) { + '>' === s ? emitTagAndPreviousTextNode() : '<' === s && startNewTag(); + } + function resetToDataState() { + ((B = 0), (V = C)); + } + function startNewTag() { + ((B = 1), (V = new MO({ idx: j }))); + } + function emitTagAndPreviousTextNode() { + var o = s.slice($, V.idx); + (o && _(o, $), + 'comment' === V.type + ? w(V.idx) + : 'doctype' === V.type + ? x(V.idx) + : (V.isOpening && i(V.name, V.idx), V.isClosing && u(V.name, V.idx)), + resetToDataState(), + ($ = j + 1)); + } + function captureTagName() { + var o = V.idx + (V.isClosing ? 2 : 1); + return s.slice(o, j).toLowerCase(); + } + $ < j && + (function emitText() { + var o = s.slice($, j); + (_(o, $), ($ = j + 1)); + })(); + } + var MO = function MO(s) { + (void 0 === s && (s = {}), + (this.idx = void 0 !== s.idx ? s.idx : -1), + (this.type = s.type || 'tag'), + (this.name = s.name || ''), + (this.isOpening = !!s.isOpening), + (this.isClosing = !!s.isClosing)); + }, + TO = (function () { + function Autolinker(s) { + (void 0 === s && (s = {}), + (this.version = Autolinker.version), + (this.urls = {}), + (this.email = !0), + (this.phone = !0), + (this.hashtag = !1), + (this.mention = !1), + (this.newWindow = !0), + (this.stripPrefix = { scheme: !0, www: !0 }), + (this.stripTrailingSlash = !0), + (this.decodePercentEncoding = !0), + (this.truncate = { length: 0, location: 'end' }), + (this.className = ''), + (this.replaceFn = null), + (this.context = void 0), + (this.sanitizeHtml = !1), + (this.matchers = null), + (this.tagBuilder = null), + (this.urls = this.normalizeUrlsCfg(s.urls)), + (this.email = 'boolean' == typeof s.email ? s.email : this.email), + (this.phone = 'boolean' == typeof s.phone ? s.phone : this.phone), + (this.hashtag = s.hashtag || this.hashtag), + (this.mention = s.mention || this.mention), + (this.newWindow = 'boolean' == typeof s.newWindow ? s.newWindow : this.newWindow), + (this.stripPrefix = this.normalizeStripPrefixCfg(s.stripPrefix)), + (this.stripTrailingSlash = + 'boolean' == typeof s.stripTrailingSlash + ? s.stripTrailingSlash + : this.stripTrailingSlash), + (this.decodePercentEncoding = + 'boolean' == typeof s.decodePercentEncoding + ? s.decodePercentEncoding + : this.decodePercentEncoding), + (this.sanitizeHtml = s.sanitizeHtml || !1)); + var o = this.mention; + if (!1 !== o && -1 === ['twitter', 'instagram', 'soundcloud', 'tiktok'].indexOf(o)) + throw new Error("invalid `mention` cfg '".concat(o, "' - see docs")); + var i = this.hashtag; + if (!1 !== i && -1 === SO.indexOf(i)) + throw new Error("invalid `hashtag` cfg '".concat(i, "' - see docs")); + ((this.truncate = this.normalizeTruncateCfg(s.truncate)), + (this.className = s.className || this.className), + (this.replaceFn = s.replaceFn || this.replaceFn), + (this.context = s.context || this)); + } + return ( + (Autolinker.link = function (s, o) { + return new Autolinker(o).link(s); + }), + (Autolinker.parse = function (s, o) { + return new Autolinker(o).parse(s); + }), + (Autolinker.prototype.normalizeUrlsCfg = function (s) { + return ( + null == s && (s = !0), + 'boolean' == typeof s + ? { schemeMatches: s, wwwMatches: s, tldMatches: s } + : { + schemeMatches: 'boolean' != typeof s.schemeMatches || s.schemeMatches, + wwwMatches: 'boolean' != typeof s.wwwMatches || s.wwwMatches, + tldMatches: 'boolean' != typeof s.tldMatches || s.tldMatches + } + ); + }), + (Autolinker.prototype.normalizeStripPrefixCfg = function (s) { + return ( + null == s && (s = !0), + 'boolean' == typeof s + ? { scheme: s, www: s } + : { + scheme: 'boolean' != typeof s.scheme || s.scheme, + www: 'boolean' != typeof s.www || s.www + } + ); + }), + (Autolinker.prototype.normalizeTruncateCfg = function (s) { + return 'number' == typeof s + ? { length: s, location: 'end' } + : (function defaults(s, o) { + for (var i in o) o.hasOwnProperty(i) && void 0 === s[i] && (s[i] = o[i]); + return s; + })(s || {}, { length: Number.POSITIVE_INFINITY, location: 'end' }); + }), + (Autolinker.prototype.parse = function (s) { + var o = this, + i = ['a', 'style', 'script'], + u = 0, + _ = []; + return ( + parseHtml(s, { + onOpenTag: function (s) { + i.indexOf(s) >= 0 && u++; + }, + onText: function (s, i) { + if (0 === u) { + var w = (function splitAndCapture(s, o) { + if (!o.global) + throw new Error("`splitRegex` must have the 'g' flag set"); + for (var i, u = [], _ = 0; (i = o.exec(s)); ) + (u.push(s.substring(_, i.index)), + u.push(i[0]), + (_ = i.index + i[0].length)); + return (u.push(s.substring(_)), u); + })(s, /( | |<|<|>|>|"|"|')/gi), + x = i; + w.forEach(function (s, i) { + if (i % 2 == 0) { + var u = o.parseText(s, x); + _.push.apply(_, u); + } + x += s.length; + }); + } + }, + onCloseTag: function (s) { + i.indexOf(s) >= 0 && (u = Math.max(u - 1, 0)); + }, + onComment: function (s) {}, + onDoctype: function (s) {} + }), + (_ = this.compactMatches(_)), + (_ = this.removeUnwantedMatches(_)) + ); + }), + (Autolinker.prototype.compactMatches = function (s) { + s.sort(function (s, o) { + return s.getOffset() - o.getOffset(); + }); + for (var o = 0; o < s.length - 1; ) { + var i = s[o], + u = i.getOffset(), + _ = i.getMatchedText().length, + w = u + _; + if (o + 1 < s.length) { + if (s[o + 1].getOffset() === u) { + var x = s[o + 1].getMatchedText().length > _ ? o : o + 1; + s.splice(x, 1); + continue; + } + if (s[o + 1].getOffset() < w) { + s.splice(o + 1, 1); + continue; + } + } + o++; + } + return s; + }), + (Autolinker.prototype.removeUnwantedMatches = function (s) { + return ( + this.hashtag || + utils_remove(s, function (s) { + return 'hashtag' === s.getType(); + }), + this.email || + utils_remove(s, function (s) { + return 'email' === s.getType(); + }), + this.phone || + utils_remove(s, function (s) { + return 'phone' === s.getType(); + }), + this.mention || + utils_remove(s, function (s) { + return 'mention' === s.getType(); + }), + this.urls.schemeMatches || + utils_remove(s, function (s) { + return 'url' === s.getType() && 'scheme' === s.getUrlMatchType(); + }), + this.urls.wwwMatches || + utils_remove(s, function (s) { + return 'url' === s.getType() && 'www' === s.getUrlMatchType(); + }), + this.urls.tldMatches || + utils_remove(s, function (s) { + return 'url' === s.getType() && 'tld' === s.getUrlMatchType(); + }), + s + ); + }), + (Autolinker.prototype.parseText = function (s, o) { + (void 0 === o && (o = 0), (o = o || 0)); + for (var i = this.getMatchers(), u = [], _ = 0, w = i.length; _ < w; _++) { + for (var x = i[_].parseMatches(s), C = 0, j = x.length; C < j; C++) + x[C].setOffset(o + x[C].getOffset()); + u.push.apply(u, x); + } + return u; + }), + (Autolinker.prototype.link = function (s) { + if (!s) return ''; + this.sanitizeHtml && (s = s.replace(//g, '>')); + for (var o = this.parse(s), i = [], u = 0, _ = 0, w = o.length; _ < w; _++) { + var x = o[_]; + (i.push(s.substring(u, x.getOffset())), + i.push(this.createMatchReturnVal(x)), + (u = x.getOffset() + x.getMatchedText().length)); + } + return (i.push(s.substring(u)), i.join('')); + }), + (Autolinker.prototype.createMatchReturnVal = function (s) { + var o; + return ( + this.replaceFn && (o = this.replaceFn.call(this.context, s)), + 'string' == typeof o + ? o + : !1 === o + ? s.getMatchedText() + : o instanceof $C + ? o.toAnchorString() + : s.buildTag().toAnchorString() + ); + }), + (Autolinker.prototype.getMatchers = function () { + if (this.matchers) return this.matchers; + var s = this.getTagBuilder(), + o = [ + new wO({ tagBuilder: s, serviceName: this.hashtag }), + new mO({ tagBuilder: s }), + new kO({ tagBuilder: s }), + new PO({ tagBuilder: s, serviceName: this.mention }), + new _O({ + tagBuilder: s, + stripPrefix: this.stripPrefix, + stripTrailingSlash: this.stripTrailingSlash, + decodePercentEncoding: this.decodePercentEncoding + }) + ]; + return (this.matchers = o); + }), + (Autolinker.prototype.getTagBuilder = function () { + var s = this.tagBuilder; + return ( + s || + (s = this.tagBuilder = + new VC({ + newWindow: this.newWindow, + truncate: this.truncate, + className: this.className + })), + s + ); + }), + (Autolinker.version = '3.16.2'), + (Autolinker.AnchorTagBuilder = VC), + (Autolinker.HtmlTag = $C), + (Autolinker.matcher = { + Email: mO, + Hashtag: wO, + Matcher: YC, + Mention: PO, + Phone: kO, + Url: _O + }), + (Autolinker.match = { + Email: WC, + Hashtag: KC, + Match: UC, + Mention: HC, + Phone: JC, + Url: GC + }), + Autolinker + ); + })(); + const NO = TO; + var RO = /www|@|\:\/\//; + function isLinkOpen(s) { + return /^\s]/i.test(s); + } + function isLinkClose(s) { + return /^<\/a\s*>/i.test(s); + } + function createLinkifier() { + var s = [], + o = new NO({ + stripPrefix: !1, + url: !0, + email: !0, + replaceFn: function (o) { + switch (o.getType()) { + case 'url': + s.push({ text: o.matchedText, url: o.getUrl() }); + break; + case 'email': + s.push({ + text: o.matchedText, + url: 'mailto:' + o.getEmail().replace(/^mailto:/i, '') + }); + } + return !1; + } + }); + return { links: s, autolinker: o }; + } + function parseTokens(s) { + var o, + i, + u, + _, + w, + x, + C, + j, + L, + B, + $, + V, + U, + z = s.tokens, + Y = null; + for (i = 0, u = z.length; i < u; i++) + if ('inline' === z[i].type) + for ($ = 0, o = (_ = z[i].children).length - 1; o >= 0; o--) + if ('link_close' !== (w = _[o]).type) { + if ( + ('htmltag' === w.type && + (isLinkOpen(w.content) && $ > 0 && $--, isLinkClose(w.content) && $++), + !($ > 0) && 'text' === w.type && RO.test(w.content)) + ) { + if ( + (Y || ((V = (Y = createLinkifier()).links), (U = Y.autolinker)), + (x = w.content), + (V.length = 0), + U.link(x), + !V.length) + ) + continue; + for (C = [], B = w.level, j = 0; j < V.length; j++) + s.inline.validateLink(V[j].url) && + ((L = x.indexOf(V[j].text)) && + C.push({ type: 'text', content: x.slice(0, L), level: B }), + C.push({ type: 'link_open', href: V[j].url, title: '', level: B++ }), + C.push({ type: 'text', content: V[j].text, level: B }), + C.push({ type: 'link_close', level: --B }), + (x = x.slice(L + V[j].text.length))); + (x.length && C.push({ type: 'text', content: x, level: B }), + (z[i].children = _ = [].concat(_.slice(0, o), C, _.slice(o + 1)))); + } + } else for (o--; _[o].level !== w.level && 'link_open' !== _[o].type; ) o--; + } + function linkify(s) { + s.core.ruler.push('linkify', parseTokens); + } + var DO = __webpack_require__(42838), + LO = __webpack_require__.n(DO); + LO().addHook && + LO().addHook('beforeSanitizeElements', function (s) { + return (s.href && s.setAttribute('rel', 'noopener noreferrer'), s); + }); + const BO = function Markdown({ + source: s, + className: o = '', + getConfigs: i = () => ({ useUnsafeMarkdown: !1 }) + }) { + if ('string' != typeof s) return null; + const u = new Remarkable({ + html: !0, + typographer: !0, + breaks: !0, + linkTarget: '_blank' + }).use(linkify); + u.core.ruler.disable(['replacements', 'smartquotes']); + const { useUnsafeMarkdown: _ } = i(), + w = u.render(s), + x = sanitizer(w, { useUnsafeMarkdown: _ }); + return s && w && x + ? Pe.createElement('div', { + className: Hn()(o, 'markdown'), + dangerouslySetInnerHTML: { __html: x } + }) + : null; + }; + function sanitizer(s, { useUnsafeMarkdown: o = !1 } = {}) { + const i = o, + u = o ? [] : ['style', 'class']; + return ( + o && + !sanitizer.hasWarnedAboutDeprecation && + (console.warn( + 'useUnsafeMarkdown display configuration parameter is deprecated since >3.26.0 and will be removed in v4.0.0.' + ), + (sanitizer.hasWarnedAboutDeprecation = !0)), + LO().sanitize(s, { + ADD_ATTR: ['target'], + FORBID_TAGS: ['style', 'form'], + ALLOW_DATA_ATTR: i, + FORBID_ATTR: u + }) + ); + } + sanitizer.hasWarnedAboutDeprecation = !1; + class BaseLayout extends Pe.Component { + render() { + const { errSelectors: s, specSelectors: o, getComponent: i } = this.props, + u = i('SvgAssets'), + _ = i('InfoContainer', !0), + w = i('VersionPragmaFilter'), + x = i('operations', !0), + C = i('Models', !0), + j = i('Webhooks', !0), + L = i('Row'), + B = i('Col'), + $ = i('errors', !0), + V = i('ServersContainer', !0), + U = i('SchemesContainer', !0), + z = i('AuthorizeBtnContainer', !0), + Y = i('FilterContainer', !0), + Z = o.isSwagger2(), + ee = o.isOAS3(), + ie = o.isOAS31(), + ae = !o.specStr(), + le = o.loadingStatus(); + let ce = null; + if ( + ('loading' === le && + (ce = Pe.createElement( + 'div', + { className: 'info' }, + Pe.createElement( + 'div', + { className: 'loading-container' }, + Pe.createElement('div', { className: 'loading' }) + ) + )), + 'failed' === le && + (ce = Pe.createElement( + 'div', + { className: 'info' }, + Pe.createElement( + 'div', + { className: 'loading-container' }, + Pe.createElement( + 'h4', + { className: 'title' }, + 'Failed to load API definition.' + ), + Pe.createElement($, null) + ) + )), + 'failedConfig' === le) + ) { + const o = s.lastError(), + i = o ? o.get('message') : ''; + ce = Pe.createElement( + 'div', + { className: 'info failed-config' }, + Pe.createElement( + 'div', + { className: 'loading-container' }, + Pe.createElement( + 'h4', + { className: 'title' }, + 'Failed to load remote configuration.' + ), + Pe.createElement('p', null, i) + ) + ); + } + if ( + (!ce && ae && (ce = Pe.createElement('h4', null, 'No API definition provided.')), ce) + ) + return Pe.createElement( + 'div', + { className: 'swagger-ui' }, + Pe.createElement('div', { className: 'loading-container' }, ce) + ); + const pe = o.servers(), + de = o.schemes(), + fe = pe && pe.size, + ye = de && de.size, + be = !!o.securityDefinitions(); + return Pe.createElement( + 'div', + { className: 'swagger-ui' }, + Pe.createElement(u, null), + Pe.createElement( + w, + { isSwagger2: Z, isOAS3: ee, alsoShow: Pe.createElement($, null) }, + Pe.createElement($, null), + Pe.createElement( + L, + { className: 'information-container' }, + Pe.createElement(B, { mobile: 12 }, Pe.createElement(_, null)) + ), + fe || ye || be + ? Pe.createElement( + 'div', + { className: 'scheme-container' }, + Pe.createElement( + B, + { className: 'schemes wrapper', mobile: 12 }, + fe || ye + ? Pe.createElement( + 'div', + { className: 'schemes-server-container' }, + fe ? Pe.createElement(V, null) : null, + ye ? Pe.createElement(U, null) : null + ) + : null, + be ? Pe.createElement(z, null) : null + ) + ) + : null, + Pe.createElement(Y, null), + Pe.createElement( + L, + null, + Pe.createElement(B, { mobile: 12, desktop: 12 }, Pe.createElement(x, null)) + ), + ie && + Pe.createElement( + L, + { className: 'webhooks-container' }, + Pe.createElement(B, { mobile: 12, desktop: 12 }, Pe.createElement(j, null)) + ), + Pe.createElement( + L, + null, + Pe.createElement(B, { mobile: 12, desktop: 12 }, Pe.createElement(C, null)) + ) + ) + ); + } + } + const core_components = () => ({ + components: { + App: fk, + authorizationPopup: AuthorizationPopup, + authorizeBtn: AuthorizeBtn, + AuthorizeBtnContainer, + authorizeOperationBtn: AuthorizeOperationBtn, + auths: Auths, + AuthItem: auth_item_Auths, + authError: AuthError, + oauth2: Oauth2, + apiKeyAuth: ApiKeyAuth, + basicAuth: BasicAuth, + clear: Clear, + liveResponse: LiveResponse, + InitializedInput, + info: qk, + InfoContainer, + InfoUrl, + InfoBasePath, + Contact: Vk, + License: zk, + JumpToPath, + CopyToClipboardBtn, + onlineValidatorBadge: OnlineValidatorBadge, + operations: Operations, + operation: operation_Operation, + OperationSummary, + OperationSummaryMethod, + OperationSummaryPath, + responses: responses_Responses, + response: response_Response, + ResponseExtension: response_extension, + responseBody: ResponseBody, + parameters: Parameters, + parameterRow: ParameterRow, + execute: Execute, + headers: headers_Headers, + errors: Errors, + contentType: ContentType, + overview: Overview, + footer: Footer, + FilterContainer, + ParamBody, + curl: Curl, + Property: property, + TryItOutButton, + Markdown: BO, + BaseLayout, + VersionPragmaFilter, + VersionStamp: version_stamp, + OperationExt: operation_extensions, + OperationExtRow: operation_extension_row, + ParameterExt: parameter_extension, + ParameterIncludeEmpty, + OperationTag, + OperationContainer, + OpenAPIVersion: openapi_version, + DeepLink: deep_link, + SvgAssets: svg_assets, + Example: example_Example, + ExamplesSelect, + ExamplesSelectValueRetainer + } + }), + form_components = () => ({ components: { ...ye } }), + base = () => [ + configsPlugin, + util, + logs, + view, + view_legacy, + plugins_spec, + err, + icons, + plugins_layout, + json_schema_5, + json_schema_5_samples, + core_components, + form_components, + swagger_client, + auth, + downloadUrlPlugin, + deep_linking, + filter, + on_complete, + plugins_request_snippets, + syntax_highlighting, + versions, + safe_render() + ], + FO = (0, qe.Map)(); + function onlyOAS3(s) { + return (o, i) => + (...u) => { + if (i.getSystem().specSelectors.isOAS3()) { + const o = s(...u); + return 'function' == typeof o ? o(i) : o; + } + return o(...u); + }; + } + const qO = onlyOAS3(Ss()(null)), + $O = onlyOAS3((s, o) => (s) => s.getSystem().specSelectors.findSchema(o)), + VO = onlyOAS3(() => (s) => { + const o = s.getSystem().specSelectors.specJson().getIn(['components', 'schemas']); + return qe.Map.isMap(o) ? o : FO; + }), + UO = onlyOAS3(() => (s) => s.getSystem().specSelectors.specJson().hasIn(['servers', 0])), + zO = onlyOAS3(Ut(Ms, (s) => s.getIn(['components', 'securitySchemes']) || null)), + wrap_selectors_validOperationMethods = + (s, o) => + (i, ...u) => + o.specSelectors.isOAS3() ? o.oas3Selectors.validOperationMethods() : s(...u), + WO = qO, + KO = qO, + HO = qO, + JO = qO, + GO = qO; + const YO = (function wrap_selectors_onlyOAS3(s) { + return (o, i) => + (...u) => { + if (i.getSystem().specSelectors.isOAS3()) { + let o = i + .getState() + .getIn(['spec', 'resolvedSubtrees', 'components', 'securitySchemes']); + return s(i, o, ...u); + } + return o(...u); + }; + })( + Ut( + (s) => s, + ({ specSelectors: s }) => s.securityDefinitions(), + (s, o) => { + let i = (0, qe.List)(); + return o + ? (o.entrySeq().forEach(([s, o]) => { + const u = o.get('type'); + if ( + ('oauth2' === u && + o + .get('flows') + .entrySeq() + .forEach(([u, _]) => { + let w = (0, qe.fromJS)({ + flow: u, + authorizationUrl: _.get('authorizationUrl'), + tokenUrl: _.get('tokenUrl'), + scopes: _.get('scopes'), + type: o.get('type'), + description: o.get('description') + }); + i = i.push(new qe.Map({ [s]: w.filter((s) => void 0 !== s) })); + }), + ('http' !== u && 'apiKey' !== u) || (i = i.push(new qe.Map({ [s]: o }))), + 'openIdConnect' === u && o.get('openIdConnectData')) + ) { + let u = o.get('openIdConnectData'); + ( + u.get('grant_types_supported') || ['authorization_code', 'implicit'] + ).forEach((_) => { + let w = + u.get('scopes_supported') && + u.get('scopes_supported').reduce((s, o) => s.set(o, ''), new qe.Map()), + x = (0, qe.fromJS)({ + flow: _, + authorizationUrl: u.get('authorization_endpoint'), + tokenUrl: u.get('token_endpoint'), + scopes: w, + type: 'oauth2', + openIdConnectUrl: o.get('openIdConnectUrl') + }); + i = i.push(new qe.Map({ [s]: x.filter((s) => void 0 !== s) })); + }); + } + }), + i) + : i; + } + ) + ); + function OAS3ComponentWrapFactory(s) { + return (o, i) => (u) => + 'function' == typeof i.specSelectors?.isOAS3 + ? i.specSelectors.isOAS3() + ? Pe.createElement(s, Rn()({}, u, i, { Ori: o })) + : Pe.createElement(o, u) + : (console.warn("OAS3 wrapper: couldn't get spec"), null); + } + const XO = (0, qe.Map)(), + selectors_isSwagger2 = () => (s) => + (function isSwagger2(s) { + const o = s.get('swagger'); + return 'string' == typeof o && '2.0' === o; + })(s.getSystem().specSelectors.specJson()), + selectors_isOAS30 = () => (s) => + (function isOAS30(s) { + const o = s.get('openapi'); + return 'string' == typeof o && /^3\.0\.([0123])(?:-rc[012])?$/.test(o); + })(s.getSystem().specSelectors.specJson()), + selectors_isOAS3 = () => (s) => s.getSystem().specSelectors.isOAS30(); + function selectors_onlyOAS3(s) { + return (o, ...i) => + (u) => { + if (u.specSelectors.isOAS3()) { + const _ = s(o, ...i); + return 'function' == typeof _ ? _(u) : _; + } + return null; + }; + } + const ZO = selectors_onlyOAS3(() => (s) => s.specSelectors.specJson().get('servers', XO)), + findSchema = (s, o) => { + const i = s.getIn(['resolvedSubtrees', 'components', 'schemas', o], null), + u = s.getIn(['json', 'components', 'schemas', o], null); + return i || u || null; + }, + QO = selectors_onlyOAS3((s, { callbacks: o, specPath: i }) => (s) => { + const u = s.specSelectors.validOperationMethods(); + return qe.Map.isMap(o) + ? o + .reduce( + (s, o, _) => { + if (!qe.Map.isMap(o)) return s; + const w = o.reduce( + (s, o, w) => { + if (!qe.Map.isMap(o)) return s; + const x = o + .entrySeq() + .filter(([s]) => u.includes(s)) + .map(([s, o]) => ({ + operation: (0, qe.Map)({ operation: o }), + method: s, + path: w, + callbackName: _, + specPath: i.concat([_, w, s]) + })); + return s.concat(x); + }, + (0, qe.List)() + ); + return s.concat(w); + }, + (0, qe.List)() + ) + .groupBy((s) => s.callbackName) + .map((s) => s.toArray()) + .toObject() + : {}; + }), + callbacks = ({ callbacks: s, specPath: o, specSelectors: i, getComponent: u }) => { + const _ = i.callbacksOperations({ callbacks: s, specPath: o }), + w = Object.keys(_), + x = u('OperationContainer', !0); + return 0 === w.length + ? Pe.createElement('span', null, 'No callbacks') + : Pe.createElement( + 'div', + null, + w.map((s) => + Pe.createElement( + 'div', + { key: `${s}` }, + Pe.createElement('h2', null, s), + _[s].map((o) => + Pe.createElement(x, { + key: `${s}-${o.path}-${o.method}`, + op: o.operation, + tag: 'callbacks', + method: o.method, + path: o.path, + specPath: o.specPath, + allowTryItOut: !1 + }) + ) + ) + ) + ); + }, + getDefaultRequestBodyValue = (s, o, i, u) => { + const _ = s.getIn(['content', o]) ?? (0, qe.OrderedMap)(), + w = _.get('schema', (0, qe.OrderedMap)()).toJS(), + x = void 0 !== _.get('examples'), + C = _.get('example'), + j = x ? _.getIn(['examples', i, 'value']) : C; + return stringify(u.getSampleSchema(w, o, { includeWriteOnly: !0 }, j)); + }, + components_request_body = ({ + userHasEditedBody: s, + requestBody: o, + requestBodyValue: i, + requestBodyInclusionSetting: u, + requestBodyErrors: _, + getComponent: w, + getConfigs: x, + specSelectors: C, + fn: j, + contentType: L, + isExecute: B, + specPath: $, + onChange: V, + onChangeIncludeEmpty: U, + activeExamplesKey: z, + updateActiveExamplesKey: Y, + setRetainRequestBodyValueFlag: Z + }) => { + const handleFile = (s) => { + V(s.target.files[0]); + }, + setIsIncludedOptions = (s) => { + let o = { key: s, shouldDispatchInit: !1, defaultValue: !0 }; + return ('no value' === u.get(s, 'no value') && (o.shouldDispatchInit = !0), o); + }, + ee = w('Markdown', !0), + ie = w('modelExample'), + ae = w('RequestBodyEditor'), + le = w('HighlightCode', !0), + ce = w('ExamplesSelectValueRetainer'), + pe = w('Example'), + de = w('ParameterIncludeEmpty'), + { showCommonExtensions: fe } = x(), + ye = o?.get('description') ?? null, + be = o?.get('content') ?? new qe.OrderedMap(); + L = L || be.keySeq().first() || ''; + const _e = be.get(L) ?? (0, qe.OrderedMap)(), + we = _e.get('schema', (0, qe.OrderedMap)()), + Se = _e.get('examples', null), + xe = Se?.map((s, i) => { + const u = s?.get('value', null); + return (u && (s = s.set('value', getDefaultRequestBodyValue(o, L, i, j), u)), s); + }); + if (((_ = qe.List.isList(_) ? _ : (0, qe.List)()), !_e.size)) return null; + const Te = 'object' === _e.getIn(['schema', 'type']), + Re = 'binary' === _e.getIn(['schema', 'format']), + $e = 'base64' === _e.getIn(['schema', 'format']); + if ( + 'application/octet-stream' === L || + 0 === L.indexOf('image/') || + 0 === L.indexOf('audio/') || + 0 === L.indexOf('video/') || + Re || + $e + ) { + const s = w('Input'); + return B + ? Pe.createElement(s, { type: 'file', onChange: handleFile }) + : Pe.createElement( + 'i', + null, + 'Example values are not available for ', + Pe.createElement('code', null, L), + ' media types.' + ); + } + if ( + Te && + ('application/x-www-form-urlencoded' === L || 0 === L.indexOf('multipart/')) && + we.get('properties', (0, qe.OrderedMap)()).size > 0 + ) { + const s = w('JsonSchemaForm'), + o = w('ParameterExt'), + x = we.get('properties', (0, qe.OrderedMap)()); + return ( + (i = qe.Map.isMap(i) ? i : (0, qe.OrderedMap)()), + Pe.createElement( + 'div', + { className: 'table-container' }, + ye && Pe.createElement(ee, { source: ye }), + Pe.createElement( + 'table', + null, + Pe.createElement( + 'tbody', + null, + qe.Map.isMap(x) && + x.entrySeq().map(([x, C]) => { + if (C.get('readOnly')) return; + const L = C.get('oneOf')?.get(0)?.toJS(), + $ = C.get('anyOf')?.get(0)?.toJS(); + C = (0, qe.fromJS)(j.mergeJsonSchema(C.toJS(), L ?? $ ?? {})); + let z = fe ? getCommonExtensions(C) : null; + const Y = we.get('required', (0, qe.List)()).includes(x), + Z = C.get('type'), + ie = C.get('format'), + ae = C.get('description'), + le = i.getIn([x, 'value']), + ce = i.getIn([x, 'errors']) || _, + pe = u.get(x) || !1; + let ye = j.getSampleSchema(C, !1, { includeWriteOnly: !0 }); + (!1 === ye && (ye = 'false'), + 0 === ye && (ye = '0'), + 'string' != typeof ye && 'object' === Z && (ye = stringify(ye)), + 'string' == typeof ye && 'array' === Z && (ye = JSON.parse(ye))); + const be = 'string' === Z && ('binary' === ie || 'base64' === ie); + return Pe.createElement( + 'tr', + { key: x, className: 'parameters', 'data-property-name': x }, + Pe.createElement( + 'td', + { className: 'parameters-col_name' }, + Pe.createElement( + 'div', + { className: Y ? 'parameter__name required' : 'parameter__name' }, + x, + Y ? Pe.createElement('span', null, ' *') : null + ), + Pe.createElement( + 'div', + { className: 'parameter__type' }, + Z, + ie && + Pe.createElement( + 'span', + { className: 'prop-format' }, + '($', + ie, + ')' + ), + fe && z.size + ? z + .entrySeq() + .map(([s, i]) => + Pe.createElement(o, { key: `${s}-${i}`, xKey: s, xVal: i }) + ) + : null + ), + Pe.createElement( + 'div', + { className: 'parameter__deprecated' }, + C.get('deprecated') ? 'deprecated' : null + ) + ), + Pe.createElement( + 'td', + { className: 'parameters-col_description' }, + Pe.createElement(ee, { source: ae }), + B + ? Pe.createElement( + 'div', + null, + Pe.createElement(s, { + fn: j, + dispatchInitialValue: !be, + schema: C, + description: x, + getComponent: w, + value: void 0 === le ? ye : le, + required: Y, + errors: ce, + onChange: (s) => { + V(s, [x]); + } + }), + Y + ? null + : Pe.createElement(de, { + onChange: (s) => U(x, s), + isIncluded: pe, + isIncludedOptions: setIsIncludedOptions(x), + isDisabled: Array.isArray(le) + ? 0 !== le.length + : !isEmptyValue(le) + }) + ) + : null + ) + ); + }) + ) + ) + ) + ); + } + const ze = getDefaultRequestBodyValue(o, L, z, j); + let We = null; + return ( + getKnownSyntaxHighlighterLanguage(ze) && (We = 'json'), + Pe.createElement( + 'div', + null, + ye && Pe.createElement(ee, { source: ye }), + xe + ? Pe.createElement(ce, { + userHasEditedBody: s, + examples: xe, + currentKey: z, + currentUserInputValue: i, + onSelect: (s) => { + Y(s); + }, + updateValue: V, + defaultToFirstExample: !0, + getComponent: w, + setRetainRequestBodyValueFlag: Z + }) + : null, + B + ? Pe.createElement( + 'div', + null, + Pe.createElement(ae, { + value: i, + errors: _, + defaultValue: ze, + onChange: V, + getComponent: w + }) + ) + : Pe.createElement(ie, { + getComponent: w, + getConfigs: x, + specSelectors: C, + expandDepth: 1, + isExecute: B, + schema: _e.get('schema'), + specPath: $.push('content', L), + example: Pe.createElement( + le, + { className: 'body-param__example', language: We }, + stringify(i) || ze + ), + includeWriteOnly: !0 + }), + xe + ? Pe.createElement(pe, { example: xe.get(z), getComponent: w, getConfigs: x }) + : null + ) + ); + }; + class operation_link_OperationLink extends Pe.Component { + render() { + const { link: s, name: o, getComponent: i } = this.props, + u = i('Markdown', !0); + let _ = s.get('operationId') || s.get('operationRef'), + w = s.get('parameters') && s.get('parameters').toJS(), + x = s.get('description'); + return Pe.createElement( + 'div', + { className: 'operation-link' }, + Pe.createElement( + 'div', + { className: 'description' }, + Pe.createElement('b', null, Pe.createElement('code', null, o)), + x ? Pe.createElement(u, { source: x }) : null + ), + Pe.createElement( + 'pre', + null, + 'Operation `', + _, + '`', + Pe.createElement('br', null), + Pe.createElement('br', null), + 'Parameters ', + (function padString(s, o) { + if ('string' != typeof o) return ''; + return o + .split('\n') + .map((o, i) => (i > 0 ? Array(s + 1).join(' ') + o : o)) + .join('\n'); + })(0, JSON.stringify(w, null, 2)) || '{}', + Pe.createElement('br', null) + ) + ); + } + } + const eA = operation_link_OperationLink, + components_servers = ({ + servers: s, + currentServer: o, + setSelectedServer: i, + setServerVariableValue: u, + getServerVariable: _, + getEffectiveServerValue: w + }) => { + const x = + (s.find((s) => s.get('url') === o) || (0, qe.OrderedMap)()).get('variables') || + (0, qe.OrderedMap)(), + C = 0 !== x.size; + ((0, Pe.useEffect)(() => { + o || i(s.first()?.get('url')); + }, []), + (0, Pe.useEffect)(() => { + const _ = s.find((s) => s.get('url') === o); + if (!_) return void i(s.first().get('url')); + (_.get('variables') || (0, qe.OrderedMap)()).map((s, i) => { + u({ server: o, key: i, val: s.get('default') || '' }); + }); + }, [o, s])); + const j = (0, Pe.useCallback)( + (s) => { + i(s.target.value); + }, + [i] + ), + L = (0, Pe.useCallback)( + (s) => { + const i = s.target.getAttribute('data-variable'), + _ = s.target.value; + u({ server: o, key: i, val: _ }); + }, + [u, o] + ); + return Pe.createElement( + 'div', + { className: 'servers' }, + Pe.createElement( + 'label', + { htmlFor: 'servers' }, + Pe.createElement( + 'select', + { onChange: j, value: o, id: 'servers' }, + s + .valueSeq() + .map((s) => + Pe.createElement( + 'option', + { value: s.get('url'), key: s.get('url') }, + s.get('url'), + s.get('description') && ` - ${s.get('description')}` + ) + ) + .toArray() + ) + ), + C && + Pe.createElement( + 'div', + null, + Pe.createElement( + 'div', + { className: 'computed-url' }, + 'Computed URL:', + Pe.createElement('code', null, w(o)) + ), + Pe.createElement('h4', null, 'Server variables'), + Pe.createElement( + 'table', + null, + Pe.createElement( + 'tbody', + null, + x.entrySeq().map(([s, i]) => + Pe.createElement( + 'tr', + { key: s }, + Pe.createElement('td', null, s), + Pe.createElement( + 'td', + null, + i.get('enum') + ? Pe.createElement( + 'select', + { 'data-variable': s, onChange: L }, + i + .get('enum') + .map((i) => + Pe.createElement( + 'option', + { selected: i === _(o, s), key: i, value: i }, + i + ) + ) + ) + : Pe.createElement('input', { + type: 'text', + value: _(o, s) || '', + onChange: L, + 'data-variable': s + }) + ) + ) + ) + ) + ) + ) + ); + }; + class ServersContainer extends Pe.Component { + render() { + const { + specSelectors: s, + oas3Selectors: o, + oas3Actions: i, + getComponent: u + } = this.props, + _ = s.servers(), + w = u('Servers'); + return _ && _.size + ? Pe.createElement( + 'div', + null, + Pe.createElement('span', { className: 'servers-title' }, 'Servers'), + Pe.createElement(w, { + servers: _, + currentServer: o.selectedServer(), + setSelectedServer: i.setSelectedServer, + setServerVariableValue: i.setServerVariableValue, + getServerVariable: o.serverVariableValue, + getEffectiveServerValue: o.serverEffectiveValue + }) + ) + : null; + } + } + const tA = Function.prototype; + class RequestBodyEditor extends Pe.PureComponent { + static defaultProps = { onChange: tA, userHasEditedBody: !1 }; + constructor(s, o) { + (super(s, o), + (this.state = { value: stringify(s.value) || s.defaultValue }), + s.onChange(s.value)); + } + applyDefaultValue = (s) => { + const { onChange: o, defaultValue: i } = s || this.props; + return (this.setState({ value: i }), o(i)); + }; + onChange = (s) => { + this.props.onChange(stringify(s)); + }; + onDomChange = (s) => { + const o = s.target.value; + this.setState({ value: o }, () => this.onChange(o)); + }; + UNSAFE_componentWillReceiveProps(s) { + (this.props.value !== s.value && + s.value !== this.state.value && + this.setState({ value: stringify(s.value) }), + !s.value && s.defaultValue && this.state.value && this.applyDefaultValue(s)); + } + render() { + let { getComponent: s, errors: o } = this.props, + { value: i } = this.state, + u = o.size > 0; + const _ = s('TextArea'); + return Pe.createElement( + 'div', + { className: 'body-param' }, + Pe.createElement(_, { + className: Hn()('body-param__text', { invalid: u }), + title: o.size ? o.join(', ') : '', + value: i, + onChange: this.onDomChange + }) + ); + } + } + class HttpAuth extends Pe.Component { + constructor(s, o) { + super(s, o); + let { name: i, schema: u } = this.props, + _ = this.getValue(); + this.state = { name: i, schema: u, value: _ }; + } + getValue() { + let { name: s, authorized: o } = this.props; + return o && o.getIn([s, 'value']); + } + onChange = (s) => { + let { onChange: o } = this.props, + { value: i, name: u } = s.target, + _ = Object.assign({}, this.state.value); + (u ? (_[u] = i) : (_ = i), this.setState({ value: _ }, () => o(this.state))); + }; + render() { + let { schema: s, getComponent: o, errSelectors: i, name: u } = this.props; + const _ = o('Input'), + w = o('Row'), + x = o('Col'), + C = o('authError'), + j = o('Markdown', !0), + L = o('JumpToPath', !0), + B = (s.get('scheme') || '').toLowerCase(); + let $ = this.getValue(), + V = i.allErrors().filter((s) => s.get('authId') === u); + if ('basic' === B) { + let o = $ ? $.get('username') : null; + return Pe.createElement( + 'div', + null, + Pe.createElement( + 'h4', + null, + Pe.createElement('code', null, u || s.get('name')), + '  (http, Basic)', + Pe.createElement(L, { path: ['securityDefinitions', u] }) + ), + o && Pe.createElement('h6', null, 'Authorized'), + Pe.createElement(w, null, Pe.createElement(j, { source: s.get('description') })), + Pe.createElement( + w, + null, + Pe.createElement('label', { htmlFor: 'auth-basic-username' }, 'Username:'), + o + ? Pe.createElement('code', null, ' ', o, ' ') + : Pe.createElement( + x, + null, + Pe.createElement(_, { + id: 'auth-basic-username', + type: 'text', + required: 'required', + name: 'username', + 'aria-label': 'auth-basic-username', + onChange: this.onChange, + autoFocus: !0 + }) + ) + ), + Pe.createElement( + w, + null, + Pe.createElement('label', { htmlFor: 'auth-basic-password' }, 'Password:'), + o + ? Pe.createElement('code', null, ' ****** ') + : Pe.createElement( + x, + null, + Pe.createElement(_, { + id: 'auth-basic-password', + autoComplete: 'new-password', + name: 'password', + type: 'password', + 'aria-label': 'auth-basic-password', + onChange: this.onChange + }) + ) + ), + V.valueSeq().map((s, o) => Pe.createElement(C, { error: s, key: o })) + ); + } + return 'bearer' === B + ? Pe.createElement( + 'div', + null, + Pe.createElement( + 'h4', + null, + Pe.createElement('code', null, u || s.get('name')), + '  (http, Bearer)', + Pe.createElement(L, { path: ['securityDefinitions', u] }) + ), + $ && Pe.createElement('h6', null, 'Authorized'), + Pe.createElement(w, null, Pe.createElement(j, { source: s.get('description') })), + Pe.createElement( + w, + null, + Pe.createElement('label', { htmlFor: 'auth-bearer-value' }, 'Value:'), + $ + ? Pe.createElement('code', null, ' ****** ') + : Pe.createElement( + x, + null, + Pe.createElement(_, { + id: 'auth-bearer-value', + type: 'text', + 'aria-label': 'auth-bearer-value', + onChange: this.onChange, + autoFocus: !0 + }) + ) + ), + V.valueSeq().map((s, o) => Pe.createElement(C, { error: s, key: o })) + ) + : Pe.createElement( + 'div', + null, + Pe.createElement( + 'em', + null, + Pe.createElement('b', null, u), + ' HTTP authentication: unsupported scheme ', + `'${B}'` + ) + ); + } + } + class operation_servers_OperationServers extends Pe.Component { + setSelectedServer = (s) => { + const { path: o, method: i } = this.props; + return (this.forceUpdate(), this.props.setSelectedServer(s, `${o}:${i}`)); + }; + setServerVariableValue = (s) => { + const { path: o, method: i } = this.props; + return ( + this.forceUpdate(), + this.props.setServerVariableValue({ ...s, namespace: `${o}:${i}` }) + ); + }; + getSelectedServer = () => { + const { path: s, method: o } = this.props; + return this.props.getSelectedServer(`${s}:${o}`); + }; + getServerVariable = (s, o) => { + const { path: i, method: u } = this.props; + return this.props.getServerVariable({ namespace: `${i}:${u}`, server: s }, o); + }; + getEffectiveServerValue = (s) => { + const { path: o, method: i } = this.props; + return this.props.getEffectiveServerValue({ server: s, namespace: `${o}:${i}` }); + }; + render() { + const { operationServers: s, pathServers: o, getComponent: i } = this.props; + if (!s && !o) return null; + const u = i('Servers'), + _ = s || o, + w = s ? 'operation' : 'path'; + return Pe.createElement( + 'div', + { className: 'opblock-section operation-servers' }, + Pe.createElement( + 'div', + { className: 'opblock-section-header' }, + Pe.createElement( + 'div', + { className: 'tab-header' }, + Pe.createElement('h4', { className: 'opblock-title' }, 'Servers') + ) + ), + Pe.createElement( + 'div', + { className: 'opblock-description-wrapper' }, + Pe.createElement( + 'h4', + { className: 'message' }, + 'These ', + w, + '-level options override the global server options.' + ), + Pe.createElement(u, { + servers: _, + currentServer: this.getSelectedServer(), + setSelectedServer: this.setSelectedServer, + setServerVariableValue: this.setServerVariableValue, + getServerVariable: this.getServerVariable, + getEffectiveServerValue: this.getEffectiveServerValue + }) + ) + ); + } + } + const rA = { + Callbacks: callbacks, + HttpAuth, + RequestBody: components_request_body, + Servers: components_servers, + ServersContainer, + RequestBodyEditor, + OperationServers: operation_servers_OperationServers, + operationLink: eA + }, + nA = new Remarkable('commonmark'); + (nA.block.ruler.enable(['table']), nA.set({ linkTarget: '_blank' })); + const sA = OAS3ComponentWrapFactory( + ({ + source: s, + className: o = '', + getConfigs: i = () => ({ useUnsafeMarkdown: !1 }) + }) => { + if ('string' != typeof s) return null; + if (s) { + const { useUnsafeMarkdown: u } = i(), + _ = sanitizer(nA.render(s), { useUnsafeMarkdown: u }); + let w; + return ( + 'string' == typeof _ && (w = _.trim()), + Pe.createElement('div', { + dangerouslySetInnerHTML: { __html: w }, + className: Hn()(o, 'renderedMarkdown') + }) + ); + } + return null; + } + ), + oA = OAS3ComponentWrapFactory(({ Ori: s, ...o }) => { + const { + schema: i, + getComponent: u, + errSelectors: _, + authorized: w, + onAuthChange: x, + name: C + } = o, + j = u('HttpAuth'); + return 'http' === i.get('type') + ? Pe.createElement(j, { + key: C, + schema: i, + name: C, + errSelectors: _, + authorized: w, + getComponent: u, + onChange: x + }) + : Pe.createElement(s, o); + }), + iA = OAS3ComponentWrapFactory(OnlineValidatorBadge); + class ModelComponent extends Pe.Component { + render() { + let { getConfigs: s, schema: o, Ori: i } = this.props, + u = ['model-box'], + _ = null; + return ( + !0 === o.get('deprecated') && + (u.push('deprecated'), + (_ = Pe.createElement( + 'span', + { className: 'model-deprecated-warning' }, + 'Deprecated:' + ))), + Pe.createElement( + 'div', + { className: u.join(' ') }, + _, + Pe.createElement( + i, + Rn()({}, this.props, { + getConfigs: s, + depth: 1, + expandDepth: this.props.expandDepth || 0 + }) + ) + ) + ); + } + } + const aA = OAS3ComponentWrapFactory(ModelComponent), + lA = OAS3ComponentWrapFactory(({ Ori: s, ...o }) => { + const { schema: i, getComponent: u, errors: _, onChange: w } = o, + x = i && i.get ? i.get('format') : null, + C = i && i.get ? i.get('type') : null, + j = u('Input'); + return C && 'string' === C && x && ('binary' === x || 'base64' === x) + ? Pe.createElement(j, { + type: 'file', + className: _.length ? 'invalid' : '', + title: _.length ? _ : '', + onChange: (s) => { + w(s.target.files[0]); + }, + disabled: s.isDisabled + }) + : Pe.createElement(s, o); + }), + cA = { + Markdown: sA, + AuthItem: oA, + OpenAPIVersion: (function OAS30ComponentWrapFactory(s) { + return (o, i) => (u) => + 'function' == typeof i.specSelectors?.isOAS30 + ? i.specSelectors.isOAS30() + ? Pe.createElement(s, Rn()({}, u, i, { Ori: o })) + : Pe.createElement(o, u) + : (console.warn("OAS30 wrapper: couldn't get spec"), null); + })((s) => { + const { Ori: o } = s; + return Pe.createElement(o, { oasVersion: '3.0' }); + }), + JsonSchema_string: lA, + model: aA, + onlineValidatorBadge: iA + }, + uA = 'oas3_set_servers', + pA = 'oas3_set_request_body_value', + hA = 'oas3_set_request_body_retain_flag', + dA = 'oas3_set_request_body_inclusion', + fA = 'oas3_set_active_examples_member', + mA = 'oas3_set_request_content_type', + gA = 'oas3_set_response_content_type', + yA = 'oas3_set_server_variable_value', + vA = 'oas3_set_request_body_validate_error', + bA = 'oas3_clear_request_body_validate_error', + _A = 'oas3_clear_request_body_value'; + function setSelectedServer(s, o) { + return { type: uA, payload: { selectedServerUrl: s, namespace: o } }; + } + function setRequestBodyValue({ value: s, pathMethod: o }) { + return { type: pA, payload: { value: s, pathMethod: o } }; + } + const setRetainRequestBodyValueFlag = ({ value: s, pathMethod: o }) => ({ + type: hA, + payload: { value: s, pathMethod: o } + }); + function setRequestBodyInclusion({ value: s, pathMethod: o, name: i }) { + return { type: dA, payload: { value: s, pathMethod: o, name: i } }; + } + function setActiveExamplesMember({ + name: s, + pathMethod: o, + contextType: i, + contextName: u + }) { + return { type: fA, payload: { name: s, pathMethod: o, contextType: i, contextName: u } }; + } + function setRequestContentType({ value: s, pathMethod: o }) { + return { type: mA, payload: { value: s, pathMethod: o } }; + } + function setResponseContentType({ value: s, path: o, method: i }) { + return { type: gA, payload: { value: s, path: o, method: i } }; + } + function setServerVariableValue({ server: s, namespace: o, key: i, val: u }) { + return { type: yA, payload: { server: s, namespace: o, key: i, val: u } }; + } + const setRequestBodyValidateError = ({ path: s, method: o, validationErrors: i }) => ({ + type: vA, + payload: { path: s, method: o, validationErrors: i } + }), + clearRequestBodyValidateError = ({ path: s, method: o }) => ({ + type: bA, + payload: { path: s, method: o } + }), + initRequestBodyValidateError = ({ pathMethod: s }) => ({ + type: bA, + payload: { path: s[0], method: s[1] } + }), + clearRequestBodyValue = ({ pathMethod: s }) => ({ type: _A, payload: { pathMethod: s } }); + var EA = __webpack_require__(60680), + wA = __webpack_require__.n(EA); + const oas3_selectors_onlyOAS3 = + (s) => + (o, ...i) => + (u) => { + if (u.getSystem().specSelectors.isOAS3()) { + const _ = s(o, ...i); + return 'function' == typeof _ ? _(u) : _; + } + return null; + }; + const SA = oas3_selectors_onlyOAS3((s, o) => { + const i = o ? [o, 'selectedServer'] : ['selectedServer']; + return s.getIn(i) || ''; + }), + xA = oas3_selectors_onlyOAS3( + (s, o, i) => s.getIn(['requestData', o, i, 'bodyValue']) || null + ), + kA = oas3_selectors_onlyOAS3( + (s, o, i) => s.getIn(['requestData', o, i, 'retainBodyValue']) || !1 + ), + selectDefaultRequestBodyValue = (s, o, i) => (s) => { + const { oas3Selectors: u, specSelectors: _, fn: w } = s.getSystem(); + if (_.isOAS3()) { + const s = u.requestContentType(o, i); + if (s) + return getDefaultRequestBodyValue( + _.specResolvedSubtree(['paths', o, i, 'requestBody']), + s, + u.activeExamplesMember(o, i, 'requestBody', 'requestBody'), + w + ); + } + return null; + }, + CA = oas3_selectors_onlyOAS3((s, o, i) => (s) => { + const { oas3Selectors: u, specSelectors: _, fn: w } = s; + let x = !1; + const C = u.requestContentType(o, i); + let j = u.requestBodyValue(o, i); + const L = _.specResolvedSubtree(['paths', o, i, 'requestBody']); + if (!L) return !1; + if ( + (qe.Map.isMap(j) && + (j = stringify( + j.mapEntries((s) => (qe.Map.isMap(s[1]) ? [s[0], s[1].get('value')] : s)).toJS() + )), + qe.List.isList(j) && (j = stringify(j)), + C) + ) { + const s = getDefaultRequestBodyValue( + L, + C, + u.activeExamplesMember(o, i, 'requestBody', 'requestBody'), + w + ); + x = !!j && j !== s; + } + return x; + }), + OA = oas3_selectors_onlyOAS3( + (s, o, i) => s.getIn(['requestData', o, i, 'bodyInclusion']) || (0, qe.Map)() + ), + AA = oas3_selectors_onlyOAS3( + (s, o, i) => s.getIn(['requestData', o, i, 'errors']) || null + ), + jA = oas3_selectors_onlyOAS3( + (s, o, i, u, _) => s.getIn(['examples', o, i, u, _, 'activeExample']) || null + ), + IA = oas3_selectors_onlyOAS3( + (s, o, i) => s.getIn(['requestData', o, i, 'requestContentType']) || null + ), + PA = oas3_selectors_onlyOAS3( + (s, o, i) => s.getIn(['requestData', o, i, 'responseContentType']) || null + ), + MA = oas3_selectors_onlyOAS3((s, o, i) => { + let u; + if ('string' != typeof o) { + const { server: s, namespace: _ } = o; + u = _ ? [_, 'serverVariableValues', s, i] : ['serverVariableValues', s, i]; + } else { + u = ['serverVariableValues', o, i]; + } + return s.getIn(u) || null; + }), + TA = oas3_selectors_onlyOAS3((s, o) => { + let i; + if ('string' != typeof o) { + const { server: s, namespace: u } = o; + i = u ? [u, 'serverVariableValues', s] : ['serverVariableValues', s]; + } else { + i = ['serverVariableValues', o]; + } + return s.getIn(i) || (0, qe.OrderedMap)(); + }), + NA = oas3_selectors_onlyOAS3((s, o) => { + var i, u; + if ('string' != typeof o) { + const { server: _, namespace: w } = o; + ((u = _), + (i = w + ? s.getIn([w, 'serverVariableValues', u]) + : s.getIn(['serverVariableValues', u]))); + } else ((u = o), (i = s.getIn(['serverVariableValues', u]))); + i = i || (0, qe.OrderedMap)(); + let _ = u; + return ( + i.map((s, o) => { + _ = _.replace(new RegExp(`{${wA()(o)}}`, 'g'), s); + }), + _ + ); + }), + RA = (function validateRequestBodyIsRequired(s) { + return (...o) => + (i) => { + const u = i.getSystem().specSelectors.specJson(); + let _ = [...o][1] || []; + return !u.getIn(['paths', ..._, 'requestBody', 'required']) || s(...o); + }; + })((s, o) => + ((s, o) => ((o = o || []), !!s.getIn(['requestData', ...o, 'bodyValue'])))(s, o) + ), + validateShallowRequired = ( + s, + { + oas3RequiredRequestBodyContentType: o, + oas3RequestContentType: i, + oas3RequestBodyValue: u + } + ) => { + let _ = []; + if (!qe.Map.isMap(u)) return _; + let w = []; + return ( + Object.keys(o.requestContentType).forEach((s) => { + if (s === i) { + o.requestContentType[s].forEach((s) => { + w.indexOf(s) < 0 && w.push(s); + }); + } + }), + w.forEach((s) => { + u.getIn([s, 'value']) || _.push(s); + }), + _ + ); + }, + DA = Ss()(['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']), + LA = { + [uA]: (s, { payload: { selectedServerUrl: o, namespace: i } }) => { + const u = i ? [i, 'selectedServer'] : ['selectedServer']; + return s.setIn(u, o); + }, + [pA]: (s, { payload: { value: o, pathMethod: i } }) => { + let [u, _] = i; + if (!qe.Map.isMap(o)) return s.setIn(['requestData', u, _, 'bodyValue'], o); + let w, + x = s.getIn(['requestData', u, _, 'bodyValue']) || (0, qe.Map)(); + qe.Map.isMap(x) || (x = (0, qe.Map)()); + const [...C] = o.keys(); + return ( + C.forEach((s) => { + let i = o.getIn([s]); + (x.has(s) && qe.Map.isMap(i)) || (w = x.setIn([s, 'value'], i)); + }), + s.setIn(['requestData', u, _, 'bodyValue'], w) + ); + }, + [hA]: (s, { payload: { value: o, pathMethod: i } }) => { + let [u, _] = i; + return s.setIn(['requestData', u, _, 'retainBodyValue'], o); + }, + [dA]: (s, { payload: { value: o, pathMethod: i, name: u } }) => { + let [_, w] = i; + return s.setIn(['requestData', _, w, 'bodyInclusion', u], o); + }, + [fA]: (s, { payload: { name: o, pathMethod: i, contextType: u, contextName: _ } }) => { + let [w, x] = i; + return s.setIn(['examples', w, x, u, _, 'activeExample'], o); + }, + [mA]: (s, { payload: { value: o, pathMethod: i } }) => { + let [u, _] = i; + return s.setIn(['requestData', u, _, 'requestContentType'], o); + }, + [gA]: (s, { payload: { value: o, path: i, method: u } }) => + s.setIn(['requestData', i, u, 'responseContentType'], o), + [yA]: (s, { payload: { server: o, namespace: i, key: u, val: _ } }) => { + const w = i ? [i, 'serverVariableValues', o, u] : ['serverVariableValues', o, u]; + return s.setIn(w, _); + }, + [vA]: (s, { payload: { path: o, method: i, validationErrors: u } }) => { + let _ = []; + if ((_.push('Required field is not provided'), u.missingBodyValue)) + return s.setIn(['requestData', o, i, 'errors'], (0, qe.fromJS)(_)); + if (u.missingRequiredKeys && u.missingRequiredKeys.length > 0) { + const { missingRequiredKeys: w } = u; + return s.updateIn(['requestData', o, i, 'bodyValue'], (0, qe.fromJS)({}), (s) => + w.reduce((s, o) => s.setIn([o, 'errors'], (0, qe.fromJS)(_)), s) + ); + } + return (console.warn('unexpected result: SET_REQUEST_BODY_VALIDATE_ERROR'), s); + }, + [bA]: (s, { payload: { path: o, method: i } }) => { + const u = s.getIn(['requestData', o, i, 'bodyValue']); + if (!qe.Map.isMap(u)) + return s.setIn(['requestData', o, i, 'errors'], (0, qe.fromJS)([])); + const [..._] = u.keys(); + return _ + ? s.updateIn(['requestData', o, i, 'bodyValue'], (0, qe.fromJS)({}), (s) => + _.reduce((s, o) => s.setIn([o, 'errors'], (0, qe.fromJS)([])), s) + ) + : s; + }, + [_A]: (s, { payload: { pathMethod: o } }) => { + let [i, u] = o; + const _ = s.getIn(['requestData', i, u, 'bodyValue']); + return _ + ? qe.Map.isMap(_) + ? s.setIn(['requestData', i, u, 'bodyValue'], (0, qe.Map)()) + : s.setIn(['requestData', i, u, 'bodyValue'], '') + : s; + } + }; + function oas3() { + return { + components: rA, + wrapComponents: cA, + statePlugins: { + spec: { wrapSelectors: be, selectors: we }, + auth: { wrapSelectors: _e }, + oas3: { actions: { ...Se }, reducers: LA, selectors: { ...xe } } + } + }; + } + const webhooks = ({ specSelectors: s, getComponent: o }) => { + const i = s.selectWebhooksOperations(), + u = Object.keys(i), + _ = o('OperationContainer', !0); + return 0 === u.length + ? null + : Pe.createElement( + 'div', + { className: 'webhooks' }, + Pe.createElement('h2', null, 'Webhooks'), + u.map((s) => + Pe.createElement( + 'div', + { key: `${s}-webhook` }, + i[s].map((o) => + Pe.createElement(_, { + key: `${s}-${o.method}-webhook`, + op: o.operation, + tag: 'webhooks', + method: o.method, + path: s, + specPath: (0, qe.List)(o.specPath), + allowTryItOut: !1 + }) + ) + ) + ) + ); + }, + oas31_components_license = ({ getComponent: s, specSelectors: o }) => { + const i = o.selectLicenseNameField(), + u = o.selectLicenseUrl(), + _ = s('Link'); + return Pe.createElement( + 'div', + { className: 'info__license' }, + u + ? Pe.createElement( + 'div', + { className: 'info__license__url' }, + Pe.createElement(_, { target: '_blank', href: sanitizeUrl(u) }, i) + ) + : Pe.createElement('span', null, i) + ); + }, + oas31_components_contact = ({ getComponent: s, specSelectors: o }) => { + const i = o.selectContactNameField(), + u = o.selectContactUrl(), + _ = o.selectContactEmailField(), + w = s('Link'); + return Pe.createElement( + 'div', + { className: 'info__contact' }, + u && + Pe.createElement( + 'div', + null, + Pe.createElement(w, { href: sanitizeUrl(u), target: '_blank' }, i, ' - Website') + ), + _ && + Pe.createElement( + w, + { href: sanitizeUrl(`mailto:${_}`) }, + u ? `Send email to ${i}` : `Contact ${i}` + ) + ); + }, + oas31_components_info = ({ getComponent: s, specSelectors: o }) => { + const i = o.version(), + u = o.url(), + _ = o.basePath(), + w = o.host(), + x = o.selectInfoSummaryField(), + C = o.selectInfoDescriptionField(), + j = o.selectInfoTitleField(), + L = o.selectInfoTermsOfServiceUrl(), + B = o.selectExternalDocsUrl(), + $ = o.selectExternalDocsDescriptionField(), + V = o.contact(), + U = o.license(), + z = s('Markdown', !0), + Y = s('Link'), + Z = s('VersionStamp'), + ee = s('OpenAPIVersion'), + ie = s('InfoUrl'), + ae = s('InfoBasePath'), + le = s('License', !0), + ce = s('Contact', !0), + pe = s('JsonSchemaDialect', !0); + return Pe.createElement( + 'div', + { className: 'info' }, + Pe.createElement( + 'hgroup', + { className: 'main' }, + Pe.createElement( + 'h2', + { className: 'title' }, + j, + Pe.createElement( + 'span', + null, + i && Pe.createElement(Z, { version: i }), + Pe.createElement(ee, { oasVersion: '3.1' }) + ) + ), + (w || _) && Pe.createElement(ae, { host: w, basePath: _ }), + u && Pe.createElement(ie, { getComponent: s, url: u }) + ), + x && Pe.createElement('p', { className: 'info__summary' }, x), + Pe.createElement( + 'div', + { className: 'info__description description' }, + Pe.createElement(z, { source: C }) + ), + L && + Pe.createElement( + 'div', + { className: 'info__tos' }, + Pe.createElement( + Y, + { target: '_blank', href: sanitizeUrl(L) }, + 'Terms of service' + ) + ), + V.size > 0 && Pe.createElement(ce, null), + U.size > 0 && Pe.createElement(le, null), + B && + Pe.createElement( + Y, + { className: 'info__extdocs', target: '_blank', href: sanitizeUrl(B) }, + $ || B + ), + Pe.createElement(pe, null) + ); + }, + json_schema_dialect = ({ getComponent: s, specSelectors: o }) => { + const i = o.selectJsonSchemaDialectField(), + u = o.selectJsonSchemaDialectDefault(), + _ = s('Link'); + return Pe.createElement( + Pe.Fragment, + null, + i && + i === u && + Pe.createElement( + 'p', + { className: 'info__jsonschemadialect' }, + 'JSON Schema dialect:', + ' ', + Pe.createElement(_, { target: '_blank', href: sanitizeUrl(i) }, i) + ), + i && + i !== u && + Pe.createElement( + 'div', + { className: 'error-wrapper' }, + Pe.createElement( + 'div', + { className: 'no-margin' }, + Pe.createElement( + 'div', + { className: 'errors' }, + Pe.createElement( + 'div', + { className: 'errors-wrapper' }, + Pe.createElement('h4', { className: 'center' }, 'Warning'), + Pe.createElement( + 'p', + { className: 'message' }, + Pe.createElement('strong', null, 'OpenAPI.jsonSchemaDialect'), + ' field contains a value different from the default value of', + ' ', + Pe.createElement(_, { target: '_blank', href: u }, u), + '. Values different from the default one are currently not supported. Please either omit the field or provide it with the default value.' + ) + ) + ) + ) + ) + ); + }, + version_pragma_filter = ({ + bypass: s, + isSwagger2: o, + isOAS3: i, + isOAS31: u, + alsoShow: _, + children: w + }) => + s + ? Pe.createElement('div', null, w) + : o && (i || u) + ? Pe.createElement( + 'div', + { className: 'version-pragma' }, + _, + Pe.createElement( + 'div', + { className: 'version-pragma__message version-pragma__message--ambiguous' }, + Pe.createElement( + 'div', + null, + Pe.createElement('h3', null, 'Unable to render this definition'), + Pe.createElement( + 'p', + null, + Pe.createElement('code', null, 'swagger'), + ' and ', + Pe.createElement('code', null, 'openapi'), + ' fields cannot be present in the same Swagger or OpenAPI definition. Please remove one of the fields.' + ), + Pe.createElement( + 'p', + null, + 'Supported version fields are ', + Pe.createElement('code', null, 'swagger: "2.0"'), + ' and those that match ', + Pe.createElement('code', null, 'openapi: 3.x.y'), + ' (for example,', + ' ', + Pe.createElement('code', null, 'openapi: 3.1.0'), + ').' + ) + ) + ) + ) + : o || i || u + ? Pe.createElement('div', null, w) + : Pe.createElement( + 'div', + { className: 'version-pragma' }, + _, + Pe.createElement( + 'div', + { className: 'version-pragma__message version-pragma__message--missing' }, + Pe.createElement( + 'div', + null, + Pe.createElement('h3', null, 'Unable to render this definition'), + Pe.createElement( + 'p', + null, + 'The provided definition does not specify a valid version field.' + ), + Pe.createElement( + 'p', + null, + 'Please indicate a valid Swagger or OpenAPI version field. Supported version fields are ', + Pe.createElement('code', null, 'swagger: "2.0"'), + ' and those that match ', + Pe.createElement('code', null, 'openapi: 3.x.y'), + ' (for example,', + ' ', + Pe.createElement('code', null, 'openapi: 3.1.0'), + ').' + ) + ) + ) + ), + getModelName = (s) => + 'string' == typeof s && s.includes('#/components/schemas/') + ? ((s) => { + const o = s.replace(/~1/g, '/').replace(/~0/g, '~'); + try { + return decodeURIComponent(o); + } catch { + return o; + } + })(s.replace(/^.*#\/components\/schemas\//, '')) + : null, + BA = (0, Pe.forwardRef)(({ schema: s, getComponent: o, onToggle: i = () => {} }, u) => { + const _ = o('JSONSchema202012'), + w = getModelName(s.get('$$ref')), + x = (0, Pe.useCallback)( + (s, o) => { + i(w, o); + }, + [w, i] + ); + return Pe.createElement(_, { name: w, schema: s.toJS(), ref: u, onExpand: x }); + }), + FA = BA, + models = ({ + specActions: s, + specSelectors: o, + layoutSelectors: i, + layoutActions: u, + getComponent: _, + getConfigs: w, + fn: x + }) => { + const C = o.selectSchemas(), + j = Object.keys(C).length > 0, + L = ['components', 'schemas'], + { docExpansion: B, defaultModelsExpandDepth: $ } = w(), + V = $ > 0 && 'none' !== B, + U = i.isShown(L, V), + z = _('Collapse'), + Y = _('JSONSchema202012'), + Z = _('ArrowUpIcon'), + ee = _('ArrowDownIcon'), + { getTitle: ie } = x.jsonSchema202012.useFn(); + (0, Pe.useEffect)(() => { + const i = U && $ > 1, + u = null != o.specResolvedSubtree(L); + i && !u && s.requestResolvedSubtree(L); + }, [U, $]); + const ae = (0, Pe.useCallback)(() => { + u.show(L, !U); + }, [U]), + le = (0, Pe.useCallback)((s) => { + null !== s && u.readyToScroll(L, s); + }, []), + handleJSONSchema202012Ref = (s) => (o) => { + null !== o && u.readyToScroll([...L, s], o); + }, + handleJSONSchema202012Expand = (i) => (u, _) => { + if (_) { + const u = [...L, i]; + null != o.specResolvedSubtree(u) || s.requestResolvedSubtree([...L, i]); + } + }; + return !j || $ < 0 + ? null + : Pe.createElement( + 'section', + { className: Hn()('models', { 'is-open': U }), ref: le }, + Pe.createElement( + 'h4', + null, + Pe.createElement( + 'button', + { 'aria-expanded': U, className: 'models-control', onClick: ae }, + Pe.createElement('span', null, 'Schemas'), + U ? Pe.createElement(Z, null) : Pe.createElement(ee, null) + ) + ), + Pe.createElement( + z, + { isOpened: U }, + Object.entries(C).map(([s, o]) => { + const i = ie(o, { lookup: 'basic' }) || s; + return Pe.createElement(Y, { + key: s, + ref: handleJSONSchema202012Ref(s), + schema: o, + name: i, + onExpand: handleJSONSchema202012Expand(s) + }); + }) + ) + ); + }, + mutual_tls_auth = ({ schema: s, getComponent: o }) => { + const i = o('JumpToPath', !0); + return Pe.createElement( + 'div', + null, + Pe.createElement( + 'h4', + null, + s.get('name'), + ' (mutualTLS)', + ' ', + Pe.createElement(i, { path: ['securityDefinitions', s.get('name')] }) + ), + Pe.createElement( + 'p', + null, + 'Mutual TLS is required by this API/Operation. Certificates are managed via your Operating System and/or your browser.' + ), + Pe.createElement('p', null, s.get('description')) + ); + }; + class auths_Auths extends Pe.Component { + constructor(s, o) { + (super(s, o), (this.state = {})); + } + onAuthChange = (s) => { + let { name: o } = s; + this.setState({ [o]: s }); + }; + submitAuth = (s) => { + s.preventDefault(); + let { authActions: o } = this.props; + o.authorizeWithPersistOption(this.state); + }; + logoutClick = (s) => { + s.preventDefault(); + let { authActions: o, definitions: i } = this.props, + u = i.map((s, o) => o).toArray(); + (this.setState(u.reduce((s, o) => ((s[o] = ''), s), {})), o.logoutWithPersistOption(u)); + }; + close = (s) => { + s.preventDefault(); + let { authActions: o } = this.props; + o.showDefinitions(!1); + }; + render() { + let { definitions: s, getComponent: o, authSelectors: i, errSelectors: u } = this.props; + const _ = o('AuthItem'), + w = o('oauth2', !0), + x = o('Button'), + C = i.authorized(), + j = s.filter((s, o) => !!C.get(o)), + L = s.filter((s) => 'oauth2' !== s.get('type') && 'mutualTLS' !== s.get('type')), + B = s.filter((s) => 'oauth2' === s.get('type')), + $ = s.filter((s) => 'mutualTLS' === s.get('type')); + return Pe.createElement( + 'div', + { className: 'auth-container' }, + L.size > 0 && + Pe.createElement( + 'form', + { onSubmit: this.submitAuth }, + L.map((s, i) => + Pe.createElement(_, { + key: i, + schema: s, + name: i, + getComponent: o, + onAuthChange: this.onAuthChange, + authorized: C, + errSelectors: u + }) + ).toArray(), + Pe.createElement( + 'div', + { className: 'auth-btn-wrapper' }, + L.size === j.size + ? Pe.createElement( + x, + { + className: 'btn modal-btn auth', + onClick: this.logoutClick, + 'aria-label': 'Remove authorization' + }, + 'Logout' + ) + : Pe.createElement( + x, + { + type: 'submit', + className: 'btn modal-btn auth authorize', + 'aria-label': 'Apply credentials' + }, + 'Authorize' + ), + Pe.createElement( + x, + { className: 'btn modal-btn auth btn-done', onClick: this.close }, + 'Close' + ) + ) + ), + B.size > 0 + ? Pe.createElement( + 'div', + null, + Pe.createElement( + 'div', + { className: 'scope-def' }, + Pe.createElement( + 'p', + null, + 'Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.' + ), + Pe.createElement( + 'p', + null, + 'API requires the following scopes. Select which ones you want to grant to Swagger UI.' + ) + ), + s + .filter((s) => 'oauth2' === s.get('type')) + .map((s, o) => + Pe.createElement( + 'div', + { key: o }, + Pe.createElement(w, { authorized: C, schema: s, name: o }) + ) + ) + .toArray() + ) + : null, + $.size > 0 && + Pe.createElement( + 'div', + null, + $.map((s, i) => + Pe.createElement(_, { + key: i, + schema: s, + name: i, + getComponent: o, + onAuthChange: this.onAuthChange, + authorized: C, + errSelectors: u + }) + ).toArray() + ) + ); + } + } + const qA = auths_Auths, + isOAS31 = (s) => { + const o = s.get('openapi'); + return 'string' == typeof o && /^3\.1\.(?:[1-9]\d*|0)$/.test(o); + }, + fn_createOnlyOAS31Selector = + (s) => + (o, ...i) => + (u) => { + if (u.getSystem().specSelectors.isOAS31()) { + const _ = s(o, ...i); + return 'function' == typeof _ ? _(u) : _; + } + return null; + }, + createOnlyOAS31SelectorWrapper = + (s) => + (o, i) => + (u, ..._) => { + if (i.getSystem().specSelectors.isOAS31()) { + const w = s(u, ..._); + return 'function' == typeof w ? w(o, i) : w; + } + return o(..._); + }, + fn_createSystemSelector = + (s) => + (o, ...i) => + (u) => { + const _ = s(o, u, ...i); + return 'function' == typeof _ ? _(u) : _; + }, + createOnlyOAS31ComponentWrapper = (s) => (o, i) => (u) => + i.specSelectors.isOAS31() + ? Pe.createElement(s, Rn()({}, u, { originalComponent: o, getSystem: i.getSystem })) + : Pe.createElement(o, u), + $A = createOnlyOAS31ComponentWrapper(({ getSystem: s }) => { + const o = s().getComponent('OAS31License', !0); + return Pe.createElement(o, null); + }), + VA = createOnlyOAS31ComponentWrapper(({ getSystem: s }) => { + const o = s().getComponent('OAS31Contact', !0); + return Pe.createElement(o, null); + }), + UA = createOnlyOAS31ComponentWrapper(({ getSystem: s }) => { + const o = s().getComponent('OAS31Info', !0); + return Pe.createElement(o, null); + }), + zA = createOnlyOAS31ComponentWrapper(({ getSystem: s, ...o }) => { + const i = s(), + { getComponent: u, fn: _, getConfigs: w } = i, + x = w(), + C = u('OAS31Model'), + j = u('JSONSchema202012'), + L = u('JSONSchema202012Keyword$schema'), + B = u('JSONSchema202012Keyword$vocabulary'), + $ = u('JSONSchema202012Keyword$id'), + V = u('JSONSchema202012Keyword$anchor'), + U = u('JSONSchema202012Keyword$dynamicAnchor'), + z = u('JSONSchema202012Keyword$ref'), + Y = u('JSONSchema202012Keyword$dynamicRef'), + Z = u('JSONSchema202012Keyword$defs'), + ee = u('JSONSchema202012Keyword$comment'), + ie = u('JSONSchema202012KeywordAllOf'), + ae = u('JSONSchema202012KeywordAnyOf'), + le = u('JSONSchema202012KeywordOneOf'), + ce = u('JSONSchema202012KeywordNot'), + pe = u('JSONSchema202012KeywordIf'), + de = u('JSONSchema202012KeywordThen'), + fe = u('JSONSchema202012KeywordElse'), + ye = u('JSONSchema202012KeywordDependentSchemas'), + be = u('JSONSchema202012KeywordPrefixItems'), + _e = u('JSONSchema202012KeywordItems'), + we = u('JSONSchema202012KeywordContains'), + Se = u('JSONSchema202012KeywordProperties'), + xe = u('JSONSchema202012KeywordPatternProperties'), + Te = u('JSONSchema202012KeywordAdditionalProperties'), + Re = u('JSONSchema202012KeywordPropertyNames'), + qe = u('JSONSchema202012KeywordUnevaluatedItems'), + $e = u('JSONSchema202012KeywordUnevaluatedProperties'), + ze = u('JSONSchema202012KeywordType'), + We = u('JSONSchema202012KeywordEnum'), + He = u('JSONSchema202012KeywordConst'), + Ye = u('JSONSchema202012KeywordConstraint'), + Xe = u('JSONSchema202012KeywordDependentRequired'), + Qe = u('JSONSchema202012KeywordContentSchema'), + et = u('JSONSchema202012KeywordTitle'), + tt = u('JSONSchema202012KeywordDescription'), + rt = u('JSONSchema202012KeywordDefault'), + nt = u('JSONSchema202012KeywordDeprecated'), + st = u('JSONSchema202012KeywordReadOnly'), + ot = u('JSONSchema202012KeywordWriteOnly'), + it = u('JSONSchema202012Accordion'), + at = u('JSONSchema202012ExpandDeepButton'), + lt = u('JSONSchema202012ChevronRightIcon'), + ct = u('withJSONSchema202012Context')(C, { + config: { + default$schema: 'https://spec.openapis.org/oas/3.1/dialect/base', + defaultExpandedLevels: x.defaultModelExpandDepth, + includeReadOnly: Boolean(o.includeReadOnly), + includeWriteOnly: Boolean(o.includeWriteOnly) + }, + components: { + JSONSchema: j, + Keyword$schema: L, + Keyword$vocabulary: B, + Keyword$id: $, + Keyword$anchor: V, + Keyword$dynamicAnchor: U, + Keyword$ref: z, + Keyword$dynamicRef: Y, + Keyword$defs: Z, + Keyword$comment: ee, + KeywordAllOf: ie, + KeywordAnyOf: ae, + KeywordOneOf: le, + KeywordNot: ce, + KeywordIf: pe, + KeywordThen: de, + KeywordElse: fe, + KeywordDependentSchemas: ye, + KeywordPrefixItems: be, + KeywordItems: _e, + KeywordContains: we, + KeywordProperties: Se, + KeywordPatternProperties: xe, + KeywordAdditionalProperties: Te, + KeywordPropertyNames: Re, + KeywordUnevaluatedItems: qe, + KeywordUnevaluatedProperties: $e, + KeywordType: ze, + KeywordEnum: We, + KeywordConst: He, + KeywordConstraint: Ye, + KeywordDependentRequired: Xe, + KeywordContentSchema: Qe, + KeywordTitle: et, + KeywordDescription: tt, + KeywordDefault: rt, + KeywordDeprecated: nt, + KeywordReadOnly: st, + KeywordWriteOnly: ot, + Accordion: it, + ExpandDeepButton: at, + ChevronRightIcon: lt + }, + fn: { + upperFirst: _.upperFirst, + isExpandable: _.jsonSchema202012.isExpandable, + getProperties: _.jsonSchema202012.getProperties + } + }); + return Pe.createElement(ct, o); + }), + WA = zA, + KA = createOnlyOAS31ComponentWrapper(({ getSystem: s }) => { + const { getComponent: o, fn: i, getConfigs: u } = s(), + _ = u(); + if (KA.ModelsWithJSONSchemaContext) + return Pe.createElement(KA.ModelsWithJSONSchemaContext, null); + const w = o('OAS31Models', !0), + x = o('JSONSchema202012'), + C = o('JSONSchema202012Keyword$schema'), + j = o('JSONSchema202012Keyword$vocabulary'), + L = o('JSONSchema202012Keyword$id'), + B = o('JSONSchema202012Keyword$anchor'), + $ = o('JSONSchema202012Keyword$dynamicAnchor'), + V = o('JSONSchema202012Keyword$ref'), + U = o('JSONSchema202012Keyword$dynamicRef'), + z = o('JSONSchema202012Keyword$defs'), + Y = o('JSONSchema202012Keyword$comment'), + Z = o('JSONSchema202012KeywordAllOf'), + ee = o('JSONSchema202012KeywordAnyOf'), + ie = o('JSONSchema202012KeywordOneOf'), + ae = o('JSONSchema202012KeywordNot'), + le = o('JSONSchema202012KeywordIf'), + ce = o('JSONSchema202012KeywordThen'), + pe = o('JSONSchema202012KeywordElse'), + de = o('JSONSchema202012KeywordDependentSchemas'), + fe = o('JSONSchema202012KeywordPrefixItems'), + ye = o('JSONSchema202012KeywordItems'), + be = o('JSONSchema202012KeywordContains'), + _e = o('JSONSchema202012KeywordProperties'), + we = o('JSONSchema202012KeywordPatternProperties'), + Se = o('JSONSchema202012KeywordAdditionalProperties'), + xe = o('JSONSchema202012KeywordPropertyNames'), + Te = o('JSONSchema202012KeywordUnevaluatedItems'), + Re = o('JSONSchema202012KeywordUnevaluatedProperties'), + qe = o('JSONSchema202012KeywordType'), + $e = o('JSONSchema202012KeywordEnum'), + ze = o('JSONSchema202012KeywordConst'), + We = o('JSONSchema202012KeywordConstraint'), + He = o('JSONSchema202012KeywordDependentRequired'), + Ye = o('JSONSchema202012KeywordContentSchema'), + Xe = o('JSONSchema202012KeywordTitle'), + Qe = o('JSONSchema202012KeywordDescription'), + et = o('JSONSchema202012KeywordDefault'), + tt = o('JSONSchema202012KeywordDeprecated'), + rt = o('JSONSchema202012KeywordReadOnly'), + nt = o('JSONSchema202012KeywordWriteOnly'), + st = o('JSONSchema202012Accordion'), + ot = o('JSONSchema202012ExpandDeepButton'), + it = o('JSONSchema202012ChevronRightIcon'), + at = o('withJSONSchema202012Context'); + return ( + (KA.ModelsWithJSONSchemaContext = at(w, { + config: { + default$schema: 'https://spec.openapis.org/oas/3.1/dialect/base', + defaultExpandedLevels: _.defaultModelsExpandDepth - 1, + includeReadOnly: !0, + includeWriteOnly: !0 + }, + components: { + JSONSchema: x, + Keyword$schema: C, + Keyword$vocabulary: j, + Keyword$id: L, + Keyword$anchor: B, + Keyword$dynamicAnchor: $, + Keyword$ref: V, + Keyword$dynamicRef: U, + Keyword$defs: z, + Keyword$comment: Y, + KeywordAllOf: Z, + KeywordAnyOf: ee, + KeywordOneOf: ie, + KeywordNot: ae, + KeywordIf: le, + KeywordThen: ce, + KeywordElse: pe, + KeywordDependentSchemas: de, + KeywordPrefixItems: fe, + KeywordItems: ye, + KeywordContains: be, + KeywordProperties: _e, + KeywordPatternProperties: we, + KeywordAdditionalProperties: Se, + KeywordPropertyNames: xe, + KeywordUnevaluatedItems: Te, + KeywordUnevaluatedProperties: Re, + KeywordType: qe, + KeywordEnum: $e, + KeywordConst: ze, + KeywordConstraint: We, + KeywordDependentRequired: He, + KeywordContentSchema: Ye, + KeywordTitle: Xe, + KeywordDescription: Qe, + KeywordDefault: et, + KeywordDeprecated: tt, + KeywordReadOnly: rt, + KeywordWriteOnly: nt, + Accordion: st, + ExpandDeepButton: ot, + ChevronRightIcon: it + }, + fn: { + upperFirst: i.upperFirst, + isExpandable: i.jsonSchema202012.isExpandable, + getProperties: i.jsonSchema202012.getProperties + } + })), + Pe.createElement(KA.ModelsWithJSONSchemaContext, null) + ); + }); + KA.ModelsWithJSONSchemaContext = null; + const HA = KA, + wrap_components_version_pragma_filter = (s, o) => (s) => { + const i = o.specSelectors.isOAS31(), + u = o.getComponent('OAS31VersionPragmaFilter'); + return Pe.createElement(u, Rn()({ isOAS31: i }, s)); + }, + JA = createOnlyOAS31ComponentWrapper(({ originalComponent: s, ...o }) => { + const { getComponent: i, schema: u } = o, + _ = i('MutualTLSAuth', !0); + return 'mutualTLS' === u.get('type') + ? Pe.createElement(_, { schema: u }) + : Pe.createElement(s, o); + }), + GA = JA, + YA = createOnlyOAS31ComponentWrapper(({ getSystem: s, ...o }) => { + const i = s().getComponent('OAS31Auths', !0); + return Pe.createElement(i, o); + }), + XA = (0, qe.Map)(), + ZA = Ut((s, o) => o.specSelectors.specJson(), isOAS31), + selectors_webhooks = () => (s) => { + const o = s.specSelectors.specJson().get('webhooks'); + return qe.Map.isMap(o) ? o : XA; + }, + QA = Ut( + [ + (s, o) => o.specSelectors.webhooks(), + (s, o) => o.specSelectors.validOperationMethods(), + (s, o) => o.specSelectors.specResolvedSubtree(['webhooks']) + ], + (s, o) => + s + .reduce( + (s, i, u) => { + if (!qe.Map.isMap(i)) return s; + const _ = i + .entrySeq() + .filter(([s]) => o.includes(s)) + .map(([s, o]) => ({ + operation: (0, qe.Map)({ operation: o }), + method: s, + path: u, + specPath: ['webhooks', u, s] + })); + return s.concat(_); + }, + (0, qe.List)() + ) + .groupBy((s) => s.path) + .map((s) => s.toArray()) + .toObject() + ), + selectors_license = () => (s) => { + const o = s.specSelectors.info().get('license'); + return qe.Map.isMap(o) ? o : XA; + }, + selectLicenseNameField = () => (s) => s.specSelectors.license().get('name', 'License'), + selectLicenseUrlField = () => (s) => s.specSelectors.license().get('url'), + ej = Ut( + [ + (s, o) => o.specSelectors.url(), + (s, o) => o.oas3Selectors.selectedServer(), + (s, o) => o.specSelectors.selectLicenseUrlField() + ], + (s, o, i) => { + if (i) return safeBuildUrl(i, s, { selectedServer: o }); + } + ), + selectLicenseIdentifierField = () => (s) => s.specSelectors.license().get('identifier'), + selectors_contact = () => (s) => { + const o = s.specSelectors.info().get('contact'); + return qe.Map.isMap(o) ? o : XA; + }, + selectContactNameField = () => (s) => + s.specSelectors.contact().get('name', 'the developer'), + selectContactEmailField = () => (s) => s.specSelectors.contact().get('email'), + selectContactUrlField = () => (s) => s.specSelectors.contact().get('url'), + fj = Ut( + [ + (s, o) => o.specSelectors.url(), + (s, o) => o.oas3Selectors.selectedServer(), + (s, o) => o.specSelectors.selectContactUrlField() + ], + (s, o, i) => { + if (i) return safeBuildUrl(i, s, { selectedServer: o }); + } + ), + selectInfoTitleField = () => (s) => s.specSelectors.info().get('title'), + selectInfoSummaryField = () => (s) => s.specSelectors.info().get('summary'), + selectInfoDescriptionField = () => (s) => s.specSelectors.info().get('description'), + selectInfoTermsOfServiceField = () => (s) => s.specSelectors.info().get('termsOfService'), + mj = Ut( + [ + (s, o) => o.specSelectors.url(), + (s, o) => o.oas3Selectors.selectedServer(), + (s, o) => o.specSelectors.selectInfoTermsOfServiceField() + ], + (s, o, i) => { + if (i) return safeBuildUrl(i, s, { selectedServer: o }); + } + ), + selectExternalDocsDescriptionField = () => (s) => + s.specSelectors.externalDocs().get('description'), + selectExternalDocsUrlField = () => (s) => s.specSelectors.externalDocs().get('url'), + _j = Ut( + [ + (s, o) => o.specSelectors.url(), + (s, o) => o.oas3Selectors.selectedServer(), + (s, o) => o.specSelectors.selectExternalDocsUrlField() + ], + (s, o, i) => { + if (i) return safeBuildUrl(i, s, { selectedServer: o }); + } + ), + selectJsonSchemaDialectField = () => (s) => + s.specSelectors.specJson().get('jsonSchemaDialect'), + selectJsonSchemaDialectDefault = () => 'https://spec.openapis.org/oas/3.1/dialect/base', + Cj = Ut( + (s, o) => o.specSelectors.definitions(), + (s, o) => o.specSelectors.specResolvedSubtree(['components', 'schemas']), + (s, o) => + qe.Map.isMap(s) + ? qe.Map.isMap(o) + ? Object.entries(s.toJS()).reduce((s, [i, u]) => { + const _ = o.get(i); + return ((s[i] = _?.toJS() || u), s); + }, {}) + : s.toJS() + : {} + ), + wrap_selectors_isOAS3 = + (s, o) => + (i, ...u) => + o.specSelectors.isOAS31() || s(...u), + Aj = createOnlyOAS31SelectorWrapper(() => (s, o) => o.oas31Selectors.selectLicenseUrl()), + Nj = createOnlyOAS31SelectorWrapper(() => (s, o) => { + const i = o.specSelectors.securityDefinitions(); + let u = s(); + return i + ? (i.entrySeq().forEach(([s, o]) => { + 'mutualTLS' === o.get('type') && (u = u.push(new qe.Map({ [s]: o }))); + }), + u) + : u; + }), + Bj = Ut( + [ + (s, o) => o.specSelectors.url(), + (s, o) => o.oas3Selectors.selectedServer(), + (s, o) => o.specSelectors.selectLicenseUrlField(), + (s, o) => o.specSelectors.selectLicenseIdentifierField() + ], + (s, o, i, u) => + i + ? safeBuildUrl(i, s, { selectedServer: o }) + : u + ? `https://spdx.org/licenses/${u}.html` + : void 0 + ), + keywords_Example = ({ schema: s, getSystem: o }) => { + const { fn: i } = o(), + { hasKeyword: u, stringify: _ } = i.jsonSchema202012.useFn(); + return u(s, 'example') + ? Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--example' }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + 'Example' + ), + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--const' + }, + _(s.example) + ) + ) + : null; + }, + keywords_Xml = ({ schema: s, getSystem: o }) => { + const i = s?.xml || {}, + { fn: u, getComponent: _ } = o(), + { useIsExpandedDeeply: w, useComponent: x } = u.jsonSchema202012, + C = w(), + j = !!(i.name || i.namespace || i.prefix), + [L, B] = (0, Pe.useState)(C), + [$, V] = (0, Pe.useState)(!1), + U = x('Accordion'), + z = x('ExpandDeepButton'), + Y = _('JSONSchema202012DeepExpansionContext')(), + Z = (0, Pe.useCallback)(() => { + B((s) => !s); + }, []), + ee = (0, Pe.useCallback)((s, o) => { + (B(o), V(o)); + }, []); + return 0 === Object.keys(i).length + ? null + : Pe.createElement( + Y.Provider, + { value: $ }, + Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--xml' }, + j + ? Pe.createElement( + Pe.Fragment, + null, + Pe.createElement( + U, + { expanded: L, onChange: Z }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + 'XML' + ) + ), + Pe.createElement(z, { expanded: L, onClick: ee }) + ) + : Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + 'XML' + ), + !0 === i.attribute && + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12__attribute json-schema-2020-12__attribute--muted' + }, + 'attribute' + ), + !0 === i.wrapped && + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12__attribute json-schema-2020-12__attribute--muted' + }, + 'wrapped' + ), + Pe.createElement( + 'strong', + { + className: + 'json-schema-2020-12__attribute json-schema-2020-12__attribute--primary' + }, + 'object' + ), + Pe.createElement( + 'ul', + { + className: Hn()('json-schema-2020-12-keyword__children', { + 'json-schema-2020-12-keyword__children--collapsed': !L + }) + }, + L && + Pe.createElement( + Pe.Fragment, + null, + i.name && + Pe.createElement( + 'li', + { className: 'json-schema-2020-12-property' }, + Pe.createElement( + 'div', + { + className: + 'json-schema-2020-12-keyword json-schema-2020-12-keyword' + }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + 'name' + ), + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' + }, + i.name + ) + ) + ), + i.namespace && + Pe.createElement( + 'li', + { className: 'json-schema-2020-12-property' }, + Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword' }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + 'namespace' + ), + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' + }, + i.namespace + ) + ) + ), + i.prefix && + Pe.createElement( + 'li', + { className: 'json-schema-2020-12-property' }, + Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword' }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + 'prefix' + ), + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' + }, + i.prefix + ) + ) + ) + ) + ) + ) + ); + }, + Discriminator_DiscriminatorMapping = ({ discriminator: s }) => { + const o = s?.mapping || {}; + return 0 === Object.keys(o).length + ? null + : Object.entries(o).map(([s, o]) => + Pe.createElement( + 'div', + { key: `${s}-${o}`, className: 'json-schema-2020-12-keyword' }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + s + ), + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' + }, + o + ) + ) + ); + }, + keywords_Discriminator_Discriminator = ({ schema: s, getSystem: o }) => { + const i = s?.discriminator || {}, + { fn: u, getComponent: _ } = o(), + { useIsExpandedDeeply: w, useComponent: x } = u.jsonSchema202012, + C = w(), + j = !!i.mapping, + [L, B] = (0, Pe.useState)(C), + [$, V] = (0, Pe.useState)(!1), + U = x('Accordion'), + z = x('ExpandDeepButton'), + Y = _('JSONSchema202012DeepExpansionContext')(), + Z = (0, Pe.useCallback)(() => { + B((s) => !s); + }, []), + ee = (0, Pe.useCallback)((s, o) => { + (B(o), V(o)); + }, []); + return 0 === Object.keys(i).length + ? null + : Pe.createElement( + Y.Provider, + { value: $ }, + Pe.createElement( + 'div', + { + className: + 'json-schema-2020-12-keyword json-schema-2020-12-keyword--discriminator' + }, + j + ? Pe.createElement( + Pe.Fragment, + null, + Pe.createElement( + U, + { expanded: L, onChange: Z }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + 'Discriminator' + ) + ), + Pe.createElement(z, { expanded: L, onClick: ee }) + ) + : Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + 'Discriminator' + ), + i.propertyName && + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12__attribute json-schema-2020-12__attribute--muted' + }, + i.propertyName + ), + Pe.createElement( + 'strong', + { + className: + 'json-schema-2020-12__attribute json-schema-2020-12__attribute--primary' + }, + 'object' + ), + Pe.createElement( + 'ul', + { + className: Hn()('json-schema-2020-12-keyword__children', { + 'json-schema-2020-12-keyword__children--collapsed': !L + }) + }, + L && + Pe.createElement( + 'li', + { className: 'json-schema-2020-12-property' }, + Pe.createElement(Discriminator_DiscriminatorMapping, { discriminator: i }) + ) + ) + ) + ); + }, + keywords_ExternalDocs = ({ schema: s, getSystem: o }) => { + const i = s?.externalDocs || {}, + { fn: u, getComponent: _ } = o(), + { useIsExpandedDeeply: w, useComponent: x } = u.jsonSchema202012, + C = w(), + j = !(!i.description && !i.url), + [L, B] = (0, Pe.useState)(C), + [$, V] = (0, Pe.useState)(!1), + U = x('Accordion'), + z = x('ExpandDeepButton'), + Y = _('JSONSchema202012KeywordDescription'), + Z = _('Link'), + ee = _('JSONSchema202012DeepExpansionContext')(), + ie = (0, Pe.useCallback)(() => { + B((s) => !s); + }, []), + ae = (0, Pe.useCallback)((s, o) => { + (B(o), V(o)); + }, []); + return 0 === Object.keys(i).length + ? null + : Pe.createElement( + ee.Provider, + { value: $ }, + Pe.createElement( + 'div', + { + className: + 'json-schema-2020-12-keyword json-schema-2020-12-keyword--externalDocs' + }, + j + ? Pe.createElement( + Pe.Fragment, + null, + Pe.createElement( + U, + { expanded: L, onChange: ie }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + 'External documentation' + ) + ), + Pe.createElement(z, { expanded: L, onClick: ae }) + ) + : Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + 'External documentation' + ), + Pe.createElement( + 'strong', + { + className: + 'json-schema-2020-12__attribute json-schema-2020-12__attribute--primary' + }, + 'object' + ), + Pe.createElement( + 'ul', + { + className: Hn()('json-schema-2020-12-keyword__children', { + 'json-schema-2020-12-keyword__children--collapsed': !L + }) + }, + L && + Pe.createElement( + Pe.Fragment, + null, + i.description && + Pe.createElement( + 'li', + { className: 'json-schema-2020-12-property' }, + Pe.createElement(Y, { schema: i, getSystem: o }) + ), + i.url && + Pe.createElement( + 'li', + { className: 'json-schema-2020-12-property' }, + Pe.createElement( + 'div', + { + className: + 'json-schema-2020-12-keyword json-schema-2020-12-keyword' + }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + 'url' + ), + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' + }, + Pe.createElement( + Z, + { target: '_blank', href: sanitizeUrl(i.url) }, + i.url + ) + ) + ) + ) + ) + ) + ) + ); + }, + keywords_Description = ({ schema: s, getSystem: o }) => { + if (!s?.description) return null; + const { getComponent: i } = o(), + u = i('Markdown'); + return Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--description' }, + Pe.createElement( + 'div', + { + className: + 'json-schema-2020-12-core-keyword__value json-schema-2020-12-core-keyword__value--secondary' + }, + Pe.createElement(u, { source: s.description }) + ) + ); + }, + $j = createOnlyOAS31ComponentWrapper(keywords_Description), + zj = createOnlyOAS31ComponentWrapper( + ({ schema: s, getSystem: o, originalComponent: i }) => { + const { getComponent: u } = o(), + _ = u('JSONSchema202012KeywordDiscriminator'), + w = u('JSONSchema202012KeywordXml'), + x = u('JSONSchema202012KeywordExample'), + C = u('JSONSchema202012KeywordExternalDocs'); + return Pe.createElement( + Pe.Fragment, + null, + Pe.createElement(i, { schema: s }), + Pe.createElement(_, { schema: s, getSystem: o }), + Pe.createElement(w, { schema: s, getSystem: o }), + Pe.createElement(C, { schema: s, getSystem: o }), + Pe.createElement(x, { schema: s, getSystem: o }) + ); + } + ), + Kj = zj, + keywords_Properties = ({ schema: s, getSystem: o }) => { + const { fn: i } = o(), + { useComponent: u } = i.jsonSchema202012, + { getDependentRequired: _, getProperties: w } = i.jsonSchema202012.useFn(), + x = i.jsonSchema202012.useConfig(), + C = Array.isArray(s?.required) ? s.required : [], + j = u('JSONSchema'), + L = w(s, x); + return 0 === Object.keys(L).length + ? null + : Pe.createElement( + 'div', + { + className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--properties' + }, + Pe.createElement( + 'ul', + null, + Object.entries(L).map(([o, i]) => { + const u = C.includes(o), + w = _(o, s); + return Pe.createElement( + 'li', + { + key: o, + className: Hn()('json-schema-2020-12-property', { + 'json-schema-2020-12-property--required': u + }) + }, + Pe.createElement(j, { name: o, schema: i, dependentRequired: w }) + ); + }) + ) + ); + }, + Jj = createOnlyOAS31ComponentWrapper(keywords_Properties), + getProperties = (s, { includeReadOnly: o, includeWriteOnly: i }) => { + if (!s?.properties) return {}; + const u = Object.entries(s.properties).filter( + ([, s]) => (!(!0 === s?.readOnly) || o) && (!(!0 === s?.writeOnly) || i) + ); + return Object.fromEntries(u); + }; + const Gj = function oas31_after_load_afterLoad({ fn: s, getSystem: o }) { + if (s.jsonSchema202012) { + const i = ((s, o) => { + const { fn: i } = o(); + if ('function' != typeof s) return null; + const { hasKeyword: u } = i.jsonSchema202012; + return (o) => + s(o) || u(o, 'example') || o?.xml || o?.discriminator || o?.externalDocs; + })(s.jsonSchema202012.isExpandable, o); + Object.assign(this.fn.jsonSchema202012, { isExpandable: i, getProperties }); + } + if ('function' == typeof s.sampleFromSchema && s.jsonSchema202012) { + const i = ((s, o) => { + const { fn: i, specSelectors: u } = o; + return Object.fromEntries( + Object.entries(s).map(([s, o]) => { + const _ = i[s]; + return [ + s, + (...s) => (u.isOAS31() ? o(...s) : 'function' == typeof _ ? _(...s) : void 0) + ]; + }) + ); + })( + { + sampleFromSchema: s.jsonSchema202012.sampleFromSchema, + sampleFromSchemaGeneric: s.jsonSchema202012.sampleFromSchemaGeneric, + createXMLExample: s.jsonSchema202012.createXMLExample, + memoizedSampleFromSchema: s.jsonSchema202012.memoizedSampleFromSchema, + memoizedCreateXMLExample: s.jsonSchema202012.memoizedCreateXMLExample, + getJsonSampleSchema: s.jsonSchema202012.getJsonSampleSchema, + getYamlSampleSchema: s.jsonSchema202012.getYamlSampleSchema, + getXmlSampleSchema: s.jsonSchema202012.getXmlSampleSchema, + getSampleSchema: s.jsonSchema202012.getSampleSchema, + mergeJsonSchema: s.jsonSchema202012.mergeJsonSchema + }, + o() + ); + Object.assign(this.fn, i); + } + }, + oas31 = ({ fn: s }) => { + const o = s.createSystemSelector || fn_createSystemSelector, + i = s.createOnlyOAS31Selector || fn_createOnlyOAS31Selector; + return { + afterLoad: Gj, + fn: { + isOAS31, + createSystemSelector: fn_createSystemSelector, + createOnlyOAS31Selector: fn_createOnlyOAS31Selector + }, + components: { + Webhooks: webhooks, + JsonSchemaDialect: json_schema_dialect, + MutualTLSAuth: mutual_tls_auth, + OAS31Info: oas31_components_info, + OAS31License: oas31_components_license, + OAS31Contact: oas31_components_contact, + OAS31VersionPragmaFilter: version_pragma_filter, + OAS31Model: FA, + OAS31Models: models, + OAS31Auths: qA, + JSONSchema202012KeywordExample: keywords_Example, + JSONSchema202012KeywordXml: keywords_Xml, + JSONSchema202012KeywordDiscriminator: keywords_Discriminator_Discriminator, + JSONSchema202012KeywordExternalDocs: keywords_ExternalDocs + }, + wrapComponents: { + InfoContainer: UA, + License: $A, + Contact: VA, + VersionPragmaFilter: wrap_components_version_pragma_filter, + Model: WA, + Models: HA, + AuthItem: GA, + auths: YA, + JSONSchema202012KeywordDescription: $j, + JSONSchema202012KeywordDefault: Kj, + JSONSchema202012KeywordProperties: Jj + }, + statePlugins: { + auth: { wrapSelectors: { definitionsToAuthorize: Nj } }, + spec: { + selectors: { + isOAS31: o(ZA), + license: selectors_license, + selectLicenseNameField, + selectLicenseUrlField, + selectLicenseIdentifierField: i(selectLicenseIdentifierField), + selectLicenseUrl: o(ej), + contact: selectors_contact, + selectContactNameField, + selectContactEmailField, + selectContactUrlField, + selectContactUrl: o(fj), + selectInfoTitleField, + selectInfoSummaryField: i(selectInfoSummaryField), + selectInfoDescriptionField, + selectInfoTermsOfServiceField, + selectInfoTermsOfServiceUrl: o(mj), + selectExternalDocsDescriptionField, + selectExternalDocsUrlField, + selectExternalDocsUrl: o(_j), + webhooks: i(selectors_webhooks), + selectWebhooksOperations: i(o(QA)), + selectJsonSchemaDialectField, + selectJsonSchemaDialectDefault, + selectSchemas: o(Cj) + }, + wrapSelectors: { isOAS3: wrap_selectors_isOAS3, selectLicenseUrl: Aj } + }, + oas31: { selectors: { selectLicenseUrl: i(o(Bj)) } } + } + }; + }, + Xj = ts().object, + eI = ts().bool, + tI = (ts().oneOfType([Xj, eI]), (0, Pe.createContext)(null)); + tI.displayName = 'JSONSchemaContext'; + const rI = (0, Pe.createContext)(0); + rI.displayName = 'JSONSchemaLevelContext'; + const nI = (0, Pe.createContext)(!1); + nI.displayName = 'JSONSchemaDeepExpansionContext'; + const sI = (0, Pe.createContext)(new Set()), + useConfig = () => { + const { config: s } = (0, Pe.useContext)(tI); + return s; + }, + useComponent = (s) => { + const { components: o } = (0, Pe.useContext)(tI); + return o[s] || null; + }, + useFn = (s = void 0) => { + const { fn: o } = (0, Pe.useContext)(tI); + return void 0 !== s ? o[s] : o; + }, + useLevel = () => { + const s = (0, Pe.useContext)(rI); + return [s, s + 1]; + }, + useIsExpanded = () => { + const [s] = useLevel(), + { defaultExpandedLevels: o } = useConfig(); + return o - s > 0; + }, + useIsExpandedDeeply = () => (0, Pe.useContext)(nI), + useRenderedSchemas = (s = void 0) => { + if (void 0 === s) return (0, Pe.useContext)(sI); + const o = (0, Pe.useContext)(sI); + return new Set([...o, s]); + }, + oI = (0, Pe.forwardRef)( + ({ schema: s, name: o = '', dependentRequired: i = [], onExpand: u = () => {} }, _) => { + const w = useFn(), + x = useIsExpanded(), + C = useIsExpandedDeeply(), + [j, L] = (0, Pe.useState)(x || C), + [B, $] = (0, Pe.useState)(C), + [V, U] = useLevel(), + z = (() => { + const [s] = useLevel(); + return s > 0; + })(), + Y = w.isExpandable(s) || i.length > 0, + Z = ((s) => useRenderedSchemas().has(s))(s), + ee = useRenderedSchemas(s), + ie = w.stringifyConstraints(s), + ae = useComponent('Accordion'), + le = useComponent('Keyword$schema'), + ce = useComponent('Keyword$vocabulary'), + pe = useComponent('Keyword$id'), + de = useComponent('Keyword$anchor'), + fe = useComponent('Keyword$dynamicAnchor'), + ye = useComponent('Keyword$ref'), + be = useComponent('Keyword$dynamicRef'), + _e = useComponent('Keyword$defs'), + we = useComponent('Keyword$comment'), + Se = useComponent('KeywordAllOf'), + xe = useComponent('KeywordAnyOf'), + Te = useComponent('KeywordOneOf'), + Re = useComponent('KeywordNot'), + qe = useComponent('KeywordIf'), + $e = useComponent('KeywordThen'), + ze = useComponent('KeywordElse'), + We = useComponent('KeywordDependentSchemas'), + He = useComponent('KeywordPrefixItems'), + Ye = useComponent('KeywordItems'), + Xe = useComponent('KeywordContains'), + Qe = useComponent('KeywordProperties'), + et = useComponent('KeywordPatternProperties'), + tt = useComponent('KeywordAdditionalProperties'), + rt = useComponent('KeywordPropertyNames'), + nt = useComponent('KeywordUnevaluatedItems'), + st = useComponent('KeywordUnevaluatedProperties'), + ot = useComponent('KeywordType'), + it = useComponent('KeywordEnum'), + at = useComponent('KeywordConst'), + lt = useComponent('KeywordConstraint'), + ct = useComponent('KeywordDependentRequired'), + ut = useComponent('KeywordContentSchema'), + pt = useComponent('KeywordTitle'), + ht = useComponent('KeywordDescription'), + dt = useComponent('KeywordDefault'), + mt = useComponent('KeywordDeprecated'), + gt = useComponent('KeywordReadOnly'), + yt = useComponent('KeywordWriteOnly'), + vt = useComponent('ExpandDeepButton'); + ((0, Pe.useEffect)(() => { + $(C); + }, [C]), + (0, Pe.useEffect)(() => { + $(B); + }, [B])); + const bt = (0, Pe.useCallback)( + (s, o) => { + (L(o), !o && $(!1), u(s, o, !1)); + }, + [u] + ), + _t = (0, Pe.useCallback)( + (s, o) => { + (L(o), $(o), u(s, o, !0)); + }, + [u] + ); + return Pe.createElement( + rI.Provider, + { value: U }, + Pe.createElement( + nI.Provider, + { value: B }, + Pe.createElement( + sI.Provider, + { value: ee }, + Pe.createElement( + 'article', + { + ref: _, + 'data-json-schema-level': V, + className: Hn()('json-schema-2020-12', { + 'json-schema-2020-12--embedded': z, + 'json-schema-2020-12--circular': Z + }) + }, + Pe.createElement( + 'div', + { className: 'json-schema-2020-12-head' }, + Y && !Z + ? Pe.createElement( + Pe.Fragment, + null, + Pe.createElement( + ae, + { expanded: j, onChange: bt }, + Pe.createElement(pt, { title: o, schema: s }) + ), + Pe.createElement(vt, { expanded: j, onClick: _t }) + ) + : Pe.createElement(pt, { title: o, schema: s }), + Pe.createElement(mt, { schema: s }), + Pe.createElement(gt, { schema: s }), + Pe.createElement(yt, { schema: s }), + Pe.createElement(ot, { schema: s, isCircular: Z }), + ie.length > 0 && + ie.map((s) => + Pe.createElement(lt, { key: `${s.scope}-${s.value}`, constraint: s }) + ) + ), + Pe.createElement( + 'div', + { + className: Hn()('json-schema-2020-12-body', { + 'json-schema-2020-12-body--collapsed': !j + }) + }, + j && + Pe.createElement( + Pe.Fragment, + null, + Pe.createElement(ht, { schema: s }), + !Z && + Y && + Pe.createElement( + Pe.Fragment, + null, + Pe.createElement(Qe, { schema: s }), + Pe.createElement(et, { schema: s }), + Pe.createElement(tt, { schema: s }), + Pe.createElement(st, { schema: s }), + Pe.createElement(rt, { schema: s }), + Pe.createElement(Se, { schema: s }), + Pe.createElement(xe, { schema: s }), + Pe.createElement(Te, { schema: s }), + Pe.createElement(Re, { schema: s }), + Pe.createElement(qe, { schema: s }), + Pe.createElement($e, { schema: s }), + Pe.createElement(ze, { schema: s }), + Pe.createElement(We, { schema: s }), + Pe.createElement(He, { schema: s }), + Pe.createElement(Ye, { schema: s }), + Pe.createElement(nt, { schema: s }), + Pe.createElement(Xe, { schema: s }), + Pe.createElement(ut, { schema: s }) + ), + Pe.createElement(it, { schema: s }), + Pe.createElement(at, { schema: s }), + Pe.createElement(ct, { schema: s, dependentRequired: i }), + Pe.createElement(dt, { schema: s }), + Pe.createElement(le, { schema: s }), + Pe.createElement(ce, { schema: s }), + Pe.createElement(pe, { schema: s }), + Pe.createElement(de, { schema: s }), + Pe.createElement(fe, { schema: s }), + Pe.createElement(ye, { schema: s }), + !Z && Y && Pe.createElement(_e, { schema: s }), + Pe.createElement(be, { schema: s }), + Pe.createElement(we, { schema: s }) + ) + ) + ) + ) + ) + ); + } + ), + iI = oI, + keywords_$schema = ({ schema: s }) => + s?.$schema + ? Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--$schema' }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + '$schema' + ), + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' + }, + s.$schema + ) + ) + : null, + $vocabulary_$vocabulary = ({ schema: s }) => { + const o = useIsExpanded(), + i = useIsExpandedDeeply(), + [u, _] = (0, Pe.useState)(o || i), + w = useComponent('Accordion'), + x = (0, Pe.useCallback)(() => { + _((s) => !s); + }, []); + return s?.$vocabulary + ? 'object' != typeof s.$vocabulary + ? null + : Pe.createElement( + 'div', + { + className: + 'json-schema-2020-12-keyword json-schema-2020-12-keyword--$vocabulary' + }, + Pe.createElement( + w, + { expanded: u, onChange: x }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + '$vocabulary' + ) + ), + Pe.createElement( + 'strong', + { + className: + 'json-schema-2020-12__attribute json-schema-2020-12__attribute--primary' + }, + 'object' + ), + Pe.createElement( + 'ul', + null, + u && + Object.entries(s.$vocabulary).map(([s, o]) => + Pe.createElement( + 'li', + { + key: s, + className: Hn()('json-schema-2020-12-$vocabulary-uri', { + 'json-schema-2020-12-$vocabulary-uri--disabled': !o + }) + }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' + }, + s + ) + ) + ) + ) + ) + : null; + }, + keywords_$id = ({ schema: s }) => + s?.$id + ? Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--$id' }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + '$id' + ), + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' + }, + s.$id + ) + ) + : null, + keywords_$anchor = ({ schema: s }) => + s?.$anchor + ? Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--$anchor' }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + '$anchor' + ), + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' + }, + s.$anchor + ) + ) + : null, + keywords_$dynamicAnchor = ({ schema: s }) => + s?.$dynamicAnchor + ? Pe.createElement( + 'div', + { + className: + 'json-schema-2020-12-keyword json-schema-2020-12-keyword--$dynamicAnchor' + }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + '$dynamicAnchor' + ), + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' + }, + s.$dynamicAnchor + ) + ) + : null, + keywords_$ref = ({ schema: s }) => + s?.$ref + ? Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--$ref' }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + '$ref' + ), + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' + }, + s.$ref + ) + ) + : null, + keywords_$dynamicRef = ({ schema: s }) => + s?.$dynamicRef + ? Pe.createElement( + 'div', + { + className: + 'json-schema-2020-12-keyword json-schema-2020-12-keyword--$dynamicRef' + }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + '$dynamicRef' + ), + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' + }, + s.$dynamicRef + ) + ) + : null, + keywords_$defs = ({ schema: s }) => { + const o = s?.$defs || {}, + i = useIsExpanded(), + u = useIsExpandedDeeply(), + [_, w] = (0, Pe.useState)(i || u), + [x, C] = (0, Pe.useState)(!1), + j = useComponent('Accordion'), + L = useComponent('ExpandDeepButton'), + B = useComponent('JSONSchema'), + $ = (0, Pe.useCallback)(() => { + w((s) => !s); + }, []), + V = (0, Pe.useCallback)((s, o) => { + (w(o), C(o)); + }, []); + return 0 === Object.keys(o).length + ? null + : Pe.createElement( + nI.Provider, + { value: x }, + Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--$defs' }, + Pe.createElement( + j, + { expanded: _, onChange: $ }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + '$defs' + ) + ), + Pe.createElement(L, { expanded: _, onClick: V }), + Pe.createElement( + 'strong', + { + className: + 'json-schema-2020-12__attribute json-schema-2020-12__attribute--primary' + }, + 'object' + ), + Pe.createElement( + 'ul', + { + className: Hn()('json-schema-2020-12-keyword__children', { + 'json-schema-2020-12-keyword__children--collapsed': !_ + }) + }, + _ && + Pe.createElement( + Pe.Fragment, + null, + Object.entries(o).map(([s, o]) => + Pe.createElement( + 'li', + { key: s, className: 'json-schema-2020-12-property' }, + Pe.createElement(B, { name: s, schema: o }) + ) + ) + ) + ) + ) + ); + }, + keywords_$comment = ({ schema: s }) => + s?.$comment + ? Pe.createElement( + 'div', + { + className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--$comment' + }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' + }, + '$comment' + ), + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' + }, + s.$comment + ) + ) + : null, + keywords_AllOf = ({ schema: s }) => { + const o = s?.allOf || [], + i = useFn(), + u = useIsExpanded(), + _ = useIsExpandedDeeply(), + [w, x] = (0, Pe.useState)(u || _), + [C, j] = (0, Pe.useState)(!1), + L = useComponent('Accordion'), + B = useComponent('ExpandDeepButton'), + $ = useComponent('JSONSchema'), + V = useComponent('KeywordType'), + U = (0, Pe.useCallback)(() => { + x((s) => !s); + }, []), + z = (0, Pe.useCallback)((s, o) => { + (x(o), j(o)); + }, []); + return Array.isArray(o) && 0 !== o.length + ? Pe.createElement( + nI.Provider, + { value: C }, + Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--allOf' }, + Pe.createElement( + L, + { expanded: w, onChange: U }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'All of' + ) + ), + Pe.createElement(B, { expanded: w, onClick: z }), + Pe.createElement(V, { schema: { allOf: o } }), + Pe.createElement( + 'ul', + { + className: Hn()('json-schema-2020-12-keyword__children', { + 'json-schema-2020-12-keyword__children--collapsed': !w + }) + }, + w && + Pe.createElement( + Pe.Fragment, + null, + o.map((s, o) => + Pe.createElement( + 'li', + { key: `#${o}`, className: 'json-schema-2020-12-property' }, + Pe.createElement($, { name: `#${o} ${i.getTitle(s)}`, schema: s }) + ) + ) + ) + ) + ) + ) + : null; + }, + keywords_AnyOf = ({ schema: s }) => { + const o = s?.anyOf || [], + i = useFn(), + u = useIsExpanded(), + _ = useIsExpandedDeeply(), + [w, x] = (0, Pe.useState)(u || _), + [C, j] = (0, Pe.useState)(!1), + L = useComponent('Accordion'), + B = useComponent('ExpandDeepButton'), + $ = useComponent('JSONSchema'), + V = useComponent('KeywordType'), + U = (0, Pe.useCallback)(() => { + x((s) => !s); + }, []), + z = (0, Pe.useCallback)((s, o) => { + (x(o), j(o)); + }, []); + return Array.isArray(o) && 0 !== o.length + ? Pe.createElement( + nI.Provider, + { value: C }, + Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--anyOf' }, + Pe.createElement( + L, + { expanded: w, onChange: U }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'Any of' + ) + ), + Pe.createElement(B, { expanded: w, onClick: z }), + Pe.createElement(V, { schema: { anyOf: o } }), + Pe.createElement( + 'ul', + { + className: Hn()('json-schema-2020-12-keyword__children', { + 'json-schema-2020-12-keyword__children--collapsed': !w + }) + }, + w && + Pe.createElement( + Pe.Fragment, + null, + o.map((s, o) => + Pe.createElement( + 'li', + { key: `#${o}`, className: 'json-schema-2020-12-property' }, + Pe.createElement($, { name: `#${o} ${i.getTitle(s)}`, schema: s }) + ) + ) + ) + ) + ) + ) + : null; + }, + keywords_OneOf = ({ schema: s }) => { + const o = s?.oneOf || [], + i = useFn(), + u = useIsExpanded(), + _ = useIsExpandedDeeply(), + [w, x] = (0, Pe.useState)(u || _), + [C, j] = (0, Pe.useState)(!1), + L = useComponent('Accordion'), + B = useComponent('ExpandDeepButton'), + $ = useComponent('JSONSchema'), + V = useComponent('KeywordType'), + U = (0, Pe.useCallback)(() => { + x((s) => !s); + }, []), + z = (0, Pe.useCallback)((s, o) => { + (x(o), j(o)); + }, []); + return Array.isArray(o) && 0 !== o.length + ? Pe.createElement( + nI.Provider, + { value: C }, + Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--oneOf' }, + Pe.createElement( + L, + { expanded: w, onChange: U }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'One of' + ) + ), + Pe.createElement(B, { expanded: w, onClick: z }), + Pe.createElement(V, { schema: { oneOf: o } }), + Pe.createElement( + 'ul', + { + className: Hn()('json-schema-2020-12-keyword__children', { + 'json-schema-2020-12-keyword__children--collapsed': !w + }) + }, + w && + Pe.createElement( + Pe.Fragment, + null, + o.map((s, o) => + Pe.createElement( + 'li', + { key: `#${o}`, className: 'json-schema-2020-12-property' }, + Pe.createElement($, { name: `#${o} ${i.getTitle(s)}`, schema: s }) + ) + ) + ) + ) + ) + ) + : null; + }, + keywords_Not = ({ schema: s }) => { + const o = useFn(), + i = useComponent('JSONSchema'); + if (!o.hasKeyword(s, 'not')) return null; + const u = Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'Not' + ); + return Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--not' }, + Pe.createElement(i, { name: u, schema: s.not }) + ); + }, + keywords_If = ({ schema: s }) => { + const o = useFn(), + i = useComponent('JSONSchema'); + if (!o.hasKeyword(s, 'if')) return null; + const u = Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'If' + ); + return Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--if' }, + Pe.createElement(i, { name: u, schema: s.if }) + ); + }, + keywords_Then = ({ schema: s }) => { + const o = useFn(), + i = useComponent('JSONSchema'); + if (!o.hasKeyword(s, 'then')) return null; + const u = Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'Then' + ); + return Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--then' }, + Pe.createElement(i, { name: u, schema: s.then }) + ); + }, + keywords_Else = ({ schema: s }) => { + const o = useFn(), + i = useComponent('JSONSchema'); + if (!o.hasKeyword(s, 'else')) return null; + const u = Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'Else' + ); + return Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--if' }, + Pe.createElement(i, { name: u, schema: s.else }) + ); + }, + keywords_DependentSchemas = ({ schema: s }) => { + const o = s?.dependentSchemas || [], + i = useIsExpanded(), + u = useIsExpandedDeeply(), + [_, w] = (0, Pe.useState)(i || u), + [x, C] = (0, Pe.useState)(!1), + j = useComponent('Accordion'), + L = useComponent('ExpandDeepButton'), + B = useComponent('JSONSchema'), + $ = (0, Pe.useCallback)(() => { + w((s) => !s); + }, []), + V = (0, Pe.useCallback)((s, o) => { + (w(o), C(o)); + }, []); + return 'object' != typeof o || 0 === Object.keys(o).length + ? null + : Pe.createElement( + nI.Provider, + { value: x }, + Pe.createElement( + 'div', + { + className: + 'json-schema-2020-12-keyword json-schema-2020-12-keyword--dependentSchemas' + }, + Pe.createElement( + j, + { expanded: _, onChange: $ }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'Dependent schemas' + ) + ), + Pe.createElement(L, { expanded: _, onClick: V }), + Pe.createElement( + 'strong', + { + className: + 'json-schema-2020-12__attribute json-schema-2020-12__attribute--primary' + }, + 'object' + ), + Pe.createElement( + 'ul', + { + className: Hn()('json-schema-2020-12-keyword__children', { + 'json-schema-2020-12-keyword__children--collapsed': !_ + }) + }, + _ && + Pe.createElement( + Pe.Fragment, + null, + Object.entries(o).map(([s, o]) => + Pe.createElement( + 'li', + { key: s, className: 'json-schema-2020-12-property' }, + Pe.createElement(B, { name: s, schema: o }) + ) + ) + ) + ) + ) + ); + }, + keywords_PrefixItems = ({ schema: s }) => { + const o = s?.prefixItems || [], + i = useFn(), + u = useIsExpanded(), + _ = useIsExpandedDeeply(), + [w, x] = (0, Pe.useState)(u || _), + [C, j] = (0, Pe.useState)(!1), + L = useComponent('Accordion'), + B = useComponent('ExpandDeepButton'), + $ = useComponent('JSONSchema'), + V = useComponent('KeywordType'), + U = (0, Pe.useCallback)(() => { + x((s) => !s); + }, []), + z = (0, Pe.useCallback)((s, o) => { + (x(o), j(o)); + }, []); + return Array.isArray(o) && 0 !== o.length + ? Pe.createElement( + nI.Provider, + { value: C }, + Pe.createElement( + 'div', + { + className: + 'json-schema-2020-12-keyword json-schema-2020-12-keyword--prefixItems' + }, + Pe.createElement( + L, + { expanded: w, onChange: U }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'Prefix items' + ) + ), + Pe.createElement(B, { expanded: w, onClick: z }), + Pe.createElement(V, { schema: { prefixItems: o } }), + Pe.createElement( + 'ul', + { + className: Hn()('json-schema-2020-12-keyword__children', { + 'json-schema-2020-12-keyword__children--collapsed': !w + }) + }, + w && + Pe.createElement( + Pe.Fragment, + null, + o.map((s, o) => + Pe.createElement( + 'li', + { key: `#${o}`, className: 'json-schema-2020-12-property' }, + Pe.createElement($, { name: `#${o} ${i.getTitle(s)}`, schema: s }) + ) + ) + ) + ) + ) + ) + : null; + }, + keywords_Items = ({ schema: s }) => { + const o = useFn(), + i = useComponent('JSONSchema'); + if (!o.hasKeyword(s, 'items')) return null; + const u = Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'Items' + ); + return Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--items' }, + Pe.createElement(i, { name: u, schema: s.items }) + ); + }, + keywords_Contains = ({ schema: s }) => { + const o = useFn(), + i = useComponent('JSONSchema'); + if (!o.hasKeyword(s, 'contains')) return null; + const u = Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'Contains' + ); + return Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--contains' }, + Pe.createElement(i, { name: u, schema: s.contains }) + ); + }, + keywords_Properties_Properties = ({ schema: s }) => { + const o = useFn(), + i = s?.properties || {}, + u = Array.isArray(s?.required) ? s.required : [], + _ = useComponent('JSONSchema'); + return 0 === Object.keys(i).length + ? null + : Pe.createElement( + 'div', + { + className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--properties' + }, + Pe.createElement( + 'ul', + null, + Object.entries(i).map(([i, w]) => { + const x = u.includes(i), + C = o.getDependentRequired(i, s); + return Pe.createElement( + 'li', + { + key: i, + className: Hn()('json-schema-2020-12-property', { + 'json-schema-2020-12-property--required': x + }) + }, + Pe.createElement(_, { name: i, schema: w, dependentRequired: C }) + ); + }) + ) + ); + }, + PatternProperties_PatternProperties = ({ schema: s }) => { + const o = s?.patternProperties || {}, + i = useComponent('JSONSchema'); + return 0 === Object.keys(o).length + ? null + : Pe.createElement( + 'div', + { + className: + 'json-schema-2020-12-keyword json-schema-2020-12-keyword--patternProperties' + }, + Pe.createElement( + 'ul', + null, + Object.entries(o).map(([s, o]) => + Pe.createElement( + 'li', + { key: s, className: 'json-schema-2020-12-property' }, + Pe.createElement(i, { name: s, schema: o }) + ) + ) + ) + ); + }, + keywords_AdditionalProperties = ({ schema: s }) => { + const o = useFn(), + { additionalProperties: i } = s, + u = useComponent('JSONSchema'); + if (!o.hasKeyword(s, 'additionalProperties')) return null; + const _ = Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'Additional properties' + ); + return Pe.createElement( + 'div', + { + className: + 'json-schema-2020-12-keyword json-schema-2020-12-keyword--additionalProperties' + }, + !0 === i + ? Pe.createElement( + Pe.Fragment, + null, + _, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12__attribute json-schema-2020-12__attribute--primary' + }, + 'allowed' + ) + ) + : !1 === i + ? Pe.createElement( + Pe.Fragment, + null, + _, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12__attribute json-schema-2020-12__attribute--primary' + }, + 'forbidden' + ) + ) + : Pe.createElement(u, { name: _, schema: i }) + ); + }, + keywords_PropertyNames = ({ schema: s }) => { + const o = useFn(), + { propertyNames: i } = s, + u = useComponent('JSONSchema'), + _ = Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'Property names' + ); + return o.hasKeyword(s, 'propertyNames') + ? Pe.createElement( + 'div', + { + className: + 'json-schema-2020-12-keyword json-schema-2020-12-keyword--propertyNames' + }, + Pe.createElement(u, { name: _, schema: i }) + ) + : null; + }, + keywords_UnevaluatedItems = ({ schema: s }) => { + const o = useFn(), + { unevaluatedItems: i } = s, + u = useComponent('JSONSchema'); + if (!o.hasKeyword(s, 'unevaluatedItems')) return null; + const _ = Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'Unevaluated items' + ); + return Pe.createElement( + 'div', + { + className: + 'json-schema-2020-12-keyword json-schema-2020-12-keyword--unevaluatedItems' + }, + Pe.createElement(u, { name: _, schema: i }) + ); + }, + keywords_UnevaluatedProperties = ({ schema: s }) => { + const o = useFn(), + { unevaluatedProperties: i } = s, + u = useComponent('JSONSchema'); + if (!o.hasKeyword(s, 'unevaluatedProperties')) return null; + const _ = Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'Unevaluated properties' + ); + return Pe.createElement( + 'div', + { + className: + 'json-schema-2020-12-keyword json-schema-2020-12-keyword--unevaluatedProperties' + }, + Pe.createElement(u, { name: _, schema: i }) + ); + }, + keywords_Type = ({ schema: s, isCircular: o = !1 }) => { + const i = useFn().getType(s), + u = o ? ' [circular]' : ''; + return Pe.createElement( + 'strong', + { + className: 'json-schema-2020-12__attribute json-schema-2020-12__attribute--primary' + }, + `${i}${u}` + ); + }, + Enum_Enum = ({ schema: s }) => { + const o = useFn(); + return Array.isArray(s?.enum) + ? Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--enum' }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'Allowed values' + ), + Pe.createElement( + 'ul', + null, + s.enum.map((s) => { + const i = o.stringify(s); + return Pe.createElement( + 'li', + { key: i }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--const' + }, + i + ) + ); + }) + ) + ) + : null; + }, + keywords_Const = ({ schema: s }) => { + const o = useFn(); + return o.hasKeyword(s, 'const') + ? Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--const' }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'Const' + ), + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--const' + }, + o.stringify(s.const) + ) + ) + : null; + }, + Constraint = ({ constraint: s }) => + Pe.createElement( + 'span', + { + className: `json-schema-2020-12__constraint json-schema-2020-12__constraint--${s.scope}` + }, + s.value + ), + aI = Pe.memo(Constraint), + DependentRequired_DependentRequired = ({ dependentRequired: s }) => + 0 === s.length + ? null + : Pe.createElement( + 'div', + { + className: + 'json-schema-2020-12-keyword json-schema-2020-12-keyword--dependentRequired' + }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'Required when defined' + ), + Pe.createElement( + 'ul', + null, + s.map((s) => + Pe.createElement( + 'li', + { key: s }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--warning' + }, + s + ) + ) + ) + ) + ), + keywords_ContentSchema = ({ schema: s }) => { + const o = useFn(), + i = useComponent('JSONSchema'); + if (!o.hasKeyword(s, 'contentSchema')) return null; + const u = Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'Content schema' + ); + return Pe.createElement( + 'div', + { + className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--contentSchema' + }, + Pe.createElement(i, { name: u, schema: s.contentSchema }) + ); + }, + Title_Title = ({ title: s = '', schema: o }) => { + const i = useFn(), + u = s || i.getTitle(o); + return u + ? Pe.createElement('div', { className: 'json-schema-2020-12__title' }, u) + : null; + }, + keywords_Description_Description = ({ schema: s }) => + s?.description + ? Pe.createElement( + 'div', + { + className: + 'json-schema-2020-12-keyword json-schema-2020-12-keyword--description' + }, + Pe.createElement( + 'div', + { + className: + 'json-schema-2020-12-core-keyword__value json-schema-2020-12-core-keyword__value--secondary' + }, + s.description + ) + ) + : null, + keywords_Default = ({ schema: s }) => { + const o = useFn(); + return o.hasKeyword(s, 'default') + ? Pe.createElement( + 'div', + { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--default' }, + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' + }, + 'Default' + ), + Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--const' + }, + o.stringify(s.default) + ) + ) + : null; + }, + keywords_Deprecated = ({ schema: s }) => + !0 !== s?.deprecated + ? null + : Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12__attribute json-schema-2020-12__attribute--warning' + }, + 'deprecated' + ), + keywords_ReadOnly = ({ schema: s }) => + !0 !== s?.readOnly + ? null + : Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12__attribute json-schema-2020-12__attribute--muted' + }, + 'read-only' + ), + keywords_WriteOnly = ({ schema: s }) => + !0 !== s?.writeOnly + ? null + : Pe.createElement( + 'span', + { + className: + 'json-schema-2020-12__attribute json-schema-2020-12__attribute--muted' + }, + 'write-only' + ), + Accordion_Accordion = ({ expanded: s = !1, children: o, onChange: i }) => { + const u = useComponent('ChevronRightIcon'), + _ = (0, Pe.useCallback)( + (o) => { + i(o, !s); + }, + [s, i] + ); + return Pe.createElement( + 'button', + { type: 'button', className: 'json-schema-2020-12-accordion', onClick: _ }, + Pe.createElement('div', { className: 'json-schema-2020-12-accordion__children' }, o), + Pe.createElement( + 'span', + { + className: Hn()('json-schema-2020-12-accordion__icon', { + 'json-schema-2020-12-accordion__icon--expanded': s, + 'json-schema-2020-12-accordion__icon--collapsed': !s + }) + }, + Pe.createElement(u, null) + ) + ); + }, + ExpandDeepButton_ExpandDeepButton = ({ expanded: s, onClick: o }) => { + const i = (0, Pe.useCallback)( + (i) => { + o(i, !s); + }, + [s, o] + ); + return Pe.createElement( + 'button', + { type: 'button', className: 'json-schema-2020-12-expand-deep-button', onClick: i }, + s ? 'Collapse all' : 'Expand all' + ); + }, + icons_ChevronRight = () => + Pe.createElement( + 'svg', + { + xmlns: 'http://www.w3.org/2000/svg', + width: '24', + height: '24', + viewBox: '0 0 24 24' + }, + Pe.createElement('path', { d: 'M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z' }) + ), + fn_upperFirst = (s) => + 'string' == typeof s ? `${s.charAt(0).toUpperCase()}${s.slice(1)}` : s, + getTitle = (s, { lookup: o = 'extended' } = {}) => { + const i = useFn(); + if (null != s?.title) return i.upperFirst(String(s.title)); + if ('extended' === o) { + if (null != s?.$anchor) return i.upperFirst(String(s.$anchor)); + if (null != s?.$id) return String(s.$id); + } + return ''; + }, + getType = (s, o = new WeakSet()) => { + const i = useFn(); + if (null == s) return 'any'; + if (i.isBooleanJSONSchema(s)) return s ? 'any' : 'never'; + if ('object' != typeof s) return 'any'; + if (o.has(s)) return 'any'; + o.add(s); + const { type: u, prefixItems: _, items: w } = s, + getArrayType = () => { + if (Array.isArray(_)) { + const s = _.map((s) => getType(s, o)), + i = w ? getType(w, o) : 'any'; + return `array<[${s.join(', ')}], ${i}>`; + } + if (w) { + return `array<${getType(w, o)}>`; + } + return 'array'; + }; + if (s.not && 'any' === getType(s.not)) return 'never'; + const handleCombiningKeywords = (i, u) => { + if (Array.isArray(s[i])) { + return `(${s[i].map((s) => getType(s, o)).join(u)})`; + } + return null; + }, + x = [ + Array.isArray(u) + ? u.map((s) => ('array' === s ? getArrayType() : s)).join(' | ') + : 'array' === u + ? getArrayType() + : [ + 'null', + 'boolean', + 'object', + 'array', + 'number', + 'integer', + 'string' + ].includes(u) + ? u + : (() => { + if ( + Object.hasOwn(s, 'prefixItems') || + Object.hasOwn(s, 'items') || + Object.hasOwn(s, 'contains') + ) + return getArrayType(); + if ( + Object.hasOwn(s, 'properties') || + Object.hasOwn(s, 'additionalProperties') || + Object.hasOwn(s, 'patternProperties') + ) + return 'object'; + if (['int32', 'int64'].includes(s.format)) return 'integer'; + if (['float', 'double'].includes(s.format)) return 'number'; + if ( + Object.hasOwn(s, 'minimum') || + Object.hasOwn(s, 'maximum') || + Object.hasOwn(s, 'exclusiveMinimum') || + Object.hasOwn(s, 'exclusiveMaximum') || + Object.hasOwn(s, 'multipleOf') + ) + return 'number | integer'; + if ( + Object.hasOwn(s, 'pattern') || + Object.hasOwn(s, 'format') || + Object.hasOwn(s, 'minLength') || + Object.hasOwn(s, 'maxLength') + ) + return 'string'; + if (void 0 !== s.const) { + if (null === s.const) return 'null'; + if ('boolean' == typeof s.const) return 'boolean'; + if ('number' == typeof s.const) + return Number.isInteger(s.const) ? 'integer' : 'number'; + if ('string' == typeof s.const) return 'string'; + if (Array.isArray(s.const)) return 'array'; + if ('object' == typeof s.const) return 'object'; + } + return null; + })(), + handleCombiningKeywords('oneOf', ' | '), + handleCombiningKeywords('anyOf', ' | '), + handleCombiningKeywords('allOf', ' & ') + ] + .filter(Boolean) + .join(' | '); + return (o.delete(s), x || 'any'); + }, + isBooleanJSONSchema = (s) => 'boolean' == typeof s, + hasKeyword = (s, o) => null !== s && 'object' == typeof s && Object.hasOwn(s, o), + isExpandable = (s) => { + const o = useFn(); + return ( + s?.$schema || + s?.$vocabulary || + s?.$id || + s?.$anchor || + s?.$dynamicAnchor || + s?.$ref || + s?.$dynamicRef || + s?.$defs || + s?.$comment || + s?.allOf || + s?.anyOf || + s?.oneOf || + o.hasKeyword(s, 'not') || + o.hasKeyword(s, 'if') || + o.hasKeyword(s, 'then') || + o.hasKeyword(s, 'else') || + s?.dependentSchemas || + s?.prefixItems || + o.hasKeyword(s, 'items') || + o.hasKeyword(s, 'contains') || + s?.properties || + s?.patternProperties || + o.hasKeyword(s, 'additionalProperties') || + o.hasKeyword(s, 'propertyNames') || + o.hasKeyword(s, 'unevaluatedItems') || + o.hasKeyword(s, 'unevaluatedProperties') || + s?.description || + s?.enum || + o.hasKeyword(s, 'const') || + o.hasKeyword(s, 'contentSchema') || + o.hasKeyword(s, 'default') + ); + }, + fn_stringify = (s) => + null === s || ['number', 'bigint', 'boolean'].includes(typeof s) + ? String(s) + : Array.isArray(s) + ? `[${s.map(fn_stringify).join(', ')}]` + : JSON.stringify(s), + stringifyConstraintRange = (s, o, i) => { + const u = 'number' == typeof o, + _ = 'number' == typeof i; + return u && _ + ? o === i + ? `${o} ${s}` + : `[${o}, ${i}] ${s}` + : u + ? `>= ${o} ${s}` + : _ + ? `<= ${i} ${s}` + : null; + }, + stringifyConstraints = (s) => { + const o = [], + i = ((s) => { + if ('number' != typeof s?.multipleOf) return null; + if (s.multipleOf <= 0) return null; + if (1 === s.multipleOf) return null; + const { multipleOf: o } = s; + if (Number.isInteger(o)) return `multiple of ${o}`; + const i = 10 ** o.toString().split('.')[1].length; + return `multiple of ${o * i}/${i}`; + })(s); + null !== i && o.push({ scope: 'number', value: i }); + const u = ((s) => { + const o = s?.minimum, + i = s?.maximum, + u = s?.exclusiveMinimum, + _ = s?.exclusiveMaximum, + w = 'number' == typeof o, + x = 'number' == typeof i, + C = 'number' == typeof u, + j = 'number' == typeof _, + L = C && (!w || o < u), + B = j && (!x || i > _); + if ((w || C) && (x || j)) + return `${L ? '(' : '['}${L ? u : o}, ${B ? _ : i}${B ? ')' : ']'}`; + if (w || C) return `${L ? '>' : '≥'} ${L ? u : o}`; + if (x || j) return `${B ? '<' : '≤'} ${B ? _ : i}`; + return null; + })(s); + (null !== u && o.push({ scope: 'number', value: u }), + s?.format && o.push({ scope: 'string', value: s.format })); + const _ = stringifyConstraintRange('characters', s?.minLength, s?.maxLength); + (null !== _ && o.push({ scope: 'string', value: _ }), + s?.pattern && o.push({ scope: 'string', value: `matches ${s?.pattern}` }), + s?.contentMediaType && + o.push({ scope: 'string', value: `media type: ${s.contentMediaType}` }), + s?.contentEncoding && + o.push({ scope: 'string', value: `encoding: ${s.contentEncoding}` })); + const w = stringifyConstraintRange( + s?.hasUniqueItems ? 'unique items' : 'items', + s?.minItems, + s?.maxItems + ); + null !== w && o.push({ scope: 'array', value: w }); + const x = stringifyConstraintRange('contained items', s?.minContains, s?.maxContains); + null !== x && o.push({ scope: 'array', value: x }); + const C = stringifyConstraintRange('properties', s?.minProperties, s?.maxProperties); + return (null !== C && o.push({ scope: 'object', value: C }), o); + }, + getDependentRequired = (s, o) => + o?.dependentRequired + ? Array.from( + Object.entries(o.dependentRequired).reduce( + (o, [i, u]) => (Array.isArray(u) && u.includes(s) ? (o.add(i), o) : o), + new Set() + ) + ) + : [], + withJSONSchemaContext = (s, o = {}) => { + const i = { + components: { + JSONSchema: iI, + Keyword$schema: keywords_$schema, + Keyword$vocabulary: $vocabulary_$vocabulary, + Keyword$id: keywords_$id, + Keyword$anchor: keywords_$anchor, + Keyword$dynamicAnchor: keywords_$dynamicAnchor, + Keyword$ref: keywords_$ref, + Keyword$dynamicRef: keywords_$dynamicRef, + Keyword$defs: keywords_$defs, + Keyword$comment: keywords_$comment, + KeywordAllOf: keywords_AllOf, + KeywordAnyOf: keywords_AnyOf, + KeywordOneOf: keywords_OneOf, + KeywordNot: keywords_Not, + KeywordIf: keywords_If, + KeywordThen: keywords_Then, + KeywordElse: keywords_Else, + KeywordDependentSchemas: keywords_DependentSchemas, + KeywordPrefixItems: keywords_PrefixItems, + KeywordItems: keywords_Items, + KeywordContains: keywords_Contains, + KeywordProperties: keywords_Properties_Properties, + KeywordPatternProperties: PatternProperties_PatternProperties, + KeywordAdditionalProperties: keywords_AdditionalProperties, + KeywordPropertyNames: keywords_PropertyNames, + KeywordUnevaluatedItems: keywords_UnevaluatedItems, + KeywordUnevaluatedProperties: keywords_UnevaluatedProperties, + KeywordType: keywords_Type, + KeywordEnum: Enum_Enum, + KeywordConst: keywords_Const, + KeywordConstraint: aI, + KeywordDependentRequired: DependentRequired_DependentRequired, + KeywordContentSchema: keywords_ContentSchema, + KeywordTitle: Title_Title, + KeywordDescription: keywords_Description_Description, + KeywordDefault: keywords_Default, + KeywordDeprecated: keywords_Deprecated, + KeywordReadOnly: keywords_ReadOnly, + KeywordWriteOnly: keywords_WriteOnly, + Accordion: Accordion_Accordion, + ExpandDeepButton: ExpandDeepButton_ExpandDeepButton, + ChevronRightIcon: icons_ChevronRight, + ...o.components + }, + config: { + default$schema: 'https://json-schema.org/draft/2020-12/schema', + defaultExpandedLevels: 0, + ...o.config + }, + fn: { + upperFirst: fn_upperFirst, + getTitle, + getType, + isBooleanJSONSchema, + hasKeyword, + isExpandable, + stringify: fn_stringify, + stringifyConstraints, + getDependentRequired, + ...o.fn + } + }, + HOC = (o) => Pe.createElement(tI.Provider, { value: i }, Pe.createElement(s, o)); + return ( + (HOC.contexts = { JSONSchemaContext: tI }), + (HOC.displayName = s.displayName), + HOC + ); + }, + json_schema_2020_12 = () => ({ + components: { + JSONSchema202012: iI, + JSONSchema202012Keyword$schema: keywords_$schema, + JSONSchema202012Keyword$vocabulary: $vocabulary_$vocabulary, + JSONSchema202012Keyword$id: keywords_$id, + JSONSchema202012Keyword$anchor: keywords_$anchor, + JSONSchema202012Keyword$dynamicAnchor: keywords_$dynamicAnchor, + JSONSchema202012Keyword$ref: keywords_$ref, + JSONSchema202012Keyword$dynamicRef: keywords_$dynamicRef, + JSONSchema202012Keyword$defs: keywords_$defs, + JSONSchema202012Keyword$comment: keywords_$comment, + JSONSchema202012KeywordAllOf: keywords_AllOf, + JSONSchema202012KeywordAnyOf: keywords_AnyOf, + JSONSchema202012KeywordOneOf: keywords_OneOf, + JSONSchema202012KeywordNot: keywords_Not, + JSONSchema202012KeywordIf: keywords_If, + JSONSchema202012KeywordThen: keywords_Then, + JSONSchema202012KeywordElse: keywords_Else, + JSONSchema202012KeywordDependentSchemas: keywords_DependentSchemas, + JSONSchema202012KeywordPrefixItems: keywords_PrefixItems, + JSONSchema202012KeywordItems: keywords_Items, + JSONSchema202012KeywordContains: keywords_Contains, + JSONSchema202012KeywordProperties: keywords_Properties_Properties, + JSONSchema202012KeywordPatternProperties: PatternProperties_PatternProperties, + JSONSchema202012KeywordAdditionalProperties: keywords_AdditionalProperties, + JSONSchema202012KeywordPropertyNames: keywords_PropertyNames, + JSONSchema202012KeywordUnevaluatedItems: keywords_UnevaluatedItems, + JSONSchema202012KeywordUnevaluatedProperties: keywords_UnevaluatedProperties, + JSONSchema202012KeywordType: keywords_Type, + JSONSchema202012KeywordEnum: Enum_Enum, + JSONSchema202012KeywordConst: keywords_Const, + JSONSchema202012KeywordConstraint: aI, + JSONSchema202012KeywordDependentRequired: DependentRequired_DependentRequired, + JSONSchema202012KeywordContentSchema: keywords_ContentSchema, + JSONSchema202012KeywordTitle: Title_Title, + JSONSchema202012KeywordDescription: keywords_Description_Description, + JSONSchema202012KeywordDefault: keywords_Default, + JSONSchema202012KeywordDeprecated: keywords_Deprecated, + JSONSchema202012KeywordReadOnly: keywords_ReadOnly, + JSONSchema202012KeywordWriteOnly: keywords_WriteOnly, + JSONSchema202012Accordion: Accordion_Accordion, + JSONSchema202012ExpandDeepButton: ExpandDeepButton_ExpandDeepButton, + JSONSchema202012ChevronRightIcon: icons_ChevronRight, + withJSONSchema202012Context: withJSONSchemaContext, + JSONSchema202012DeepExpansionContext: () => nI + }, + fn: { + upperFirst: fn_upperFirst, + jsonSchema202012: { + isExpandable, + hasKeyword, + useFn, + useConfig, + useComponent, + useIsExpandedDeeply + } + } + }); + var lI = __webpack_require__(11331), + cI = __webpack_require__.n(lI); + const array = (s, { sample: o }) => + ((s, o = {}) => { + const { minItems: i, maxItems: u, uniqueItems: _ } = o, + { contains: w, minContains: x, maxContains: C } = o; + let j = [...s]; + if (null != w && 'object' == typeof w) { + if (Number.isInteger(x) && x > 1) { + const s = j.at(0); + for (let o = 1; o < x; o += 1) j.unshift(s); + } + Number.isInteger(C); + } + if ( + (Number.isInteger(u) && u > 0 && (j = s.slice(0, u)), Number.isInteger(i) && i > 0) + ) + for (let s = 0; j.length < i; s += 1) j.push(j[s % j.length]); + return (!0 === _ && (j = Array.from(new Set(j))), j); + })(o, s), + object = () => { + throw new Error('Not implemented'); + }, + bytes = (s) => St()(s), + random_pick = (s) => s.at(0), + predicates_isBooleanJSONSchema = (s) => 'boolean' == typeof s, + isJSONSchemaObject = (s) => cI()(s), + isJSONSchema = (s) => predicates_isBooleanJSONSchema(s) || isJSONSchemaObject(s); + const uI = class Registry { + data = {}; + register(s, o) { + this.data[s] = o; + } + unregister(s) { + void 0 === s ? (this.data = {}) : delete this.data[s]; + } + get(s) { + return this.data[s]; + } + }, + int32 = () => (2 ** 30) >>> 0, + int64 = () => 2 ** 53 - 1, + generators_float = () => 0.1, + generators_double = () => 0.1, + email = () => 'user@example.com', + idn_email = () => '실례@example.com', + hostname = () => 'example.com', + idn_hostname = () => '실례.com', + ipv4 = () => '198.51.100.42', + ipv6 = () => '2001:0db8:5b96:0000:0000:426f:8e17:642a', + uri = () => 'https://example.com/', + uri_reference = () => 'path/index.html', + iri = () => 'https://실례.com/', + iri_reference = () => 'path/실례.html', + uuid = () => '3fa85f64-5717-4562-b3fc-2c963f66afa6', + uri_template = () => 'https://example.com/dictionary/{term:1}/{term}', + json_pointer = () => '/a/b/c', + relative_json_pointer = () => '1/0', + date_time = () => new Date().toISOString(), + date = () => new Date().toISOString().substring(0, 10), + time = () => new Date().toISOString().substring(11), + duration = () => 'P3D', + generators_password = () => '********', + regex = () => '^[a-z]+$'; + const pI = new (class FormatRegistry extends uI { + #t = { + int32, + int64, + float: generators_float, + double: generators_double, + email, + 'idn-email': idn_email, + hostname, + 'idn-hostname': idn_hostname, + ipv4, + ipv6, + uri, + 'uri-reference': uri_reference, + iri, + 'iri-reference': iri_reference, + uuid, + 'uri-template': uri_template, + 'json-pointer': json_pointer, + 'relative-json-pointer': relative_json_pointer, + 'date-time': date_time, + date, + time, + duration, + password: generators_password, + regex + }; + data = { ...this.#t }; + get defaults() { + return { ...this.#t }; + } + })(), + formatAPI = (s, o) => + 'function' == typeof o ? pI.register(s, o) : null === o ? pI.unregister(s) : pI.get(s); + formatAPI.getDefaults = () => pI.defaults; + const hI = formatAPI; + var dI = __webpack_require__(48287).Buffer; + const _7bit = (s) => dI.from(s).toString('ascii'); + var fI = __webpack_require__(48287).Buffer; + const _8bit = (s) => fI.from(s).toString('utf8'); + var mI = __webpack_require__(48287).Buffer; + const encoders_binary = (s) => mI.from(s).toString('binary'), + quoted_printable = (s) => { + let o = ''; + for (let i = 0; i < s.length; i++) { + const u = s.charCodeAt(i); + if (61 === u) o += '=3D'; + else if ((u >= 33 && u <= 60) || (u >= 62 && u <= 126) || 9 === u || 32 === u) + o += s.charAt(i); + else if (13 === u || 10 === u) o += '\r\n'; + else if (u > 126) { + const u = unescape(encodeURIComponent(s.charAt(i))); + for (let s = 0; s < u.length; s++) + o += '=' + ('0' + u.charCodeAt(s).toString(16)).slice(-2).toUpperCase(); + } else o += '=' + ('0' + u.toString(16)).slice(-2).toUpperCase(); + } + return o; + }; + var gI = __webpack_require__(48287).Buffer; + const base16 = (s) => gI.from(s).toString('hex'); + var yI = __webpack_require__(48287).Buffer; + const base32 = (s) => { + const o = yI.from(s).toString('utf8'), + i = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + let u = 0, + _ = '', + w = 0, + x = 0; + for (let s = 0; s < o.length; s++) + for (w = (w << 8) | o.charCodeAt(s), x += 8; x >= 5; ) + ((_ += i.charAt((w >>> (x - 5)) & 31)), (x -= 5)); + x > 0 && ((_ += i.charAt((w << (5 - x)) & 31)), (u = (8 - ((8 * o.length) % 5)) % 5)); + for (let s = 0; s < u; s++) _ += '='; + return _; + }; + var vI = __webpack_require__(48287).Buffer; + const base64 = (s) => vI.from(s).toString('base64'); + var bI = __webpack_require__(48287).Buffer; + const base64url = (s) => bI.from(s).toString('base64url'); + const _I = new (class EncoderRegistry extends uI { + #t = { + '7bit': _7bit, + '8bit': _8bit, + binary: encoders_binary, + 'quoted-printable': quoted_printable, + base16, + base32, + base64, + base64url + }; + data = { ...this.#t }; + get defaults() { + return { ...this.#t }; + } + })(), + encoderAPI = (s, o) => + 'function' == typeof o ? _I.register(s, o) : null === o ? _I.unregister(s) : _I.get(s); + encoderAPI.getDefaults = () => _I.defaults; + const EI = encoderAPI, + wI = { + 'text/plain': () => 'string', + 'text/css': () => '.selector { border: 1px solid red }', + 'text/csv': () => 'value1,value2,value3', + 'text/html': () => '

      content

      ', + 'text/calendar': () => 'BEGIN:VCALENDAR', + 'text/javascript': () => "console.dir('Hello world!');", + 'text/xml': () => 'John Doe', + 'text/*': () => 'string' + }, + SI = { 'image/*': () => bytes(25).toString('binary') }, + xI = { 'audio/*': () => bytes(25).toString('binary') }, + kI = { 'video/*': () => bytes(25).toString('binary') }, + CI = { + 'application/json': () => '{"key":"value"}', + 'application/ld+json': () => '{"name": "John Doe"}', + 'application/x-httpd-php': () => "Hello World!

      '; ?>", + 'application/rtf': () => String.raw`{\rtf1\adeflang1025\ansi\ansicpg1252\uc1`, + 'application/x-sh': () => 'echo "Hello World!"', + 'application/xhtml+xml': () => '

      content

      ', + 'application/*': () => bytes(25).toString('binary') + }; + const OI = new (class MediaTypeRegistry extends uI { + #t = { ...wI, ...SI, ...xI, ...kI, ...CI }; + data = { ...this.#t }; + get defaults() { + return { ...this.#t }; + } + })(), + mediaTypeAPI = (s, o) => { + if ('function' == typeof o) return OI.register(s, o); + if (null === o) return OI.unregister(s); + const i = s.split(';').at(0), + u = `${i.split('/').at(0)}/*`; + return OI.get(s) || OI.get(i) || OI.get(u); + }; + mediaTypeAPI.getDefaults = () => OI.defaults; + const AI = mediaTypeAPI, + applyStringConstraints = (s, o = {}) => { + const { maxLength: i, minLength: u } = o; + let _ = s; + if ( + (Number.isInteger(i) && i > 0 && (_ = _.slice(0, i)), Number.isInteger(u) && u > 0) + ) { + let s = 0; + for (; _.length < u; ) _ += _[s++ % _.length]; + } + return _; + }, + types_string = (s, { sample: o } = {}) => { + const { contentEncoding: i, contentMediaType: u, contentSchema: _ } = s, + { pattern: w, format: x } = s, + C = EI(i) || Mx(); + let j; + return ( + (j = + 'string' == typeof w + ? applyStringConstraints( + ((s) => { + try { + return new (us())(s).gen(); + } catch { + return 'string'; + } + })(w), + s + ) + : 'string' == typeof x + ? ((s) => { + const { format: o } = s, + i = hI(o); + return 'function' == typeof i ? i(s) : 'string'; + })(s) + : isJSONSchema(_) && 'string' == typeof u && void 0 !== o + ? Array.isArray(o) || 'object' == typeof o + ? JSON.stringify(o) + : applyStringConstraints(String(o), s) + : 'string' == typeof u + ? ((s) => { + const { contentMediaType: o } = s, + i = AI(o); + return 'function' == typeof i ? i(s) : 'string'; + })(s) + : applyStringConstraints('string', s)), + C(j) + ); + }, + applyNumberConstraints = (s, o = {}) => { + const { minimum: i, maximum: u, exclusiveMinimum: _, exclusiveMaximum: w } = o, + { multipleOf: x } = o, + C = Number.isInteger(s) ? 1 : Number.EPSILON; + let j = 'number' == typeof i ? i : null, + L = 'number' == typeof u ? u : null, + B = s; + if ( + ('number' == typeof _ && (j = null !== j ? Math.max(j, _ + C) : _ + C), + 'number' == typeof w && (L = null !== L ? Math.min(L, w - C) : w - C), + (B = (j > L && s) || j || L || B), + 'number' == typeof x && x > 0) + ) { + const s = B % x; + B = 0 === s ? B : B + x - s; + } + return B; + }, + types_number = (s) => { + const { format: o } = s; + let i; + return ( + (i = + 'string' == typeof o + ? ((s) => { + const { format: o } = s, + i = hI(o); + return 'function' == typeof i ? i(s) : 0; + })(s) + : 0), + applyNumberConstraints(i, s) + ); + }, + types_integer = (s) => { + const { format: o } = s; + let i; + return ( + (i = + 'string' == typeof o + ? ((s) => { + const { format: o } = s, + i = hI(o); + if ('function' == typeof i) return i(s); + switch (o) { + case 'int32': + return int32(); + case 'int64': + return int64(); + } + return 0; + })(s) + : 0), + applyNumberConstraints(i, s) + ); + }, + types_boolean = (s) => 'boolean' != typeof s.default || s.default, + jI = new Proxy( + { + array, + object, + string: types_string, + number: types_number, + integer: types_integer, + boolean: types_boolean, + null: () => null + }, + { + get: (s, o) => + 'string' == typeof o && Object.hasOwn(s, o) ? s[o] : () => `Unknown Type: ${o}` + } + ), + II = ['array', 'object', 'number', 'integer', 'string', 'boolean', 'null'], + hasExample = (s) => { + if (!isJSONSchemaObject(s)) return !1; + const { examples: o, example: i, default: u } = s; + return !!(Array.isArray(o) && o.length >= 1) || void 0 !== u || void 0 !== i; + }, + extractExample = (s) => { + if (!isJSONSchemaObject(s)) return null; + const { examples: o, example: i, default: u } = s; + return Array.isArray(o) && o.length >= 1 + ? o.at(0) + : void 0 !== u + ? u + : void 0 !== i + ? i + : void 0; + }, + PI = { + array: [ + 'items', + 'prefixItems', + 'contains', + 'maxContains', + 'minContains', + 'maxItems', + 'minItems', + 'uniqueItems', + 'unevaluatedItems' + ], + object: [ + 'properties', + 'additionalProperties', + 'patternProperties', + 'propertyNames', + 'minProperties', + 'maxProperties', + 'required', + 'dependentSchemas', + 'dependentRequired', + 'unevaluatedProperties' + ], + string: [ + 'pattern', + 'format', + 'minLength', + 'maxLength', + 'contentEncoding', + 'contentMediaType', + 'contentSchema' + ], + integer: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'] + }; + PI.number = PI.integer; + const MI = 'string', + inferTypeFromValue = (s) => + void 0 === s + ? null + : null === s + ? 'null' + : Array.isArray(s) + ? 'array' + : Number.isInteger(s) + ? 'integer' + : typeof s, + foldType = (s) => { + if (Array.isArray(s) && s.length >= 1) { + if (s.includes('array')) return 'array'; + if (s.includes('object')) return 'object'; + { + const o = random_pick(s); + if (II.includes(o)) return o; + } + } + return II.includes(s) ? s : null; + }, + inferType = (s, o = new WeakSet()) => { + if (!isJSONSchemaObject(s)) return MI; + if (o.has(s)) return MI; + o.add(s); + let { type: i, const: u } = s; + if (((i = foldType(i)), 'string' != typeof i)) { + const o = Object.keys(PI); + e: for (let u = 0; u < o.length; u += 1) { + const _ = o[u], + w = PI[_]; + for (let o = 0; o < w.length; o += 1) { + const u = w[o]; + if (Object.hasOwn(s, u)) { + i = _; + break e; + } + } + } + } + if ('string' != typeof i && void 0 !== u) { + const s = inferTypeFromValue(u); + i = 'string' == typeof s ? s : i; + } + if ('string' != typeof i) { + const combineTypes = (i) => { + if (Array.isArray(s[i])) { + const u = s[i].map((s) => inferType(s, o)); + return foldType(u); + } + return null; + }, + u = combineTypes('allOf'), + _ = combineTypes('anyOf'), + w = combineTypes('oneOf'), + x = s.not ? inferType(s.not, o) : null; + (u || _ || w || x) && (i = foldType([u, _, w, x].filter(Boolean))); + } + if ('string' != typeof i && hasExample(s)) { + const o = extractExample(s), + u = inferTypeFromValue(o); + i = 'string' == typeof u ? u : i; + } + return (o.delete(s), i || MI); + }, + type_getType = (s) => inferType(s), + typeCast = (s) => + predicates_isBooleanJSONSchema(s) + ? ((s) => (!1 === s ? { not: {} } : {}))(s) + : isJSONSchemaObject(s) + ? s + : {}, + merge_merge = (s, o, i = {}) => { + if (predicates_isBooleanJSONSchema(s) && !0 === s) return !0; + if (predicates_isBooleanJSONSchema(s) && !1 === s) return !1; + if (predicates_isBooleanJSONSchema(o) && !0 === o) return !0; + if (predicates_isBooleanJSONSchema(o) && !1 === o) return !1; + if (!isJSONSchema(s)) return o; + if (!isJSONSchema(o)) return s; + const u = { ...o, ...s }; + if (o.type && s.type && Array.isArray(o.type) && 'string' == typeof o.type) { + const i = normalizeArray(o.type).concat(s.type); + u.type = Array.from(new Set(i)); + } + if ( + (Array.isArray(o.required) && + Array.isArray(s.required) && + (u.required = [...new Set([...s.required, ...o.required])]), + o.properties && s.properties) + ) { + const _ = new Set([...Object.keys(o.properties), ...Object.keys(s.properties)]); + u.properties = {}; + for (const w of _) { + const _ = o.properties[w] || {}, + x = s.properties[w] || {}; + (_.readOnly && !i.includeReadOnly) || (_.writeOnly && !i.includeWriteOnly) + ? (u.required = (u.required || []).filter((s) => s !== w)) + : (u.properties[w] = merge_merge(x, _, i)); + } + } + return ( + isJSONSchema(o.items) && + isJSONSchema(s.items) && + (u.items = merge_merge(s.items, o.items, i)), + isJSONSchema(o.contains) && + isJSONSchema(s.contains) && + (u.contains = merge_merge(s.contains, o.contains, i)), + isJSONSchema(o.contentSchema) && + isJSONSchema(s.contentSchema) && + (u.contentSchema = merge_merge(s.contentSchema, o.contentSchema, i)), + u + ); + }, + TI = merge_merge, + main_sampleFromSchemaGeneric = (s, o = {}, i = void 0, u = !1) => { + if (null == s && void 0 === i) return; + ('function' == typeof s?.toJS && (s = s.toJS()), (s = typeCast(s))); + let _ = void 0 !== i || hasExample(s); + const w = !_ && Array.isArray(s.oneOf) && s.oneOf.length > 0, + x = !_ && Array.isArray(s.anyOf) && s.anyOf.length > 0; + if (!_ && (w || x)) { + const i = typeCast(random_pick(w ? s.oneOf : s.anyOf)); + (!(s = TI(s, i, o)).xml && i.xml && (s.xml = i.xml), + hasExample(s) && hasExample(i) && (_ = !0)); + } + const C = {}; + let { xml: j, properties: L, additionalProperties: B, items: $, contains: V } = s || {}, + U = type_getType(s), + { includeReadOnly: z, includeWriteOnly: Y } = o; + j = j || {}; + let Z, + { name: ee, prefix: ie, namespace: ae } = j, + le = {}; + if ( + (Object.hasOwn(s, 'type') || (s.type = U), + u && ((ee = ee || 'notagname'), (Z = (ie ? `${ie}:` : '') + ee), ae)) + ) { + C[ie ? `xmlns:${ie}` : 'xmlns'] = ae; + } + u && (le[Z] = []); + const ce = objectify(L); + let pe, + de = 0; + const hasExceededMaxProperties = () => + Number.isInteger(s.maxProperties) && s.maxProperties > 0 && de >= s.maxProperties, + canAddProperty = (o) => + !(Number.isInteger(s.maxProperties) && s.maxProperties > 0) || + (!hasExceededMaxProperties() && + (!((o) => + !Array.isArray(s.required) || + 0 === s.required.length || + !s.required.includes(o))(o) || + s.maxProperties - + de - + (() => { + if (!Array.isArray(s.required) || 0 === s.required.length) return 0; + let o = 0; + return ( + u + ? s.required.forEach((s) => (o += void 0 === le[s] ? 0 : 1)) + : s.required.forEach((s) => { + o += void 0 === le[Z]?.find((o) => void 0 !== o[s]) ? 0 : 1; + }), + s.required.length - o + ); + })() > + 0)); + if ( + ((pe = u + ? (i, _ = void 0) => { + if (s && ce[i]) { + if (((ce[i].xml = ce[i].xml || {}), ce[i].xml.attribute)) { + const s = Array.isArray(ce[i].enum) ? random_pick(ce[i].enum) : void 0; + if (hasExample(ce[i])) C[ce[i].xml.name || i] = extractExample(ce[i]); + else if (void 0 !== s) C[ce[i].xml.name || i] = s; + else { + const s = typeCast(ce[i]), + o = type_getType(s), + u = ce[i].xml.name || i; + C[u] = jI[o](s); + } + return; + } + ce[i].xml.name = ce[i].xml.name || i; + } else ce[i] || !1 === B || (ce[i] = { xml: { name: i } }); + let w = main_sampleFromSchemaGeneric(ce[i], o, _, u); + canAddProperty(i) && + (de++, Array.isArray(w) ? (le[Z] = le[Z].concat(w)) : le[Z].push(w)); + } + : (i, _) => { + if (canAddProperty(i)) { + if ( + cI()(s.discriminator?.mapping) && + s.discriminator.propertyName === i && + 'string' == typeof s.$$ref + ) { + for (const o in s.discriminator.mapping) + if (-1 !== s.$$ref.search(s.discriminator.mapping[o])) { + le[i] = o; + break; + } + } else le[i] = main_sampleFromSchemaGeneric(ce[i], o, _, u); + de++; + } + }), + _) + ) { + let _; + if (((_ = void 0 !== i ? i : extractExample(s)), !u)) { + if ('number' == typeof _ && 'string' === U) return `${_}`; + if ('string' != typeof _ || 'string' === U) return _; + try { + return JSON.parse(_); + } catch { + return _; + } + } + if ('array' === U) { + if (!Array.isArray(_)) { + if ('string' == typeof _) return _; + _ = [_]; + } + let i = []; + return ( + isJSONSchemaObject($) && + (($.xml = $.xml || j || {}), + ($.xml.name = $.xml.name || j.name), + (i = _.map((s) => main_sampleFromSchemaGeneric($, o, s, u)))), + isJSONSchemaObject(V) && + ((V.xml = V.xml || j || {}), + (V.xml.name = V.xml.name || j.name), + (i = [main_sampleFromSchemaGeneric(V, o, void 0, u), ...i])), + (i = jI.array(s, { sample: i })), + j.wrapped ? ((le[Z] = i), hs()(C) || le[Z].push({ _attr: C })) : (le = i), + le + ); + } + if ('object' === U) { + if ('string' == typeof _) return _; + for (const s in _) + Object.hasOwn(_, s) && + ((ce[s]?.readOnly && !z) || + (ce[s]?.writeOnly && !Y) || + (ce[s]?.xml?.attribute ? (C[ce[s].xml.name || s] = _[s]) : pe(s, _[s]))); + return (hs()(C) || le[Z].push({ _attr: C }), le); + } + return ((le[Z] = hs()(C) ? _ : [{ _attr: C }, _]), le); + } + if ('array' === U) { + let i = []; + if (isJSONSchemaObject(V)) + if ( + (u && ((V.xml = V.xml || s.xml || {}), (V.xml.name = V.xml.name || j.name)), + Array.isArray(V.anyOf)) + ) { + const { anyOf: s, ..._ } = $; + i.push( + ...V.anyOf.map((s) => main_sampleFromSchemaGeneric(TI(s, _, o), o, void 0, u)) + ); + } else if (Array.isArray(V.oneOf)) { + const { oneOf: s, ..._ } = $; + i.push( + ...V.oneOf.map((s) => main_sampleFromSchemaGeneric(TI(s, _, o), o, void 0, u)) + ); + } else { + if (!(!u || (u && j.wrapped))) + return main_sampleFromSchemaGeneric(V, o, void 0, u); + i.push(main_sampleFromSchemaGeneric(V, o, void 0, u)); + } + if (isJSONSchemaObject($)) + if ( + (u && (($.xml = $.xml || s.xml || {}), ($.xml.name = $.xml.name || j.name)), + Array.isArray($.anyOf)) + ) { + const { anyOf: s, ..._ } = $; + i.push( + ...$.anyOf.map((s) => main_sampleFromSchemaGeneric(TI(s, _, o), o, void 0, u)) + ); + } else if (Array.isArray($.oneOf)) { + const { oneOf: s, ..._ } = $; + i.push( + ...$.oneOf.map((s) => main_sampleFromSchemaGeneric(TI(s, _, o), o, void 0, u)) + ); + } else { + if (!(!u || (u && j.wrapped))) + return main_sampleFromSchemaGeneric($, o, void 0, u); + i.push(main_sampleFromSchemaGeneric($, o, void 0, u)); + } + return ( + (i = jI.array(s, { sample: i })), + u && j.wrapped ? ((le[Z] = i), hs()(C) || le[Z].push({ _attr: C }), le) : i + ); + } + if ('object' === U) { + for (let s in ce) + Object.hasOwn(ce, s) && + (ce[s]?.deprecated || + (ce[s]?.readOnly && !z) || + (ce[s]?.writeOnly && !Y) || + pe(s)); + if ((u && C && le[Z].push({ _attr: C }), hasExceededMaxProperties())) return le; + if (predicates_isBooleanJSONSchema(B) && B) + (u + ? le[Z].push({ additionalProp: 'Anything can be here' }) + : (le.additionalProp1 = {}), + de++); + else if (isJSONSchemaObject(B)) { + const i = B, + _ = main_sampleFromSchemaGeneric(i, o, void 0, u); + if (u && 'string' == typeof i?.xml?.name && 'notagname' !== i?.xml?.name) + le[Z].push(_); + else { + const o = + Number.isInteger(s.minProperties) && s.minProperties > 0 && de < s.minProperties + ? s.minProperties - de + : 3; + for (let s = 1; s <= o; s++) { + if (hasExceededMaxProperties()) return le; + if (u) { + const o = {}; + ((o['additionalProp' + s] = _.notagname), le[Z].push(o)); + } else le['additionalProp' + s] = _; + de++; + } + } + } + return le; + } + let fe; + if (void 0 !== s.const) fe = s.const; + else if (s && Array.isArray(s.enum)) fe = random_pick(normalizeArray(s.enum)); + else { + const i = isJSONSchemaObject(s.contentSchema) + ? main_sampleFromSchemaGeneric(s.contentSchema, o, void 0, u) + : void 0; + fe = jI[U](s, { sample: i }); + } + return u ? ((le[Z] = hs()(C) ? fe : [{ _attr: C }, fe]), le) : fe; + }, + main_createXMLExample = (s, o, i) => { + const u = main_sampleFromSchemaGeneric(s, o, i, !0); + if (u) return 'string' == typeof u ? u : ls()(u, { declaration: !0, indent: '\t' }); + }, + main_sampleFromSchema = (s, o, i) => main_sampleFromSchemaGeneric(s, o, i, !1), + main_resolver = (s, o, i) => [s, JSON.stringify(o), JSON.stringify(i)], + NI = utils_memoizeN(main_createXMLExample, main_resolver), + RI = utils_memoizeN(main_sampleFromSchema, main_resolver); + const DI = new (class OptionRegistry extends uI { + #t = {}; + data = { ...this.#t }; + get defaults() { + return { ...this.#t }; + } + })(), + api_optionAPI = (s, o) => (void 0 !== o && DI.register(s, o), DI.get(s)), + LI = [{ when: /json/, shouldStringifyTypes: ['string'] }], + BI = ['object'], + fn_get_json_sample_schema = (s) => (o, i, u, _) => { + const { fn: w } = s(), + x = w.jsonSchema202012.memoizedSampleFromSchema(o, i, _), + C = typeof x, + j = LI.reduce((s, o) => (o.when.test(u) ? [...s, ...o.shouldStringifyTypes] : s), BI); + return mt()(j, (s) => s === C) ? JSON.stringify(x, null, 2) : x; + }, + fn_get_yaml_sample_schema = (s) => (o, i, u, _) => { + const { fn: w } = s(), + x = w.jsonSchema202012.getJsonSampleSchema(o, i, u, _); + let C; + try { + ((C = mn.dump(mn.load(x), { lineWidth: -1 }, { schema: nn })), + '\n' === C[C.length - 1] && (C = C.slice(0, C.length - 1))); + } catch (s) { + return (console.error(s), 'error: could not generate yaml example'); + } + return C.replace(/\t/g, ' '); + }, + fn_get_xml_sample_schema = (s) => (o, i, u) => { + const { fn: _ } = s(); + if ((o && !o.xml && (o.xml = {}), o && !o.xml.name)) { + if (!o.$$ref && (o.type || o.items || o.properties || o.additionalProperties)) + return '\n\x3c!-- XML example cannot be generated; root element name is undefined --\x3e'; + if (o.$$ref) { + let s = o.$$ref.match(/\S*\/(\S+)$/); + o.xml.name = s[1]; + } + } + return _.jsonSchema202012.memoizedCreateXMLExample(o, i, u); + }, + fn_get_sample_schema = + (s) => + (o, i = '', u = {}, _ = void 0) => { + const { fn: w } = s(); + return ( + 'function' == typeof o?.toJS && (o = o.toJS()), + 'function' == typeof _?.toJS && (_ = _.toJS()), + /xml/.test(i) + ? w.jsonSchema202012.getXmlSampleSchema(o, u, _) + : /(yaml|yml)/.test(i) + ? w.jsonSchema202012.getYamlSampleSchema(o, u, i, _) + : w.jsonSchema202012.getJsonSampleSchema(o, u, i, _) + ); + }, + json_schema_2020_12_samples = ({ getSystem: s }) => { + const o = fn_get_json_sample_schema(s), + i = fn_get_yaml_sample_schema(s), + u = fn_get_xml_sample_schema(s), + _ = fn_get_sample_schema(s); + return { + fn: { + jsonSchema202012: { + sampleFromSchema: main_sampleFromSchema, + sampleFromSchemaGeneric: main_sampleFromSchemaGeneric, + sampleOptionAPI: api_optionAPI, + sampleEncoderAPI: EI, + sampleFormatAPI: hI, + sampleMediaTypeAPI: AI, + createXMLExample: main_createXMLExample, + memoizedSampleFromSchema: RI, + memoizedCreateXMLExample: NI, + getJsonSampleSchema: o, + getYamlSampleSchema: i, + getXmlSampleSchema: u, + getSampleSchema: _, + mergeJsonSchema: TI + } + } + }; + }; + function PresetApis() { + return [base, oas3, json_schema_2020_12, json_schema_2020_12_samples, oas31]; + } + const inline_plugin = (s) => () => ({ fn: s.fn, components: s.components }), + factorization_system = (s) => { + const o = We()( + { + layout: { layout: s.layout, filter: s.filter }, + spec: { spec: '', url: s.url }, + requestSnippets: s.requestSnippets + }, + s.initialState + ); + if (s.initialState) + for (const [i, u] of Object.entries(s.initialState)) void 0 === u && delete o[i]; + return { system: { configs: s.configs }, plugins: s.presets, state: o }; + }, + sources_query = () => (s) => { + const o = s.queryConfigEnabled + ? (() => { + const s = new URLSearchParams(at.location.search); + return Object.fromEntries(s); + })() + : {}; + return Object.entries(o).reduce( + (s, [o, i]) => ( + 'config' === o + ? (s.configUrl = i) + : 'urls.primaryName' === o + ? (s[o] = i) + : (s = ao()(s, o, i)), + s + ), + {} + ); + }, + sources_url = + ({ url: s, system: o }) => + async (i) => { + if (!s) return {}; + if ('function' != typeof o.configsActions?.getConfigByUrl) return {}; + const u = (() => { + const s = {}; + return ( + (s.promise = new Promise((o, i) => { + ((s.resolve = o), (s.reject = i)); + })), + s + ); + })(); + return ( + o.configsActions.getConfigByUrl( + { + url: s, + loadRemoteConfig: !0, + requestInterceptor: i.requestInterceptor, + responseInterceptor: i.responseInterceptor + }, + (s) => { + u.resolve(s); + } + ), + u.promise + ); + }, + runtime = () => () => { + const s = {}; + return ( + globalThis.location && + (s.oauth2RedirectUrl = `${globalThis.location.protocol}//${globalThis.location.host}${globalThis.location.pathname.substring(0, globalThis.location.pathname.lastIndexOf('/'))}/oauth2-redirect.html`), + s + ); + }, + FI = Object.freeze({ + dom_id: null, + domNode: null, + spec: {}, + url: '', + urls: null, + configUrl: null, + layout: 'BaseLayout', + docExpansion: 'list', + maxDisplayedTags: -1, + filter: !1, + validatorUrl: 'https://validator.swagger.io/validator', + oauth2RedirectUrl: void 0, + persistAuthorization: !1, + configs: {}, + displayOperationId: !1, + displayRequestDuration: !1, + deepLinking: !1, + tryItOutEnabled: !1, + requestInterceptor: (s) => ((s.curlOptions = []), s), + responseInterceptor: (s) => s, + showMutatedRequest: !0, + defaultModelRendering: 'example', + defaultModelExpandDepth: 1, + defaultModelsExpandDepth: 1, + showExtensions: !1, + showCommonExtensions: !1, + withCredentials: !1, + requestSnippetsEnabled: !1, + requestSnippets: { + generators: { + curl_bash: { title: 'cURL (bash)', syntax: 'bash' }, + curl_powershell: { title: 'cURL (PowerShell)', syntax: 'powershell' }, + curl_cmd: { title: 'cURL (CMD)', syntax: 'bash' } + }, + defaultExpanded: !0, + languages: null + }, + supportedSubmitMethods: [ + 'get', + 'put', + 'post', + 'delete', + 'options', + 'head', + 'patch', + 'trace' + ], + queryConfigEnabled: !1, + presets: [PresetApis], + plugins: [], + initialState: {}, + fn: {}, + components: {}, + syntaxHighlight: { activated: !0, theme: 'agate' }, + operationsSorter: null, + tagsSorter: null, + onComplete: null, + modelPropertyMacro: null, + parameterMacro: null + }); + var qI = __webpack_require__(61448), + $I = __webpack_require__.n(qI), + VI = __webpack_require__(77731), + UI = __webpack_require__.n(VI); + const type_casters_array = (s, o = []) => (Array.isArray(s) ? s : o), + type_casters_boolean = (s, o = !1) => + !0 === s || + 'true' === s || + 1 === s || + '1' === s || + (!1 !== s && 'false' !== s && 0 !== s && '0' !== s && o), + dom_node = (s) => (null === s || 'null' === s ? null : s), + type_casters_filter = (s) => { + const o = String(s); + return type_casters_boolean(s, o); + }, + type_casters_function = (s, o) => ('function' == typeof s ? s : o), + nullable_array = (s) => (Array.isArray(s) ? s : null), + nullable_function = (s) => ('function' == typeof s ? s : null), + nullable_string = (s) => (null === s || 'null' === s ? null : String(s)), + type_casters_number = (s, o = -1) => { + const i = parseInt(s, 10); + return Number.isNaN(i) ? o : i; + }, + type_casters_object = (s, o = {}) => (cI()(s) ? s : o), + sorter = (s) => ('function' == typeof s || 'string' == typeof s ? s : null), + type_casters_string = (s) => String(s), + syntax_highlight = (s, o) => + cI()(s) ? s : !1 === s || 'false' === s || 0 === s || '0' === s ? { activated: !1 } : o, + undefined_string = (s) => (void 0 === s || 'undefined' === s ? void 0 : String(s)), + zI = { + components: { typeCaster: type_casters_object }, + configs: { typeCaster: type_casters_object }, + configUrl: { typeCaster: nullable_string }, + deepLinking: { typeCaster: type_casters_boolean, defaultValue: FI.deepLinking }, + defaultModelExpandDepth: { + typeCaster: type_casters_number, + defaultValue: FI.defaultModelExpandDepth + }, + defaultModelRendering: { typeCaster: type_casters_string }, + defaultModelsExpandDepth: { + typeCaster: type_casters_number, + defaultValue: FI.defaultModelsExpandDepth + }, + displayOperationId: { + typeCaster: type_casters_boolean, + defaultValue: FI.displayOperationId + }, + displayRequestDuration: { + typeCaster: type_casters_boolean, + defaultValue: FI.displayRequestDuration + }, + docExpansion: { typeCaster: type_casters_string }, + dom_id: { typeCaster: nullable_string }, + domNode: { typeCaster: dom_node }, + filter: { typeCaster: type_casters_filter }, + fn: { typeCaster: type_casters_object }, + initialState: { typeCaster: type_casters_object }, + layout: { typeCaster: type_casters_string }, + maxDisplayedTags: { + typeCaster: type_casters_number, + defaultValue: FI.maxDisplayedTags + }, + modelPropertyMacro: { typeCaster: nullable_function }, + oauth2RedirectUrl: { typeCaster: undefined_string }, + onComplete: { typeCaster: nullable_function }, + operationsSorter: { typeCaster: sorter }, + paramaterMacro: { typeCaster: nullable_function }, + persistAuthorization: { + typeCaster: type_casters_boolean, + defaultValue: FI.persistAuthorization + }, + plugins: { typeCaster: type_casters_array, defaultValue: FI.plugins }, + presets: { typeCaster: type_casters_array, defaultValue: FI.presets }, + requestInterceptor: { + typeCaster: type_casters_function, + defaultValue: FI.requestInterceptor + }, + requestSnippets: { typeCaster: type_casters_object, defaultValue: FI.requestSnippets }, + requestSnippetsEnabled: { + typeCaster: type_casters_boolean, + defaultValue: FI.requestSnippetsEnabled + }, + responseInterceptor: { + typeCaster: type_casters_function, + defaultValue: FI.responseInterceptor + }, + showCommonExtensions: { + typeCaster: type_casters_boolean, + defaultValue: FI.showCommonExtensions + }, + showExtensions: { typeCaster: type_casters_boolean, defaultValue: FI.showExtensions }, + showMutatedRequest: { + typeCaster: type_casters_boolean, + defaultValue: FI.showMutatedRequest + }, + spec: { typeCaster: type_casters_object, defaultValue: FI.spec }, + supportedSubmitMethods: { + typeCaster: type_casters_array, + defaultValue: FI.supportedSubmitMethods + }, + syntaxHighlight: { typeCaster: syntax_highlight, defaultValue: FI.syntaxHighlight }, + 'syntaxHighlight.activated': { + typeCaster: type_casters_boolean, + defaultValue: FI.syntaxHighlight.activated + }, + 'syntaxHighlight.theme': { typeCaster: type_casters_string }, + tagsSorter: { typeCaster: sorter }, + tryItOutEnabled: { typeCaster: type_casters_boolean, defaultValue: FI.tryItOutEnabled }, + url: { typeCaster: type_casters_string }, + urls: { typeCaster: nullable_array }, + 'urls.primaryName': { typeCaster: type_casters_string }, + validatorUrl: { typeCaster: nullable_string }, + withCredentials: { typeCaster: type_casters_boolean, defaultValue: FI.withCredentials } + }, + type_cast = (s) => + Object.entries(zI).reduce( + (s, [o, { typeCaster: i, defaultValue: u }]) => { + if ($I()(s, o)) { + const _ = i(jn()(s, o), u); + s = UI()(o, _, s); + } + return s; + }, + { ...s } + ), + config_merge = (s, ...o) => { + let i = Symbol.for('domNode'), + u = Symbol.for('primaryName'); + const _ = []; + for (const s of o) { + const o = { ...s }; + (Object.hasOwn(o, 'domNode') && ((i = o.domNode), delete o.domNode), + Object.hasOwn(o, 'urls.primaryName') + ? ((u = o['urls.primaryName']), delete o['urls.primaryName']) + : Array.isArray(o.urls) && + Object.hasOwn(o.urls, 'primaryName') && + ((u = o.urls.primaryName), delete o.urls.primaryName), + _.push(o)); + } + const w = We()(s, ..._); + return ( + i !== Symbol.for('domNode') && (w.domNode = i), + u !== Symbol.for('primaryName') && Array.isArray(w.urls) && (w.urls.primaryName = u), + type_cast(w) + ); + }; + function SwaggerUI(s) { + const o = sources_query()(s), + i = runtime()(), + u = SwaggerUI.config.merge({}, SwaggerUI.config.defaults, i, s, o), + _ = factorization_system(u), + w = inline_plugin(u), + x = new Store(_); + x.register([u.plugins, w]); + const C = x.getSystem(), + persistConfigs = (s) => { + (x.setConfigs(s), C.configsActions.loaded()); + }, + updateSpec = (s) => { + !o.url && 'object' == typeof s.spec && Object.keys(s.spec).length > 0 + ? (C.specActions.updateUrl(''), + C.specActions.updateLoadingStatus('success'), + C.specActions.updateSpec(JSON.stringify(s.spec))) + : 'function' == typeof C.specActions.download && + s.url && + !s.urls && + (C.specActions.updateUrl(s.url), C.specActions.download(s.url)); + }, + render = (s) => { + if (s.domNode) C.render(s.domNode, 'App'); + else if (s.dom_id) { + const o = document.querySelector(s.dom_id); + C.render(o, 'App'); + } else + null === s.dom_id || + null === s.domNode || + console.error('Skipped rendering: no `dom_id` or `domNode` was specified'); + }; + return u.configUrl + ? ((async () => { + const { configUrl: s } = u, + i = await sources_url({ url: s, system: C })(u), + _ = SwaggerUI.config.merge({}, u, i, o); + (persistConfigs(_), null !== i && updateSpec(_), render(_)); + })(), + C) + : (persistConfigs(u), updateSpec(u), render(u), C); + } + ((SwaggerUI.System = Store), + (SwaggerUI.config = { + defaults: FI, + merge: config_merge, + typeCast: type_cast, + typeCastMappings: zI + }), + (SwaggerUI.presets = { base, apis: PresetApis }), + (SwaggerUI.plugins = { + Auth: auth, + Configs: configsPlugin, + DeepLining: deep_linking, + Err: err, + Filter: filter, + Icons: icons, + JSONSchema5: json_schema_5, + JSONSchema5Samples: json_schema_5_samples, + JSONSchema202012: json_schema_2020_12, + JSONSchema202012Samples: json_schema_2020_12_samples, + Layout: plugins_layout, + Logs: logs, + OpenAPI30: oas3, + OpenAPI31: oas3, + OnComplete: on_complete, + RequestSnippets: plugins_request_snippets, + Spec: plugins_spec, + SwaggerClient: swagger_client, + Util: util, + View: view, + ViewLegacy: view_legacy, + DownloadUrl: downloadUrlPlugin, + SyntaxHighlighting: syntax_highlighting, + Versions: versions, + SafeRender: safe_render + })); + const WI = SwaggerUI; + })(), + (_ = _.default) + ); + })() +); diff --git a/backend/open_webui/static/swagger-ui/swagger-ui.css b/backend/open_webui/static/swagger-ui/swagger-ui.css new file mode 100644 index 0000000000000000000000000000000000000000..4efa5efeecbce3be6dc389e16648e77b48b6ec75 --- /dev/null +++ b/backend/open_webui/static/swagger-ui/swagger-ui.css @@ -0,0 +1,9310 @@ +.swagger-ui { + color: #3b4151; + font-family: sans-serif; /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */ +} +.swagger-ui html { + line-height: 1.15; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; +} +.swagger-ui body { + margin: 0; +} +.swagger-ui article, +.swagger-ui aside, +.swagger-ui footer, +.swagger-ui header, +.swagger-ui nav, +.swagger-ui section { + display: block; +} +.swagger-ui h1 { + font-size: 2em; + margin: 0.67em 0; +} +.swagger-ui figcaption, +.swagger-ui figure, +.swagger-ui main { + display: block; +} +.swagger-ui figure { + margin: 1em 40px; +} +.swagger-ui hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} +.swagger-ui pre { + font-family: monospace, monospace; + font-size: 1em; +} +.swagger-ui a { + background-color: transparent; + -webkit-text-decoration-skip: objects; +} +.swagger-ui abbr[title] { + border-bottom: none; + text-decoration: underline; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} +.swagger-ui b, +.swagger-ui strong { + font-weight: inherit; + font-weight: bolder; +} +.swagger-ui code, +.swagger-ui kbd, +.swagger-ui samp { + font-family: monospace, monospace; + font-size: 1em; +} +.swagger-ui dfn { + font-style: italic; +} +.swagger-ui mark { + background-color: #ff0; + color: #000; +} +.swagger-ui small { + font-size: 80%; +} +.swagger-ui sub, +.swagger-ui sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +.swagger-ui sub { + bottom: -0.25em; +} +.swagger-ui sup { + top: -0.5em; +} +.swagger-ui audio, +.swagger-ui video { + display: inline-block; +} +.swagger-ui audio:not([controls]) { + display: none; + height: 0; +} +.swagger-ui img { + border-style: none; +} +.swagger-ui svg:not(:root) { + overflow: hidden; +} +.swagger-ui button, +.swagger-ui input, +.swagger-ui optgroup, +.swagger-ui select, +.swagger-ui textarea { + font-family: sans-serif; + font-size: 100%; + line-height: 1.15; + margin: 0; +} +.swagger-ui button, +.swagger-ui input { + overflow: visible; +} +.swagger-ui button, +.swagger-ui select { + text-transform: none; +} +.swagger-ui [type='reset'], +.swagger-ui [type='submit'], +.swagger-ui button, +.swagger-ui html [type='button'] { + -webkit-appearance: button; +} +.swagger-ui [type='button']::-moz-focus-inner, +.swagger-ui [type='reset']::-moz-focus-inner, +.swagger-ui [type='submit']::-moz-focus-inner, +.swagger-ui button::-moz-focus-inner { + border-style: none; + padding: 0; +} +.swagger-ui [type='button']:-moz-focusring, +.swagger-ui [type='reset']:-moz-focusring, +.swagger-ui [type='submit']:-moz-focusring, +.swagger-ui button:-moz-focusring { + outline: 1px dotted ButtonText; +} +.swagger-ui fieldset { + padding: 0.35em 0.75em 0.625em; +} +.swagger-ui legend { + box-sizing: border-box; + color: inherit; + display: table; + max-width: 100%; + padding: 0; + white-space: normal; +} +.swagger-ui progress { + display: inline-block; + vertical-align: baseline; +} +.swagger-ui textarea { + overflow: auto; +} +.swagger-ui [type='checkbox'], +.swagger-ui [type='radio'] { + box-sizing: border-box; + padding: 0; +} +.swagger-ui [type='number']::-webkit-inner-spin-button, +.swagger-ui [type='number']::-webkit-outer-spin-button { + height: auto; +} +.swagger-ui [type='search'] { + -webkit-appearance: textfield; + outline-offset: -2px; +} +.swagger-ui [type='search']::-webkit-search-cancel-button, +.swagger-ui [type='search']::-webkit-search-decoration { + -webkit-appearance: none; +} +.swagger-ui ::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; +} +.swagger-ui details, +.swagger-ui menu { + display: block; +} +.swagger-ui summary { + display: list-item; +} +.swagger-ui canvas { + display: inline-block; +} +.swagger-ui [hidden], +.swagger-ui template { + display: none; +} +.swagger-ui .debug * { + outline: 1px solid gold; +} +.swagger-ui .debug-white * { + outline: 1px solid #fff; +} +.swagger-ui .debug-black * { + outline: 1px solid #000; +} +.swagger-ui .debug-grid { + background: transparent + url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTRDOTY4N0U2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTRDOTY4N0Q2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3NjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3NzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsBS+GMAAAAjSURBVHjaYvz//z8DLsD4gcGXiYEAGBIKGBne//fFpwAgwAB98AaF2pjlUQAAAABJRU5ErkJggg==) + repeat 0 0; +} +.swagger-ui .debug-grid-16 { + background: transparent + url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODYyRjhERDU2N0YyMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODYyRjhERDQ2N0YyMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3QTY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3QjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvCS01IAAABMSURBVHjaYmR4/5+BFPBfAMFm/MBgx8RAGWCn1AAmSg34Q6kBDKMGMDCwICeMIemF/5QawEipAWwUhwEjMDvbAWlWkvVBwu8vQIABAEwBCph8U6c0AAAAAElFTkSuQmCC) + repeat 0 0; +} +.swagger-ui .debug-grid-8-solid { + background: #fff + url(data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAAAAD/4QMxaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzExMSA3OS4xNTgzMjUsIDIwMTUvMDkvMTAtMDE6MTA6MjAgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE1IChNYWNpbnRvc2gpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkIxMjI0OTczNjdCMzExRTZCMkJDRTI0MDgxMDAyMTcxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkIxMjI0OTc0NjdCMzExRTZCMkJDRTI0MDgxMDAyMTcxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QjEyMjQ5NzE2N0IzMTFFNkIyQkNFMjQwODEwMDIxNzEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QjEyMjQ5NzI2N0IzMTFFNkIyQkNFMjQwODEwMDIxNzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAbGhopHSlBJiZBQi8vL0JHPz4+P0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHAR0pKTQmND8oKD9HPzU/R0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0f/wAARCAAIAAgDASIAAhEBAxEB/8QAWQABAQAAAAAAAAAAAAAAAAAAAAYBAQEAAAAAAAAAAAAAAAAAAAIEEAEBAAMBAAAAAAAAAAAAAAABADECA0ERAAEDBQAAAAAAAAAAAAAAAAARITFBUWESIv/aAAwDAQACEQMRAD8AoOnTV1QTD7JJshP3vSM3P//Z) + repeat 0 0; +} +.swagger-ui .debug-grid-16-solid { + background: #fff + url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NzY3MkJEN0U2N0M1MTFFNkIyQkNFMjQwODEwMDIxNzEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NzY3MkJEN0Y2N0M1MTFFNkIyQkNFMjQwODEwMDIxNzEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3QzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3RDY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pve6J3kAAAAzSURBVHjaYvz//z8D0UDsMwMjSRoYP5Gq4SPNbRjVMEQ1fCRDg+in/6+J1AJUxsgAEGAA31BAJMS0GYEAAAAASUVORK5CYII=) + repeat 0 0; +} +.swagger-ui .border-box, +.swagger-ui a, +.swagger-ui article, +.swagger-ui body, +.swagger-ui code, +.swagger-ui dd, +.swagger-ui div, +.swagger-ui dl, +.swagger-ui dt, +.swagger-ui fieldset, +.swagger-ui footer, +.swagger-ui form, +.swagger-ui h1, +.swagger-ui h2, +.swagger-ui h3, +.swagger-ui h4, +.swagger-ui h5, +.swagger-ui h6, +.swagger-ui header, +.swagger-ui html, +.swagger-ui input[type='email'], +.swagger-ui input[type='number'], +.swagger-ui input[type='password'], +.swagger-ui input[type='tel'], +.swagger-ui input[type='text'], +.swagger-ui input[type='url'], +.swagger-ui legend, +.swagger-ui li, +.swagger-ui main, +.swagger-ui ol, +.swagger-ui p, +.swagger-ui pre, +.swagger-ui section, +.swagger-ui table, +.swagger-ui td, +.swagger-ui textarea, +.swagger-ui th, +.swagger-ui tr, +.swagger-ui ul { + box-sizing: border-box; +} +.swagger-ui .aspect-ratio { + height: 0; + position: relative; +} +.swagger-ui .aspect-ratio--16x9 { + padding-bottom: 56.25%; +} +.swagger-ui .aspect-ratio--9x16 { + padding-bottom: 177.77%; +} +.swagger-ui .aspect-ratio--4x3 { + padding-bottom: 75%; +} +.swagger-ui .aspect-ratio--3x4 { + padding-bottom: 133.33%; +} +.swagger-ui .aspect-ratio--6x4 { + padding-bottom: 66.6%; +} +.swagger-ui .aspect-ratio--4x6 { + padding-bottom: 150%; +} +.swagger-ui .aspect-ratio--8x5 { + padding-bottom: 62.5%; +} +.swagger-ui .aspect-ratio--5x8 { + padding-bottom: 160%; +} +.swagger-ui .aspect-ratio--7x5 { + padding-bottom: 71.42%; +} +.swagger-ui .aspect-ratio--5x7 { + padding-bottom: 140%; +} +.swagger-ui .aspect-ratio--1x1 { + padding-bottom: 100%; +} +.swagger-ui .aspect-ratio--object { + bottom: 0; + height: 100%; + left: 0; + position: absolute; + right: 0; + top: 0; + width: 100%; + z-index: 100; +} +@media screen and (min-width: 30em) { + .swagger-ui .aspect-ratio-ns { + height: 0; + position: relative; + } + .swagger-ui .aspect-ratio--16x9-ns { + padding-bottom: 56.25%; + } + .swagger-ui .aspect-ratio--9x16-ns { + padding-bottom: 177.77%; + } + .swagger-ui .aspect-ratio--4x3-ns { + padding-bottom: 75%; + } + .swagger-ui .aspect-ratio--3x4-ns { + padding-bottom: 133.33%; + } + .swagger-ui .aspect-ratio--6x4-ns { + padding-bottom: 66.6%; + } + .swagger-ui .aspect-ratio--4x6-ns { + padding-bottom: 150%; + } + .swagger-ui .aspect-ratio--8x5-ns { + padding-bottom: 62.5%; + } + .swagger-ui .aspect-ratio--5x8-ns { + padding-bottom: 160%; + } + .swagger-ui .aspect-ratio--7x5-ns { + padding-bottom: 71.42%; + } + .swagger-ui .aspect-ratio--5x7-ns { + padding-bottom: 140%; + } + .swagger-ui .aspect-ratio--1x1-ns { + padding-bottom: 100%; + } + .swagger-ui .aspect-ratio--object-ns { + bottom: 0; + height: 100%; + left: 0; + position: absolute; + right: 0; + top: 0; + width: 100%; + z-index: 100; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .aspect-ratio-m { + height: 0; + position: relative; + } + .swagger-ui .aspect-ratio--16x9-m { + padding-bottom: 56.25%; + } + .swagger-ui .aspect-ratio--9x16-m { + padding-bottom: 177.77%; + } + .swagger-ui .aspect-ratio--4x3-m { + padding-bottom: 75%; + } + .swagger-ui .aspect-ratio--3x4-m { + padding-bottom: 133.33%; + } + .swagger-ui .aspect-ratio--6x4-m { + padding-bottom: 66.6%; + } + .swagger-ui .aspect-ratio--4x6-m { + padding-bottom: 150%; + } + .swagger-ui .aspect-ratio--8x5-m { + padding-bottom: 62.5%; + } + .swagger-ui .aspect-ratio--5x8-m { + padding-bottom: 160%; + } + .swagger-ui .aspect-ratio--7x5-m { + padding-bottom: 71.42%; + } + .swagger-ui .aspect-ratio--5x7-m { + padding-bottom: 140%; + } + .swagger-ui .aspect-ratio--1x1-m { + padding-bottom: 100%; + } + .swagger-ui .aspect-ratio--object-m { + bottom: 0; + height: 100%; + left: 0; + position: absolute; + right: 0; + top: 0; + width: 100%; + z-index: 100; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .aspect-ratio-l { + height: 0; + position: relative; + } + .swagger-ui .aspect-ratio--16x9-l { + padding-bottom: 56.25%; + } + .swagger-ui .aspect-ratio--9x16-l { + padding-bottom: 177.77%; + } + .swagger-ui .aspect-ratio--4x3-l { + padding-bottom: 75%; + } + .swagger-ui .aspect-ratio--3x4-l { + padding-bottom: 133.33%; + } + .swagger-ui .aspect-ratio--6x4-l { + padding-bottom: 66.6%; + } + .swagger-ui .aspect-ratio--4x6-l { + padding-bottom: 150%; + } + .swagger-ui .aspect-ratio--8x5-l { + padding-bottom: 62.5%; + } + .swagger-ui .aspect-ratio--5x8-l { + padding-bottom: 160%; + } + .swagger-ui .aspect-ratio--7x5-l { + padding-bottom: 71.42%; + } + .swagger-ui .aspect-ratio--5x7-l { + padding-bottom: 140%; + } + .swagger-ui .aspect-ratio--1x1-l { + padding-bottom: 100%; + } + .swagger-ui .aspect-ratio--object-l { + bottom: 0; + height: 100%; + left: 0; + position: absolute; + right: 0; + top: 0; + width: 100%; + z-index: 100; + } +} +.swagger-ui img { + max-width: 100%; +} +.swagger-ui .cover { + background-size: cover !important; +} +.swagger-ui .contain { + background-size: contain !important; +} +@media screen and (min-width: 30em) { + .swagger-ui .cover-ns { + background-size: cover !important; + } + .swagger-ui .contain-ns { + background-size: contain !important; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .cover-m { + background-size: cover !important; + } + .swagger-ui .contain-m { + background-size: contain !important; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .cover-l { + background-size: cover !important; + } + .swagger-ui .contain-l { + background-size: contain !important; + } +} +.swagger-ui .bg-center { + background-position: 50%; + background-repeat: no-repeat; +} +.swagger-ui .bg-top { + background-position: top; + background-repeat: no-repeat; +} +.swagger-ui .bg-right { + background-position: 100%; + background-repeat: no-repeat; +} +.swagger-ui .bg-bottom { + background-position: bottom; + background-repeat: no-repeat; +} +.swagger-ui .bg-left { + background-position: 0; + background-repeat: no-repeat; +} +@media screen and (min-width: 30em) { + .swagger-ui .bg-center-ns { + background-position: 50%; + background-repeat: no-repeat; + } + .swagger-ui .bg-top-ns { + background-position: top; + background-repeat: no-repeat; + } + .swagger-ui .bg-right-ns { + background-position: 100%; + background-repeat: no-repeat; + } + .swagger-ui .bg-bottom-ns { + background-position: bottom; + background-repeat: no-repeat; + } + .swagger-ui .bg-left-ns { + background-position: 0; + background-repeat: no-repeat; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .bg-center-m { + background-position: 50%; + background-repeat: no-repeat; + } + .swagger-ui .bg-top-m { + background-position: top; + background-repeat: no-repeat; + } + .swagger-ui .bg-right-m { + background-position: 100%; + background-repeat: no-repeat; + } + .swagger-ui .bg-bottom-m { + background-position: bottom; + background-repeat: no-repeat; + } + .swagger-ui .bg-left-m { + background-position: 0; + background-repeat: no-repeat; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .bg-center-l { + background-position: 50%; + background-repeat: no-repeat; + } + .swagger-ui .bg-top-l { + background-position: top; + background-repeat: no-repeat; + } + .swagger-ui .bg-right-l { + background-position: 100%; + background-repeat: no-repeat; + } + .swagger-ui .bg-bottom-l { + background-position: bottom; + background-repeat: no-repeat; + } + .swagger-ui .bg-left-l { + background-position: 0; + background-repeat: no-repeat; + } +} +.swagger-ui .outline { + outline: 1px solid; +} +.swagger-ui .outline-transparent { + outline: 1px solid transparent; +} +.swagger-ui .outline-0 { + outline: 0; +} +@media screen and (min-width: 30em) { + .swagger-ui .outline-ns { + outline: 1px solid; + } + .swagger-ui .outline-transparent-ns { + outline: 1px solid transparent; + } + .swagger-ui .outline-0-ns { + outline: 0; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .outline-m { + outline: 1px solid; + } + .swagger-ui .outline-transparent-m { + outline: 1px solid transparent; + } + .swagger-ui .outline-0-m { + outline: 0; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .outline-l { + outline: 1px solid; + } + .swagger-ui .outline-transparent-l { + outline: 1px solid transparent; + } + .swagger-ui .outline-0-l { + outline: 0; + } +} +.swagger-ui .ba { + border-style: solid; + border-width: 1px; +} +.swagger-ui .bt { + border-top-style: solid; + border-top-width: 1px; +} +.swagger-ui .br { + border-right-style: solid; + border-right-width: 1px; +} +.swagger-ui .bb { + border-bottom-style: solid; + border-bottom-width: 1px; +} +.swagger-ui .bl { + border-left-style: solid; + border-left-width: 1px; +} +.swagger-ui .bn { + border-style: none; + border-width: 0; +} +@media screen and (min-width: 30em) { + .swagger-ui .ba-ns { + border-style: solid; + border-width: 1px; + } + .swagger-ui .bt-ns { + border-top-style: solid; + border-top-width: 1px; + } + .swagger-ui .br-ns { + border-right-style: solid; + border-right-width: 1px; + } + .swagger-ui .bb-ns { + border-bottom-style: solid; + border-bottom-width: 1px; + } + .swagger-ui .bl-ns { + border-left-style: solid; + border-left-width: 1px; + } + .swagger-ui .bn-ns { + border-style: none; + border-width: 0; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .ba-m { + border-style: solid; + border-width: 1px; + } + .swagger-ui .bt-m { + border-top-style: solid; + border-top-width: 1px; + } + .swagger-ui .br-m { + border-right-style: solid; + border-right-width: 1px; + } + .swagger-ui .bb-m { + border-bottom-style: solid; + border-bottom-width: 1px; + } + .swagger-ui .bl-m { + border-left-style: solid; + border-left-width: 1px; + } + .swagger-ui .bn-m { + border-style: none; + border-width: 0; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .ba-l { + border-style: solid; + border-width: 1px; + } + .swagger-ui .bt-l { + border-top-style: solid; + border-top-width: 1px; + } + .swagger-ui .br-l { + border-right-style: solid; + border-right-width: 1px; + } + .swagger-ui .bb-l { + border-bottom-style: solid; + border-bottom-width: 1px; + } + .swagger-ui .bl-l { + border-left-style: solid; + border-left-width: 1px; + } + .swagger-ui .bn-l { + border-style: none; + border-width: 0; + } +} +.swagger-ui .b--black { + border-color: #000; +} +.swagger-ui .b--near-black { + border-color: #111; +} +.swagger-ui .b--dark-gray { + border-color: #333; +} +.swagger-ui .b--mid-gray { + border-color: #555; +} +.swagger-ui .b--gray { + border-color: #777; +} +.swagger-ui .b--silver { + border-color: #999; +} +.swagger-ui .b--light-silver { + border-color: #aaa; +} +.swagger-ui .b--moon-gray { + border-color: #ccc; +} +.swagger-ui .b--light-gray { + border-color: #eee; +} +.swagger-ui .b--near-white { + border-color: #f4f4f4; +} +.swagger-ui .b--white { + border-color: #fff; +} +.swagger-ui .b--white-90 { + border-color: hsla(0, 0%, 100%, 0.9); +} +.swagger-ui .b--white-80 { + border-color: hsla(0, 0%, 100%, 0.8); +} +.swagger-ui .b--white-70 { + border-color: hsla(0, 0%, 100%, 0.7); +} +.swagger-ui .b--white-60 { + border-color: hsla(0, 0%, 100%, 0.6); +} +.swagger-ui .b--white-50 { + border-color: hsla(0, 0%, 100%, 0.5); +} +.swagger-ui .b--white-40 { + border-color: hsla(0, 0%, 100%, 0.4); +} +.swagger-ui .b--white-30 { + border-color: hsla(0, 0%, 100%, 0.3); +} +.swagger-ui .b--white-20 { + border-color: hsla(0, 0%, 100%, 0.2); +} +.swagger-ui .b--white-10 { + border-color: hsla(0, 0%, 100%, 0.1); +} +.swagger-ui .b--white-05 { + border-color: hsla(0, 0%, 100%, 0.05); +} +.swagger-ui .b--white-025 { + border-color: hsla(0, 0%, 100%, 0.025); +} +.swagger-ui .b--white-0125 { + border-color: hsla(0, 0%, 100%, 0.013); +} +.swagger-ui .b--black-90 { + border-color: rgba(0, 0, 0, 0.9); +} +.swagger-ui .b--black-80 { + border-color: rgba(0, 0, 0, 0.8); +} +.swagger-ui .b--black-70 { + border-color: rgba(0, 0, 0, 0.7); +} +.swagger-ui .b--black-60 { + border-color: rgba(0, 0, 0, 0.6); +} +.swagger-ui .b--black-50 { + border-color: rgba(0, 0, 0, 0.5); +} +.swagger-ui .b--black-40 { + border-color: rgba(0, 0, 0, 0.4); +} +.swagger-ui .b--black-30 { + border-color: rgba(0, 0, 0, 0.3); +} +.swagger-ui .b--black-20 { + border-color: rgba(0, 0, 0, 0.2); +} +.swagger-ui .b--black-10 { + border-color: rgba(0, 0, 0, 0.1); +} +.swagger-ui .b--black-05 { + border-color: rgba(0, 0, 0, 0.05); +} +.swagger-ui .b--black-025 { + border-color: rgba(0, 0, 0, 0.025); +} +.swagger-ui .b--black-0125 { + border-color: rgba(0, 0, 0, 0.013); +} +.swagger-ui .b--dark-red { + border-color: #e7040f; +} +.swagger-ui .b--red { + border-color: #ff4136; +} +.swagger-ui .b--light-red { + border-color: #ff725c; +} +.swagger-ui .b--orange { + border-color: #ff6300; +} +.swagger-ui .b--gold { + border-color: #ffb700; +} +.swagger-ui .b--yellow { + border-color: gold; +} +.swagger-ui .b--light-yellow { + border-color: #fbf1a9; +} +.swagger-ui .b--purple { + border-color: #5e2ca5; +} +.swagger-ui .b--light-purple { + border-color: #a463f2; +} +.swagger-ui .b--dark-pink { + border-color: #d5008f; +} +.swagger-ui .b--hot-pink { + border-color: #ff41b4; +} +.swagger-ui .b--pink { + border-color: #ff80cc; +} +.swagger-ui .b--light-pink { + border-color: #ffa3d7; +} +.swagger-ui .b--dark-green { + border-color: #137752; +} +.swagger-ui .b--green { + border-color: #19a974; +} +.swagger-ui .b--light-green { + border-color: #9eebcf; +} +.swagger-ui .b--navy { + border-color: #001b44; +} +.swagger-ui .b--dark-blue { + border-color: #00449e; +} +.swagger-ui .b--blue { + border-color: #357edd; +} +.swagger-ui .b--light-blue { + border-color: #96ccff; +} +.swagger-ui .b--lightest-blue { + border-color: #cdecff; +} +.swagger-ui .b--washed-blue { + border-color: #f6fffe; +} +.swagger-ui .b--washed-green { + border-color: #e8fdf5; +} +.swagger-ui .b--washed-yellow { + border-color: #fffceb; +} +.swagger-ui .b--washed-red { + border-color: #ffdfdf; +} +.swagger-ui .b--transparent { + border-color: transparent; +} +.swagger-ui .b--inherit { + border-color: inherit; +} +.swagger-ui .br0 { + border-radius: 0; +} +.swagger-ui .br1 { + border-radius: 0.125rem; +} +.swagger-ui .br2 { + border-radius: 0.25rem; +} +.swagger-ui .br3 { + border-radius: 0.5rem; +} +.swagger-ui .br4 { + border-radius: 1rem; +} +.swagger-ui .br-100 { + border-radius: 100%; +} +.swagger-ui .br-pill { + border-radius: 9999px; +} +.swagger-ui .br--bottom { + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.swagger-ui .br--top { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} +.swagger-ui .br--right { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.swagger-ui .br--left { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +@media screen and (min-width: 30em) { + .swagger-ui .br0-ns { + border-radius: 0; + } + .swagger-ui .br1-ns { + border-radius: 0.125rem; + } + .swagger-ui .br2-ns { + border-radius: 0.25rem; + } + .swagger-ui .br3-ns { + border-radius: 0.5rem; + } + .swagger-ui .br4-ns { + border-radius: 1rem; + } + .swagger-ui .br-100-ns { + border-radius: 100%; + } + .swagger-ui .br-pill-ns { + border-radius: 9999px; + } + .swagger-ui .br--bottom-ns { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + .swagger-ui .br--top-ns { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + .swagger-ui .br--right-ns { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + } + .swagger-ui .br--left-ns { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .br0-m { + border-radius: 0; + } + .swagger-ui .br1-m { + border-radius: 0.125rem; + } + .swagger-ui .br2-m { + border-radius: 0.25rem; + } + .swagger-ui .br3-m { + border-radius: 0.5rem; + } + .swagger-ui .br4-m { + border-radius: 1rem; + } + .swagger-ui .br-100-m { + border-radius: 100%; + } + .swagger-ui .br-pill-m { + border-radius: 9999px; + } + .swagger-ui .br--bottom-m { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + .swagger-ui .br--top-m { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + .swagger-ui .br--right-m { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + } + .swagger-ui .br--left-m { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .br0-l { + border-radius: 0; + } + .swagger-ui .br1-l { + border-radius: 0.125rem; + } + .swagger-ui .br2-l { + border-radius: 0.25rem; + } + .swagger-ui .br3-l { + border-radius: 0.5rem; + } + .swagger-ui .br4-l { + border-radius: 1rem; + } + .swagger-ui .br-100-l { + border-radius: 100%; + } + .swagger-ui .br-pill-l { + border-radius: 9999px; + } + .swagger-ui .br--bottom-l { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + .swagger-ui .br--top-l { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + .swagger-ui .br--right-l { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + } + .swagger-ui .br--left-l { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + } +} +.swagger-ui .b--dotted { + border-style: dotted; +} +.swagger-ui .b--dashed { + border-style: dashed; +} +.swagger-ui .b--solid { + border-style: solid; +} +.swagger-ui .b--none { + border-style: none; +} +@media screen and (min-width: 30em) { + .swagger-ui .b--dotted-ns { + border-style: dotted; + } + .swagger-ui .b--dashed-ns { + border-style: dashed; + } + .swagger-ui .b--solid-ns { + border-style: solid; + } + .swagger-ui .b--none-ns { + border-style: none; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .b--dotted-m { + border-style: dotted; + } + .swagger-ui .b--dashed-m { + border-style: dashed; + } + .swagger-ui .b--solid-m { + border-style: solid; + } + .swagger-ui .b--none-m { + border-style: none; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .b--dotted-l { + border-style: dotted; + } + .swagger-ui .b--dashed-l { + border-style: dashed; + } + .swagger-ui .b--solid-l { + border-style: solid; + } + .swagger-ui .b--none-l { + border-style: none; + } +} +.swagger-ui .bw0 { + border-width: 0; +} +.swagger-ui .bw1 { + border-width: 0.125rem; +} +.swagger-ui .bw2 { + border-width: 0.25rem; +} +.swagger-ui .bw3 { + border-width: 0.5rem; +} +.swagger-ui .bw4 { + border-width: 1rem; +} +.swagger-ui .bw5 { + border-width: 2rem; +} +.swagger-ui .bt-0 { + border-top-width: 0; +} +.swagger-ui .br-0 { + border-right-width: 0; +} +.swagger-ui .bb-0 { + border-bottom-width: 0; +} +.swagger-ui .bl-0 { + border-left-width: 0; +} +@media screen and (min-width: 30em) { + .swagger-ui .bw0-ns { + border-width: 0; + } + .swagger-ui .bw1-ns { + border-width: 0.125rem; + } + .swagger-ui .bw2-ns { + border-width: 0.25rem; + } + .swagger-ui .bw3-ns { + border-width: 0.5rem; + } + .swagger-ui .bw4-ns { + border-width: 1rem; + } + .swagger-ui .bw5-ns { + border-width: 2rem; + } + .swagger-ui .bt-0-ns { + border-top-width: 0; + } + .swagger-ui .br-0-ns { + border-right-width: 0; + } + .swagger-ui .bb-0-ns { + border-bottom-width: 0; + } + .swagger-ui .bl-0-ns { + border-left-width: 0; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .bw0-m { + border-width: 0; + } + .swagger-ui .bw1-m { + border-width: 0.125rem; + } + .swagger-ui .bw2-m { + border-width: 0.25rem; + } + .swagger-ui .bw3-m { + border-width: 0.5rem; + } + .swagger-ui .bw4-m { + border-width: 1rem; + } + .swagger-ui .bw5-m { + border-width: 2rem; + } + .swagger-ui .bt-0-m { + border-top-width: 0; + } + .swagger-ui .br-0-m { + border-right-width: 0; + } + .swagger-ui .bb-0-m { + border-bottom-width: 0; + } + .swagger-ui .bl-0-m { + border-left-width: 0; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .bw0-l { + border-width: 0; + } + .swagger-ui .bw1-l { + border-width: 0.125rem; + } + .swagger-ui .bw2-l { + border-width: 0.25rem; + } + .swagger-ui .bw3-l { + border-width: 0.5rem; + } + .swagger-ui .bw4-l { + border-width: 1rem; + } + .swagger-ui .bw5-l { + border-width: 2rem; + } + .swagger-ui .bt-0-l { + border-top-width: 0; + } + .swagger-ui .br-0-l { + border-right-width: 0; + } + .swagger-ui .bb-0-l { + border-bottom-width: 0; + } + .swagger-ui .bl-0-l { + border-left-width: 0; + } +} +.swagger-ui .shadow-1 { + box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.2); +} +.swagger-ui .shadow-2 { + box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.2); +} +.swagger-ui .shadow-3 { + box-shadow: 2px 2px 4px 2px rgba(0, 0, 0, 0.2); +} +.swagger-ui .shadow-4 { + box-shadow: 2px 2px 8px 0 rgba(0, 0, 0, 0.2); +} +.swagger-ui .shadow-5 { + box-shadow: 4px 4px 8px 0 rgba(0, 0, 0, 0.2); +} +@media screen and (min-width: 30em) { + .swagger-ui .shadow-1-ns { + box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.2); + } + .swagger-ui .shadow-2-ns { + box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.2); + } + .swagger-ui .shadow-3-ns { + box-shadow: 2px 2px 4px 2px rgba(0, 0, 0, 0.2); + } + .swagger-ui .shadow-4-ns { + box-shadow: 2px 2px 8px 0 rgba(0, 0, 0, 0.2); + } + .swagger-ui .shadow-5-ns { + box-shadow: 4px 4px 8px 0 rgba(0, 0, 0, 0.2); + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .shadow-1-m { + box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.2); + } + .swagger-ui .shadow-2-m { + box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.2); + } + .swagger-ui .shadow-3-m { + box-shadow: 2px 2px 4px 2px rgba(0, 0, 0, 0.2); + } + .swagger-ui .shadow-4-m { + box-shadow: 2px 2px 8px 0 rgba(0, 0, 0, 0.2); + } + .swagger-ui .shadow-5-m { + box-shadow: 4px 4px 8px 0 rgba(0, 0, 0, 0.2); + } +} +@media screen and (min-width: 60em) { + .swagger-ui .shadow-1-l { + box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.2); + } + .swagger-ui .shadow-2-l { + box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.2); + } + .swagger-ui .shadow-3-l { + box-shadow: 2px 2px 4px 2px rgba(0, 0, 0, 0.2); + } + .swagger-ui .shadow-4-l { + box-shadow: 2px 2px 8px 0 rgba(0, 0, 0, 0.2); + } + .swagger-ui .shadow-5-l { + box-shadow: 4px 4px 8px 0 rgba(0, 0, 0, 0.2); + } +} +.swagger-ui .pre { + overflow-x: auto; + overflow-y: hidden; + overflow: scroll; +} +.swagger-ui .top-0 { + top: 0; +} +.swagger-ui .right-0 { + right: 0; +} +.swagger-ui .bottom-0 { + bottom: 0; +} +.swagger-ui .left-0 { + left: 0; +} +.swagger-ui .top-1 { + top: 1rem; +} +.swagger-ui .right-1 { + right: 1rem; +} +.swagger-ui .bottom-1 { + bottom: 1rem; +} +.swagger-ui .left-1 { + left: 1rem; +} +.swagger-ui .top-2 { + top: 2rem; +} +.swagger-ui .right-2 { + right: 2rem; +} +.swagger-ui .bottom-2 { + bottom: 2rem; +} +.swagger-ui .left-2 { + left: 2rem; +} +.swagger-ui .top--1 { + top: -1rem; +} +.swagger-ui .right--1 { + right: -1rem; +} +.swagger-ui .bottom--1 { + bottom: -1rem; +} +.swagger-ui .left--1 { + left: -1rem; +} +.swagger-ui .top--2 { + top: -2rem; +} +.swagger-ui .right--2 { + right: -2rem; +} +.swagger-ui .bottom--2 { + bottom: -2rem; +} +.swagger-ui .left--2 { + left: -2rem; +} +.swagger-ui .absolute--fill { + bottom: 0; + left: 0; + right: 0; + top: 0; +} +@media screen and (min-width: 30em) { + .swagger-ui .top-0-ns { + top: 0; + } + .swagger-ui .left-0-ns { + left: 0; + } + .swagger-ui .right-0-ns { + right: 0; + } + .swagger-ui .bottom-0-ns { + bottom: 0; + } + .swagger-ui .top-1-ns { + top: 1rem; + } + .swagger-ui .left-1-ns { + left: 1rem; + } + .swagger-ui .right-1-ns { + right: 1rem; + } + .swagger-ui .bottom-1-ns { + bottom: 1rem; + } + .swagger-ui .top-2-ns { + top: 2rem; + } + .swagger-ui .left-2-ns { + left: 2rem; + } + .swagger-ui .right-2-ns { + right: 2rem; + } + .swagger-ui .bottom-2-ns { + bottom: 2rem; + } + .swagger-ui .top--1-ns { + top: -1rem; + } + .swagger-ui .right--1-ns { + right: -1rem; + } + .swagger-ui .bottom--1-ns { + bottom: -1rem; + } + .swagger-ui .left--1-ns { + left: -1rem; + } + .swagger-ui .top--2-ns { + top: -2rem; + } + .swagger-ui .right--2-ns { + right: -2rem; + } + .swagger-ui .bottom--2-ns { + bottom: -2rem; + } + .swagger-ui .left--2-ns { + left: -2rem; + } + .swagger-ui .absolute--fill-ns { + bottom: 0; + left: 0; + right: 0; + top: 0; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .top-0-m { + top: 0; + } + .swagger-ui .left-0-m { + left: 0; + } + .swagger-ui .right-0-m { + right: 0; + } + .swagger-ui .bottom-0-m { + bottom: 0; + } + .swagger-ui .top-1-m { + top: 1rem; + } + .swagger-ui .left-1-m { + left: 1rem; + } + .swagger-ui .right-1-m { + right: 1rem; + } + .swagger-ui .bottom-1-m { + bottom: 1rem; + } + .swagger-ui .top-2-m { + top: 2rem; + } + .swagger-ui .left-2-m { + left: 2rem; + } + .swagger-ui .right-2-m { + right: 2rem; + } + .swagger-ui .bottom-2-m { + bottom: 2rem; + } + .swagger-ui .top--1-m { + top: -1rem; + } + .swagger-ui .right--1-m { + right: -1rem; + } + .swagger-ui .bottom--1-m { + bottom: -1rem; + } + .swagger-ui .left--1-m { + left: -1rem; + } + .swagger-ui .top--2-m { + top: -2rem; + } + .swagger-ui .right--2-m { + right: -2rem; + } + .swagger-ui .bottom--2-m { + bottom: -2rem; + } + .swagger-ui .left--2-m { + left: -2rem; + } + .swagger-ui .absolute--fill-m { + bottom: 0; + left: 0; + right: 0; + top: 0; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .top-0-l { + top: 0; + } + .swagger-ui .left-0-l { + left: 0; + } + .swagger-ui .right-0-l { + right: 0; + } + .swagger-ui .bottom-0-l { + bottom: 0; + } + .swagger-ui .top-1-l { + top: 1rem; + } + .swagger-ui .left-1-l { + left: 1rem; + } + .swagger-ui .right-1-l { + right: 1rem; + } + .swagger-ui .bottom-1-l { + bottom: 1rem; + } + .swagger-ui .top-2-l { + top: 2rem; + } + .swagger-ui .left-2-l { + left: 2rem; + } + .swagger-ui .right-2-l { + right: 2rem; + } + .swagger-ui .bottom-2-l { + bottom: 2rem; + } + .swagger-ui .top--1-l { + top: -1rem; + } + .swagger-ui .right--1-l { + right: -1rem; + } + .swagger-ui .bottom--1-l { + bottom: -1rem; + } + .swagger-ui .left--1-l { + left: -1rem; + } + .swagger-ui .top--2-l { + top: -2rem; + } + .swagger-ui .right--2-l { + right: -2rem; + } + .swagger-ui .bottom--2-l { + bottom: -2rem; + } + .swagger-ui .left--2-l { + left: -2rem; + } + .swagger-ui .absolute--fill-l { + bottom: 0; + left: 0; + right: 0; + top: 0; + } +} +.swagger-ui .cf:after, +.swagger-ui .cf:before { + content: ' '; + display: table; +} +.swagger-ui .cf:after { + clear: both; +} +.swagger-ui .cf { + zoom: 1; +} +.swagger-ui .cl { + clear: left; +} +.swagger-ui .cr { + clear: right; +} +.swagger-ui .cb { + clear: both; +} +.swagger-ui .cn { + clear: none; +} +@media screen and (min-width: 30em) { + .swagger-ui .cl-ns { + clear: left; + } + .swagger-ui .cr-ns { + clear: right; + } + .swagger-ui .cb-ns { + clear: both; + } + .swagger-ui .cn-ns { + clear: none; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .cl-m { + clear: left; + } + .swagger-ui .cr-m { + clear: right; + } + .swagger-ui .cb-m { + clear: both; + } + .swagger-ui .cn-m { + clear: none; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .cl-l { + clear: left; + } + .swagger-ui .cr-l { + clear: right; + } + .swagger-ui .cb-l { + clear: both; + } + .swagger-ui .cn-l { + clear: none; + } +} +.swagger-ui .flex { + display: flex; +} +.swagger-ui .inline-flex { + display: inline-flex; +} +.swagger-ui .flex-auto { + flex: 1 1 auto; + min-height: 0; + min-width: 0; +} +.swagger-ui .flex-none { + flex: none; +} +.swagger-ui .flex-column { + flex-direction: column; +} +.swagger-ui .flex-row { + flex-direction: row; +} +.swagger-ui .flex-wrap { + flex-wrap: wrap; +} +.swagger-ui .flex-nowrap { + flex-wrap: nowrap; +} +.swagger-ui .flex-wrap-reverse { + flex-wrap: wrap-reverse; +} +.swagger-ui .flex-column-reverse { + flex-direction: column-reverse; +} +.swagger-ui .flex-row-reverse { + flex-direction: row-reverse; +} +.swagger-ui .items-start { + align-items: flex-start; +} +.swagger-ui .items-end { + align-items: flex-end; +} +.swagger-ui .items-center { + align-items: center; +} +.swagger-ui .items-baseline { + align-items: baseline; +} +.swagger-ui .items-stretch { + align-items: stretch; +} +.swagger-ui .self-start { + align-self: flex-start; +} +.swagger-ui .self-end { + align-self: flex-end; +} +.swagger-ui .self-center { + align-self: center; +} +.swagger-ui .self-baseline { + align-self: baseline; +} +.swagger-ui .self-stretch { + align-self: stretch; +} +.swagger-ui .justify-start { + justify-content: flex-start; +} +.swagger-ui .justify-end { + justify-content: flex-end; +} +.swagger-ui .justify-center { + justify-content: center; +} +.swagger-ui .justify-between { + justify-content: space-between; +} +.swagger-ui .justify-around { + justify-content: space-around; +} +.swagger-ui .content-start { + align-content: flex-start; +} +.swagger-ui .content-end { + align-content: flex-end; +} +.swagger-ui .content-center { + align-content: center; +} +.swagger-ui .content-between { + align-content: space-between; +} +.swagger-ui .content-around { + align-content: space-around; +} +.swagger-ui .content-stretch { + align-content: stretch; +} +.swagger-ui .order-0 { + order: 0; +} +.swagger-ui .order-1 { + order: 1; +} +.swagger-ui .order-2 { + order: 2; +} +.swagger-ui .order-3 { + order: 3; +} +.swagger-ui .order-4 { + order: 4; +} +.swagger-ui .order-5 { + order: 5; +} +.swagger-ui .order-6 { + order: 6; +} +.swagger-ui .order-7 { + order: 7; +} +.swagger-ui .order-8 { + order: 8; +} +.swagger-ui .order-last { + order: 99999; +} +.swagger-ui .flex-grow-0 { + flex-grow: 0; +} +.swagger-ui .flex-grow-1 { + flex-grow: 1; +} +.swagger-ui .flex-shrink-0 { + flex-shrink: 0; +} +.swagger-ui .flex-shrink-1 { + flex-shrink: 1; +} +@media screen and (min-width: 30em) { + .swagger-ui .flex-ns { + display: flex; + } + .swagger-ui .inline-flex-ns { + display: inline-flex; + } + .swagger-ui .flex-auto-ns { + flex: 1 1 auto; + min-height: 0; + min-width: 0; + } + .swagger-ui .flex-none-ns { + flex: none; + } + .swagger-ui .flex-column-ns { + flex-direction: column; + } + .swagger-ui .flex-row-ns { + flex-direction: row; + } + .swagger-ui .flex-wrap-ns { + flex-wrap: wrap; + } + .swagger-ui .flex-nowrap-ns { + flex-wrap: nowrap; + } + .swagger-ui .flex-wrap-reverse-ns { + flex-wrap: wrap-reverse; + } + .swagger-ui .flex-column-reverse-ns { + flex-direction: column-reverse; + } + .swagger-ui .flex-row-reverse-ns { + flex-direction: row-reverse; + } + .swagger-ui .items-start-ns { + align-items: flex-start; + } + .swagger-ui .items-end-ns { + align-items: flex-end; + } + .swagger-ui .items-center-ns { + align-items: center; + } + .swagger-ui .items-baseline-ns { + align-items: baseline; + } + .swagger-ui .items-stretch-ns { + align-items: stretch; + } + .swagger-ui .self-start-ns { + align-self: flex-start; + } + .swagger-ui .self-end-ns { + align-self: flex-end; + } + .swagger-ui .self-center-ns { + align-self: center; + } + .swagger-ui .self-baseline-ns { + align-self: baseline; + } + .swagger-ui .self-stretch-ns { + align-self: stretch; + } + .swagger-ui .justify-start-ns { + justify-content: flex-start; + } + .swagger-ui .justify-end-ns { + justify-content: flex-end; + } + .swagger-ui .justify-center-ns { + justify-content: center; + } + .swagger-ui .justify-between-ns { + justify-content: space-between; + } + .swagger-ui .justify-around-ns { + justify-content: space-around; + } + .swagger-ui .content-start-ns { + align-content: flex-start; + } + .swagger-ui .content-end-ns { + align-content: flex-end; + } + .swagger-ui .content-center-ns { + align-content: center; + } + .swagger-ui .content-between-ns { + align-content: space-between; + } + .swagger-ui .content-around-ns { + align-content: space-around; + } + .swagger-ui .content-stretch-ns { + align-content: stretch; + } + .swagger-ui .order-0-ns { + order: 0; + } + .swagger-ui .order-1-ns { + order: 1; + } + .swagger-ui .order-2-ns { + order: 2; + } + .swagger-ui .order-3-ns { + order: 3; + } + .swagger-ui .order-4-ns { + order: 4; + } + .swagger-ui .order-5-ns { + order: 5; + } + .swagger-ui .order-6-ns { + order: 6; + } + .swagger-ui .order-7-ns { + order: 7; + } + .swagger-ui .order-8-ns { + order: 8; + } + .swagger-ui .order-last-ns { + order: 99999; + } + .swagger-ui .flex-grow-0-ns { + flex-grow: 0; + } + .swagger-ui .flex-grow-1-ns { + flex-grow: 1; + } + .swagger-ui .flex-shrink-0-ns { + flex-shrink: 0; + } + .swagger-ui .flex-shrink-1-ns { + flex-shrink: 1; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .flex-m { + display: flex; + } + .swagger-ui .inline-flex-m { + display: inline-flex; + } + .swagger-ui .flex-auto-m { + flex: 1 1 auto; + min-height: 0; + min-width: 0; + } + .swagger-ui .flex-none-m { + flex: none; + } + .swagger-ui .flex-column-m { + flex-direction: column; + } + .swagger-ui .flex-row-m { + flex-direction: row; + } + .swagger-ui .flex-wrap-m { + flex-wrap: wrap; + } + .swagger-ui .flex-nowrap-m { + flex-wrap: nowrap; + } + .swagger-ui .flex-wrap-reverse-m { + flex-wrap: wrap-reverse; + } + .swagger-ui .flex-column-reverse-m { + flex-direction: column-reverse; + } + .swagger-ui .flex-row-reverse-m { + flex-direction: row-reverse; + } + .swagger-ui .items-start-m { + align-items: flex-start; + } + .swagger-ui .items-end-m { + align-items: flex-end; + } + .swagger-ui .items-center-m { + align-items: center; + } + .swagger-ui .items-baseline-m { + align-items: baseline; + } + .swagger-ui .items-stretch-m { + align-items: stretch; + } + .swagger-ui .self-start-m { + align-self: flex-start; + } + .swagger-ui .self-end-m { + align-self: flex-end; + } + .swagger-ui .self-center-m { + align-self: center; + } + .swagger-ui .self-baseline-m { + align-self: baseline; + } + .swagger-ui .self-stretch-m { + align-self: stretch; + } + .swagger-ui .justify-start-m { + justify-content: flex-start; + } + .swagger-ui .justify-end-m { + justify-content: flex-end; + } + .swagger-ui .justify-center-m { + justify-content: center; + } + .swagger-ui .justify-between-m { + justify-content: space-between; + } + .swagger-ui .justify-around-m { + justify-content: space-around; + } + .swagger-ui .content-start-m { + align-content: flex-start; + } + .swagger-ui .content-end-m { + align-content: flex-end; + } + .swagger-ui .content-center-m { + align-content: center; + } + .swagger-ui .content-between-m { + align-content: space-between; + } + .swagger-ui .content-around-m { + align-content: space-around; + } + .swagger-ui .content-stretch-m { + align-content: stretch; + } + .swagger-ui .order-0-m { + order: 0; + } + .swagger-ui .order-1-m { + order: 1; + } + .swagger-ui .order-2-m { + order: 2; + } + .swagger-ui .order-3-m { + order: 3; + } + .swagger-ui .order-4-m { + order: 4; + } + .swagger-ui .order-5-m { + order: 5; + } + .swagger-ui .order-6-m { + order: 6; + } + .swagger-ui .order-7-m { + order: 7; + } + .swagger-ui .order-8-m { + order: 8; + } + .swagger-ui .order-last-m { + order: 99999; + } + .swagger-ui .flex-grow-0-m { + flex-grow: 0; + } + .swagger-ui .flex-grow-1-m { + flex-grow: 1; + } + .swagger-ui .flex-shrink-0-m { + flex-shrink: 0; + } + .swagger-ui .flex-shrink-1-m { + flex-shrink: 1; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .flex-l { + display: flex; + } + .swagger-ui .inline-flex-l { + display: inline-flex; + } + .swagger-ui .flex-auto-l { + flex: 1 1 auto; + min-height: 0; + min-width: 0; + } + .swagger-ui .flex-none-l { + flex: none; + } + .swagger-ui .flex-column-l { + flex-direction: column; + } + .swagger-ui .flex-row-l { + flex-direction: row; + } + .swagger-ui .flex-wrap-l { + flex-wrap: wrap; + } + .swagger-ui .flex-nowrap-l { + flex-wrap: nowrap; + } + .swagger-ui .flex-wrap-reverse-l { + flex-wrap: wrap-reverse; + } + .swagger-ui .flex-column-reverse-l { + flex-direction: column-reverse; + } + .swagger-ui .flex-row-reverse-l { + flex-direction: row-reverse; + } + .swagger-ui .items-start-l { + align-items: flex-start; + } + .swagger-ui .items-end-l { + align-items: flex-end; + } + .swagger-ui .items-center-l { + align-items: center; + } + .swagger-ui .items-baseline-l { + align-items: baseline; + } + .swagger-ui .items-stretch-l { + align-items: stretch; + } + .swagger-ui .self-start-l { + align-self: flex-start; + } + .swagger-ui .self-end-l { + align-self: flex-end; + } + .swagger-ui .self-center-l { + align-self: center; + } + .swagger-ui .self-baseline-l { + align-self: baseline; + } + .swagger-ui .self-stretch-l { + align-self: stretch; + } + .swagger-ui .justify-start-l { + justify-content: flex-start; + } + .swagger-ui .justify-end-l { + justify-content: flex-end; + } + .swagger-ui .justify-center-l { + justify-content: center; + } + .swagger-ui .justify-between-l { + justify-content: space-between; + } + .swagger-ui .justify-around-l { + justify-content: space-around; + } + .swagger-ui .content-start-l { + align-content: flex-start; + } + .swagger-ui .content-end-l { + align-content: flex-end; + } + .swagger-ui .content-center-l { + align-content: center; + } + .swagger-ui .content-between-l { + align-content: space-between; + } + .swagger-ui .content-around-l { + align-content: space-around; + } + .swagger-ui .content-stretch-l { + align-content: stretch; + } + .swagger-ui .order-0-l { + order: 0; + } + .swagger-ui .order-1-l { + order: 1; + } + .swagger-ui .order-2-l { + order: 2; + } + .swagger-ui .order-3-l { + order: 3; + } + .swagger-ui .order-4-l { + order: 4; + } + .swagger-ui .order-5-l { + order: 5; + } + .swagger-ui .order-6-l { + order: 6; + } + .swagger-ui .order-7-l { + order: 7; + } + .swagger-ui .order-8-l { + order: 8; + } + .swagger-ui .order-last-l { + order: 99999; + } + .swagger-ui .flex-grow-0-l { + flex-grow: 0; + } + .swagger-ui .flex-grow-1-l { + flex-grow: 1; + } + .swagger-ui .flex-shrink-0-l { + flex-shrink: 0; + } + .swagger-ui .flex-shrink-1-l { + flex-shrink: 1; + } +} +.swagger-ui .dn { + display: none; +} +.swagger-ui .di { + display: inline; +} +.swagger-ui .db { + display: block; +} +.swagger-ui .dib { + display: inline-block; +} +.swagger-ui .dit { + display: inline-table; +} +.swagger-ui .dt { + display: table; +} +.swagger-ui .dtc { + display: table-cell; +} +.swagger-ui .dt-row { + display: table-row; +} +.swagger-ui .dt-row-group { + display: table-row-group; +} +.swagger-ui .dt-column { + display: table-column; +} +.swagger-ui .dt-column-group { + display: table-column-group; +} +.swagger-ui .dt--fixed { + table-layout: fixed; + width: 100%; +} +@media screen and (min-width: 30em) { + .swagger-ui .dn-ns { + display: none; + } + .swagger-ui .di-ns { + display: inline; + } + .swagger-ui .db-ns { + display: block; + } + .swagger-ui .dib-ns { + display: inline-block; + } + .swagger-ui .dit-ns { + display: inline-table; + } + .swagger-ui .dt-ns { + display: table; + } + .swagger-ui .dtc-ns { + display: table-cell; + } + .swagger-ui .dt-row-ns { + display: table-row; + } + .swagger-ui .dt-row-group-ns { + display: table-row-group; + } + .swagger-ui .dt-column-ns { + display: table-column; + } + .swagger-ui .dt-column-group-ns { + display: table-column-group; + } + .swagger-ui .dt--fixed-ns { + table-layout: fixed; + width: 100%; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .dn-m { + display: none; + } + .swagger-ui .di-m { + display: inline; + } + .swagger-ui .db-m { + display: block; + } + .swagger-ui .dib-m { + display: inline-block; + } + .swagger-ui .dit-m { + display: inline-table; + } + .swagger-ui .dt-m { + display: table; + } + .swagger-ui .dtc-m { + display: table-cell; + } + .swagger-ui .dt-row-m { + display: table-row; + } + .swagger-ui .dt-row-group-m { + display: table-row-group; + } + .swagger-ui .dt-column-m { + display: table-column; + } + .swagger-ui .dt-column-group-m { + display: table-column-group; + } + .swagger-ui .dt--fixed-m { + table-layout: fixed; + width: 100%; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .dn-l { + display: none; + } + .swagger-ui .di-l { + display: inline; + } + .swagger-ui .db-l { + display: block; + } + .swagger-ui .dib-l { + display: inline-block; + } + .swagger-ui .dit-l { + display: inline-table; + } + .swagger-ui .dt-l { + display: table; + } + .swagger-ui .dtc-l { + display: table-cell; + } + .swagger-ui .dt-row-l { + display: table-row; + } + .swagger-ui .dt-row-group-l { + display: table-row-group; + } + .swagger-ui .dt-column-l { + display: table-column; + } + .swagger-ui .dt-column-group-l { + display: table-column-group; + } + .swagger-ui .dt--fixed-l { + table-layout: fixed; + width: 100%; + } +} +.swagger-ui .fl { + _display: inline; + float: left; +} +.swagger-ui .fr { + _display: inline; + float: right; +} +.swagger-ui .fn { + float: none; +} +@media screen and (min-width: 30em) { + .swagger-ui .fl-ns { + _display: inline; + float: left; + } + .swagger-ui .fr-ns { + _display: inline; + float: right; + } + .swagger-ui .fn-ns { + float: none; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .fl-m { + _display: inline; + float: left; + } + .swagger-ui .fr-m { + _display: inline; + float: right; + } + .swagger-ui .fn-m { + float: none; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .fl-l { + _display: inline; + float: left; + } + .swagger-ui .fr-l { + _display: inline; + float: right; + } + .swagger-ui .fn-l { + float: none; + } +} +.swagger-ui .sans-serif { + font-family: + -apple-system, + BlinkMacSystemFont, + avenir next, + avenir, + helvetica, + helvetica neue, + ubuntu, + roboto, + noto, + segoe ui, + arial, + sans-serif; +} +.swagger-ui .serif { + font-family: georgia, serif; +} +.swagger-ui .system-sans-serif { + font-family: sans-serif; +} +.swagger-ui .system-serif { + font-family: serif; +} +.swagger-ui .code, +.swagger-ui code { + font-family: Consolas, monaco, monospace; +} +.swagger-ui .courier { + font-family: + Courier Next, + courier, + monospace; +} +.swagger-ui .helvetica { + font-family: + helvetica neue, + helvetica, + sans-serif; +} +.swagger-ui .avenir { + font-family: + avenir next, + avenir, + sans-serif; +} +.swagger-ui .athelas { + font-family: athelas, georgia, serif; +} +.swagger-ui .georgia { + font-family: georgia, serif; +} +.swagger-ui .times { + font-family: times, serif; +} +.swagger-ui .bodoni { + font-family: + Bodoni MT, + serif; +} +.swagger-ui .calisto { + font-family: + Calisto MT, + serif; +} +.swagger-ui .garamond { + font-family: garamond, serif; +} +.swagger-ui .baskerville { + font-family: baskerville, serif; +} +.swagger-ui .i { + font-style: italic; +} +.swagger-ui .fs-normal { + font-style: normal; +} +@media screen and (min-width: 30em) { + .swagger-ui .i-ns { + font-style: italic; + } + .swagger-ui .fs-normal-ns { + font-style: normal; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .i-m { + font-style: italic; + } + .swagger-ui .fs-normal-m { + font-style: normal; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .i-l { + font-style: italic; + } + .swagger-ui .fs-normal-l { + font-style: normal; + } +} +.swagger-ui .normal { + font-weight: 400; +} +.swagger-ui .b { + font-weight: 700; +} +.swagger-ui .fw1 { + font-weight: 100; +} +.swagger-ui .fw2 { + font-weight: 200; +} +.swagger-ui .fw3 { + font-weight: 300; +} +.swagger-ui .fw4 { + font-weight: 400; +} +.swagger-ui .fw5 { + font-weight: 500; +} +.swagger-ui .fw6 { + font-weight: 600; +} +.swagger-ui .fw7 { + font-weight: 700; +} +.swagger-ui .fw8 { + font-weight: 800; +} +.swagger-ui .fw9 { + font-weight: 900; +} +@media screen and (min-width: 30em) { + .swagger-ui .normal-ns { + font-weight: 400; + } + .swagger-ui .b-ns { + font-weight: 700; + } + .swagger-ui .fw1-ns { + font-weight: 100; + } + .swagger-ui .fw2-ns { + font-weight: 200; + } + .swagger-ui .fw3-ns { + font-weight: 300; + } + .swagger-ui .fw4-ns { + font-weight: 400; + } + .swagger-ui .fw5-ns { + font-weight: 500; + } + .swagger-ui .fw6-ns { + font-weight: 600; + } + .swagger-ui .fw7-ns { + font-weight: 700; + } + .swagger-ui .fw8-ns { + font-weight: 800; + } + .swagger-ui .fw9-ns { + font-weight: 900; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .normal-m { + font-weight: 400; + } + .swagger-ui .b-m { + font-weight: 700; + } + .swagger-ui .fw1-m { + font-weight: 100; + } + .swagger-ui .fw2-m { + font-weight: 200; + } + .swagger-ui .fw3-m { + font-weight: 300; + } + .swagger-ui .fw4-m { + font-weight: 400; + } + .swagger-ui .fw5-m { + font-weight: 500; + } + .swagger-ui .fw6-m { + font-weight: 600; + } + .swagger-ui .fw7-m { + font-weight: 700; + } + .swagger-ui .fw8-m { + font-weight: 800; + } + .swagger-ui .fw9-m { + font-weight: 900; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .normal-l { + font-weight: 400; + } + .swagger-ui .b-l { + font-weight: 700; + } + .swagger-ui .fw1-l { + font-weight: 100; + } + .swagger-ui .fw2-l { + font-weight: 200; + } + .swagger-ui .fw3-l { + font-weight: 300; + } + .swagger-ui .fw4-l { + font-weight: 400; + } + .swagger-ui .fw5-l { + font-weight: 500; + } + .swagger-ui .fw6-l { + font-weight: 600; + } + .swagger-ui .fw7-l { + font-weight: 700; + } + .swagger-ui .fw8-l { + font-weight: 800; + } + .swagger-ui .fw9-l { + font-weight: 900; + } +} +.swagger-ui .input-reset { + -webkit-appearance: none; + -moz-appearance: none; +} +.swagger-ui .button-reset::-moz-focus-inner, +.swagger-ui .input-reset::-moz-focus-inner { + border: 0; + padding: 0; +} +.swagger-ui .h1 { + height: 1rem; +} +.swagger-ui .h2 { + height: 2rem; +} +.swagger-ui .h3 { + height: 4rem; +} +.swagger-ui .h4 { + height: 8rem; +} +.swagger-ui .h5 { + height: 16rem; +} +.swagger-ui .h-25 { + height: 25%; +} +.swagger-ui .h-50 { + height: 50%; +} +.swagger-ui .h-75 { + height: 75%; +} +.swagger-ui .h-100 { + height: 100%; +} +.swagger-ui .min-h-100 { + min-height: 100%; +} +.swagger-ui .vh-25 { + height: 25vh; +} +.swagger-ui .vh-50 { + height: 50vh; +} +.swagger-ui .vh-75 { + height: 75vh; +} +.swagger-ui .vh-100 { + height: 100vh; +} +.swagger-ui .min-vh-100 { + min-height: 100vh; +} +.swagger-ui .h-auto { + height: auto; +} +.swagger-ui .h-inherit { + height: inherit; +} +@media screen and (min-width: 30em) { + .swagger-ui .h1-ns { + height: 1rem; + } + .swagger-ui .h2-ns { + height: 2rem; + } + .swagger-ui .h3-ns { + height: 4rem; + } + .swagger-ui .h4-ns { + height: 8rem; + } + .swagger-ui .h5-ns { + height: 16rem; + } + .swagger-ui .h-25-ns { + height: 25%; + } + .swagger-ui .h-50-ns { + height: 50%; + } + .swagger-ui .h-75-ns { + height: 75%; + } + .swagger-ui .h-100-ns { + height: 100%; + } + .swagger-ui .min-h-100-ns { + min-height: 100%; + } + .swagger-ui .vh-25-ns { + height: 25vh; + } + .swagger-ui .vh-50-ns { + height: 50vh; + } + .swagger-ui .vh-75-ns { + height: 75vh; + } + .swagger-ui .vh-100-ns { + height: 100vh; + } + .swagger-ui .min-vh-100-ns { + min-height: 100vh; + } + .swagger-ui .h-auto-ns { + height: auto; + } + .swagger-ui .h-inherit-ns { + height: inherit; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .h1-m { + height: 1rem; + } + .swagger-ui .h2-m { + height: 2rem; + } + .swagger-ui .h3-m { + height: 4rem; + } + .swagger-ui .h4-m { + height: 8rem; + } + .swagger-ui .h5-m { + height: 16rem; + } + .swagger-ui .h-25-m { + height: 25%; + } + .swagger-ui .h-50-m { + height: 50%; + } + .swagger-ui .h-75-m { + height: 75%; + } + .swagger-ui .h-100-m { + height: 100%; + } + .swagger-ui .min-h-100-m { + min-height: 100%; + } + .swagger-ui .vh-25-m { + height: 25vh; + } + .swagger-ui .vh-50-m { + height: 50vh; + } + .swagger-ui .vh-75-m { + height: 75vh; + } + .swagger-ui .vh-100-m { + height: 100vh; + } + .swagger-ui .min-vh-100-m { + min-height: 100vh; + } + .swagger-ui .h-auto-m { + height: auto; + } + .swagger-ui .h-inherit-m { + height: inherit; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .h1-l { + height: 1rem; + } + .swagger-ui .h2-l { + height: 2rem; + } + .swagger-ui .h3-l { + height: 4rem; + } + .swagger-ui .h4-l { + height: 8rem; + } + .swagger-ui .h5-l { + height: 16rem; + } + .swagger-ui .h-25-l { + height: 25%; + } + .swagger-ui .h-50-l { + height: 50%; + } + .swagger-ui .h-75-l { + height: 75%; + } + .swagger-ui .h-100-l { + height: 100%; + } + .swagger-ui .min-h-100-l { + min-height: 100%; + } + .swagger-ui .vh-25-l { + height: 25vh; + } + .swagger-ui .vh-50-l { + height: 50vh; + } + .swagger-ui .vh-75-l { + height: 75vh; + } + .swagger-ui .vh-100-l { + height: 100vh; + } + .swagger-ui .min-vh-100-l { + min-height: 100vh; + } + .swagger-ui .h-auto-l { + height: auto; + } + .swagger-ui .h-inherit-l { + height: inherit; + } +} +.swagger-ui .tracked { + letter-spacing: 0.1em; +} +.swagger-ui .tracked-tight { + letter-spacing: -0.05em; +} +.swagger-ui .tracked-mega { + letter-spacing: 0.25em; +} +@media screen and (min-width: 30em) { + .swagger-ui .tracked-ns { + letter-spacing: 0.1em; + } + .swagger-ui .tracked-tight-ns { + letter-spacing: -0.05em; + } + .swagger-ui .tracked-mega-ns { + letter-spacing: 0.25em; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .tracked-m { + letter-spacing: 0.1em; + } + .swagger-ui .tracked-tight-m { + letter-spacing: -0.05em; + } + .swagger-ui .tracked-mega-m { + letter-spacing: 0.25em; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .tracked-l { + letter-spacing: 0.1em; + } + .swagger-ui .tracked-tight-l { + letter-spacing: -0.05em; + } + .swagger-ui .tracked-mega-l { + letter-spacing: 0.25em; + } +} +.swagger-ui .lh-solid { + line-height: 1; +} +.swagger-ui .lh-title { + line-height: 1.25; +} +.swagger-ui .lh-copy { + line-height: 1.5; +} +@media screen and (min-width: 30em) { + .swagger-ui .lh-solid-ns { + line-height: 1; + } + .swagger-ui .lh-title-ns { + line-height: 1.25; + } + .swagger-ui .lh-copy-ns { + line-height: 1.5; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .lh-solid-m { + line-height: 1; + } + .swagger-ui .lh-title-m { + line-height: 1.25; + } + .swagger-ui .lh-copy-m { + line-height: 1.5; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .lh-solid-l { + line-height: 1; + } + .swagger-ui .lh-title-l { + line-height: 1.25; + } + .swagger-ui .lh-copy-l { + line-height: 1.5; + } +} +.swagger-ui .link { + -webkit-text-decoration: none; + text-decoration: none; +} +.swagger-ui .link, +.swagger-ui .link:active, +.swagger-ui .link:focus, +.swagger-ui .link:hover, +.swagger-ui .link:link, +.swagger-ui .link:visited { + transition: color 0.15s ease-in; +} +.swagger-ui .link:focus { + outline: 1px dotted currentColor; +} +.swagger-ui .list { + list-style-type: none; +} +.swagger-ui .mw-100 { + max-width: 100%; +} +.swagger-ui .mw1 { + max-width: 1rem; +} +.swagger-ui .mw2 { + max-width: 2rem; +} +.swagger-ui .mw3 { + max-width: 4rem; +} +.swagger-ui .mw4 { + max-width: 8rem; +} +.swagger-ui .mw5 { + max-width: 16rem; +} +.swagger-ui .mw6 { + max-width: 32rem; +} +.swagger-ui .mw7 { + max-width: 48rem; +} +.swagger-ui .mw8 { + max-width: 64rem; +} +.swagger-ui .mw9 { + max-width: 96rem; +} +.swagger-ui .mw-none { + max-width: none; +} +@media screen and (min-width: 30em) { + .swagger-ui .mw-100-ns { + max-width: 100%; + } + .swagger-ui .mw1-ns { + max-width: 1rem; + } + .swagger-ui .mw2-ns { + max-width: 2rem; + } + .swagger-ui .mw3-ns { + max-width: 4rem; + } + .swagger-ui .mw4-ns { + max-width: 8rem; + } + .swagger-ui .mw5-ns { + max-width: 16rem; + } + .swagger-ui .mw6-ns { + max-width: 32rem; + } + .swagger-ui .mw7-ns { + max-width: 48rem; + } + .swagger-ui .mw8-ns { + max-width: 64rem; + } + .swagger-ui .mw9-ns { + max-width: 96rem; + } + .swagger-ui .mw-none-ns { + max-width: none; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .mw-100-m { + max-width: 100%; + } + .swagger-ui .mw1-m { + max-width: 1rem; + } + .swagger-ui .mw2-m { + max-width: 2rem; + } + .swagger-ui .mw3-m { + max-width: 4rem; + } + .swagger-ui .mw4-m { + max-width: 8rem; + } + .swagger-ui .mw5-m { + max-width: 16rem; + } + .swagger-ui .mw6-m { + max-width: 32rem; + } + .swagger-ui .mw7-m { + max-width: 48rem; + } + .swagger-ui .mw8-m { + max-width: 64rem; + } + .swagger-ui .mw9-m { + max-width: 96rem; + } + .swagger-ui .mw-none-m { + max-width: none; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .mw-100-l { + max-width: 100%; + } + .swagger-ui .mw1-l { + max-width: 1rem; + } + .swagger-ui .mw2-l { + max-width: 2rem; + } + .swagger-ui .mw3-l { + max-width: 4rem; + } + .swagger-ui .mw4-l { + max-width: 8rem; + } + .swagger-ui .mw5-l { + max-width: 16rem; + } + .swagger-ui .mw6-l { + max-width: 32rem; + } + .swagger-ui .mw7-l { + max-width: 48rem; + } + .swagger-ui .mw8-l { + max-width: 64rem; + } + .swagger-ui .mw9-l { + max-width: 96rem; + } + .swagger-ui .mw-none-l { + max-width: none; + } +} +.swagger-ui .w1 { + width: 1rem; +} +.swagger-ui .w2 { + width: 2rem; +} +.swagger-ui .w3 { + width: 4rem; +} +.swagger-ui .w4 { + width: 8rem; +} +.swagger-ui .w5 { + width: 16rem; +} +.swagger-ui .w-10 { + width: 10%; +} +.swagger-ui .w-20 { + width: 20%; +} +.swagger-ui .w-25 { + width: 25%; +} +.swagger-ui .w-30 { + width: 30%; +} +.swagger-ui .w-33 { + width: 33%; +} +.swagger-ui .w-34 { + width: 34%; +} +.swagger-ui .w-40 { + width: 40%; +} +.swagger-ui .w-50 { + width: 50%; +} +.swagger-ui .w-60 { + width: 60%; +} +.swagger-ui .w-70 { + width: 70%; +} +.swagger-ui .w-75 { + width: 75%; +} +.swagger-ui .w-80 { + width: 80%; +} +.swagger-ui .w-90 { + width: 90%; +} +.swagger-ui .w-100 { + width: 100%; +} +.swagger-ui .w-third { + width: 33.3333333333%; +} +.swagger-ui .w-two-thirds { + width: 66.6666666667%; +} +.swagger-ui .w-auto { + width: auto; +} +@media screen and (min-width: 30em) { + .swagger-ui .w1-ns { + width: 1rem; + } + .swagger-ui .w2-ns { + width: 2rem; + } + .swagger-ui .w3-ns { + width: 4rem; + } + .swagger-ui .w4-ns { + width: 8rem; + } + .swagger-ui .w5-ns { + width: 16rem; + } + .swagger-ui .w-10-ns { + width: 10%; + } + .swagger-ui .w-20-ns { + width: 20%; + } + .swagger-ui .w-25-ns { + width: 25%; + } + .swagger-ui .w-30-ns { + width: 30%; + } + .swagger-ui .w-33-ns { + width: 33%; + } + .swagger-ui .w-34-ns { + width: 34%; + } + .swagger-ui .w-40-ns { + width: 40%; + } + .swagger-ui .w-50-ns { + width: 50%; + } + .swagger-ui .w-60-ns { + width: 60%; + } + .swagger-ui .w-70-ns { + width: 70%; + } + .swagger-ui .w-75-ns { + width: 75%; + } + .swagger-ui .w-80-ns { + width: 80%; + } + .swagger-ui .w-90-ns { + width: 90%; + } + .swagger-ui .w-100-ns { + width: 100%; + } + .swagger-ui .w-third-ns { + width: 33.3333333333%; + } + .swagger-ui .w-two-thirds-ns { + width: 66.6666666667%; + } + .swagger-ui .w-auto-ns { + width: auto; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .w1-m { + width: 1rem; + } + .swagger-ui .w2-m { + width: 2rem; + } + .swagger-ui .w3-m { + width: 4rem; + } + .swagger-ui .w4-m { + width: 8rem; + } + .swagger-ui .w5-m { + width: 16rem; + } + .swagger-ui .w-10-m { + width: 10%; + } + .swagger-ui .w-20-m { + width: 20%; + } + .swagger-ui .w-25-m { + width: 25%; + } + .swagger-ui .w-30-m { + width: 30%; + } + .swagger-ui .w-33-m { + width: 33%; + } + .swagger-ui .w-34-m { + width: 34%; + } + .swagger-ui .w-40-m { + width: 40%; + } + .swagger-ui .w-50-m { + width: 50%; + } + .swagger-ui .w-60-m { + width: 60%; + } + .swagger-ui .w-70-m { + width: 70%; + } + .swagger-ui .w-75-m { + width: 75%; + } + .swagger-ui .w-80-m { + width: 80%; + } + .swagger-ui .w-90-m { + width: 90%; + } + .swagger-ui .w-100-m { + width: 100%; + } + .swagger-ui .w-third-m { + width: 33.3333333333%; + } + .swagger-ui .w-two-thirds-m { + width: 66.6666666667%; + } + .swagger-ui .w-auto-m { + width: auto; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .w1-l { + width: 1rem; + } + .swagger-ui .w2-l { + width: 2rem; + } + .swagger-ui .w3-l { + width: 4rem; + } + .swagger-ui .w4-l { + width: 8rem; + } + .swagger-ui .w5-l { + width: 16rem; + } + .swagger-ui .w-10-l { + width: 10%; + } + .swagger-ui .w-20-l { + width: 20%; + } + .swagger-ui .w-25-l { + width: 25%; + } + .swagger-ui .w-30-l { + width: 30%; + } + .swagger-ui .w-33-l { + width: 33%; + } + .swagger-ui .w-34-l { + width: 34%; + } + .swagger-ui .w-40-l { + width: 40%; + } + .swagger-ui .w-50-l { + width: 50%; + } + .swagger-ui .w-60-l { + width: 60%; + } + .swagger-ui .w-70-l { + width: 70%; + } + .swagger-ui .w-75-l { + width: 75%; + } + .swagger-ui .w-80-l { + width: 80%; + } + .swagger-ui .w-90-l { + width: 90%; + } + .swagger-ui .w-100-l { + width: 100%; + } + .swagger-ui .w-third-l { + width: 33.3333333333%; + } + .swagger-ui .w-two-thirds-l { + width: 66.6666666667%; + } + .swagger-ui .w-auto-l { + width: auto; + } +} +.swagger-ui .overflow-visible { + overflow: visible; +} +.swagger-ui .overflow-hidden { + overflow: hidden; +} +.swagger-ui .overflow-scroll { + overflow: scroll; +} +.swagger-ui .overflow-auto { + overflow: auto; +} +.swagger-ui .overflow-x-visible { + overflow-x: visible; +} +.swagger-ui .overflow-x-hidden { + overflow-x: hidden; +} +.swagger-ui .overflow-x-scroll { + overflow-x: scroll; +} +.swagger-ui .overflow-x-auto { + overflow-x: auto; +} +.swagger-ui .overflow-y-visible { + overflow-y: visible; +} +.swagger-ui .overflow-y-hidden { + overflow-y: hidden; +} +.swagger-ui .overflow-y-scroll { + overflow-y: scroll; +} +.swagger-ui .overflow-y-auto { + overflow-y: auto; +} +@media screen and (min-width: 30em) { + .swagger-ui .overflow-visible-ns { + overflow: visible; + } + .swagger-ui .overflow-hidden-ns { + overflow: hidden; + } + .swagger-ui .overflow-scroll-ns { + overflow: scroll; + } + .swagger-ui .overflow-auto-ns { + overflow: auto; + } + .swagger-ui .overflow-x-visible-ns { + overflow-x: visible; + } + .swagger-ui .overflow-x-hidden-ns { + overflow-x: hidden; + } + .swagger-ui .overflow-x-scroll-ns { + overflow-x: scroll; + } + .swagger-ui .overflow-x-auto-ns { + overflow-x: auto; + } + .swagger-ui .overflow-y-visible-ns { + overflow-y: visible; + } + .swagger-ui .overflow-y-hidden-ns { + overflow-y: hidden; + } + .swagger-ui .overflow-y-scroll-ns { + overflow-y: scroll; + } + .swagger-ui .overflow-y-auto-ns { + overflow-y: auto; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .overflow-visible-m { + overflow: visible; + } + .swagger-ui .overflow-hidden-m { + overflow: hidden; + } + .swagger-ui .overflow-scroll-m { + overflow: scroll; + } + .swagger-ui .overflow-auto-m { + overflow: auto; + } + .swagger-ui .overflow-x-visible-m { + overflow-x: visible; + } + .swagger-ui .overflow-x-hidden-m { + overflow-x: hidden; + } + .swagger-ui .overflow-x-scroll-m { + overflow-x: scroll; + } + .swagger-ui .overflow-x-auto-m { + overflow-x: auto; + } + .swagger-ui .overflow-y-visible-m { + overflow-y: visible; + } + .swagger-ui .overflow-y-hidden-m { + overflow-y: hidden; + } + .swagger-ui .overflow-y-scroll-m { + overflow-y: scroll; + } + .swagger-ui .overflow-y-auto-m { + overflow-y: auto; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .overflow-visible-l { + overflow: visible; + } + .swagger-ui .overflow-hidden-l { + overflow: hidden; + } + .swagger-ui .overflow-scroll-l { + overflow: scroll; + } + .swagger-ui .overflow-auto-l { + overflow: auto; + } + .swagger-ui .overflow-x-visible-l { + overflow-x: visible; + } + .swagger-ui .overflow-x-hidden-l { + overflow-x: hidden; + } + .swagger-ui .overflow-x-scroll-l { + overflow-x: scroll; + } + .swagger-ui .overflow-x-auto-l { + overflow-x: auto; + } + .swagger-ui .overflow-y-visible-l { + overflow-y: visible; + } + .swagger-ui .overflow-y-hidden-l { + overflow-y: hidden; + } + .swagger-ui .overflow-y-scroll-l { + overflow-y: scroll; + } + .swagger-ui .overflow-y-auto-l { + overflow-y: auto; + } +} +.swagger-ui .static { + position: static; +} +.swagger-ui .relative { + position: relative; +} +.swagger-ui .absolute { + position: absolute; +} +.swagger-ui .fixed { + position: fixed; +} +@media screen and (min-width: 30em) { + .swagger-ui .static-ns { + position: static; + } + .swagger-ui .relative-ns { + position: relative; + } + .swagger-ui .absolute-ns { + position: absolute; + } + .swagger-ui .fixed-ns { + position: fixed; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .static-m { + position: static; + } + .swagger-ui .relative-m { + position: relative; + } + .swagger-ui .absolute-m { + position: absolute; + } + .swagger-ui .fixed-m { + position: fixed; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .static-l { + position: static; + } + .swagger-ui .relative-l { + position: relative; + } + .swagger-ui .absolute-l { + position: absolute; + } + .swagger-ui .fixed-l { + position: fixed; + } +} +.swagger-ui .o-100 { + opacity: 1; +} +.swagger-ui .o-90 { + opacity: 0.9; +} +.swagger-ui .o-80 { + opacity: 0.8; +} +.swagger-ui .o-70 { + opacity: 0.7; +} +.swagger-ui .o-60 { + opacity: 0.6; +} +.swagger-ui .o-50 { + opacity: 0.5; +} +.swagger-ui .o-40 { + opacity: 0.4; +} +.swagger-ui .o-30 { + opacity: 0.3; +} +.swagger-ui .o-20 { + opacity: 0.2; +} +.swagger-ui .o-10 { + opacity: 0.1; +} +.swagger-ui .o-05 { + opacity: 0.05; +} +.swagger-ui .o-025 { + opacity: 0.025; +} +.swagger-ui .o-0 { + opacity: 0; +} +.swagger-ui .rotate-45 { + transform: rotate(45deg); +} +.swagger-ui .rotate-90 { + transform: rotate(90deg); +} +.swagger-ui .rotate-135 { + transform: rotate(135deg); +} +.swagger-ui .rotate-180 { + transform: rotate(180deg); +} +.swagger-ui .rotate-225 { + transform: rotate(225deg); +} +.swagger-ui .rotate-270 { + transform: rotate(270deg); +} +.swagger-ui .rotate-315 { + transform: rotate(315deg); +} +@media screen and (min-width: 30em) { + .swagger-ui .rotate-45-ns { + transform: rotate(45deg); + } + .swagger-ui .rotate-90-ns { + transform: rotate(90deg); + } + .swagger-ui .rotate-135-ns { + transform: rotate(135deg); + } + .swagger-ui .rotate-180-ns { + transform: rotate(180deg); + } + .swagger-ui .rotate-225-ns { + transform: rotate(225deg); + } + .swagger-ui .rotate-270-ns { + transform: rotate(270deg); + } + .swagger-ui .rotate-315-ns { + transform: rotate(315deg); + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .rotate-45-m { + transform: rotate(45deg); + } + .swagger-ui .rotate-90-m { + transform: rotate(90deg); + } + .swagger-ui .rotate-135-m { + transform: rotate(135deg); + } + .swagger-ui .rotate-180-m { + transform: rotate(180deg); + } + .swagger-ui .rotate-225-m { + transform: rotate(225deg); + } + .swagger-ui .rotate-270-m { + transform: rotate(270deg); + } + .swagger-ui .rotate-315-m { + transform: rotate(315deg); + } +} +@media screen and (min-width: 60em) { + .swagger-ui .rotate-45-l { + transform: rotate(45deg); + } + .swagger-ui .rotate-90-l { + transform: rotate(90deg); + } + .swagger-ui .rotate-135-l { + transform: rotate(135deg); + } + .swagger-ui .rotate-180-l { + transform: rotate(180deg); + } + .swagger-ui .rotate-225-l { + transform: rotate(225deg); + } + .swagger-ui .rotate-270-l { + transform: rotate(270deg); + } + .swagger-ui .rotate-315-l { + transform: rotate(315deg); + } +} +.swagger-ui .black-90 { + color: rgba(0, 0, 0, 0.9); +} +.swagger-ui .black-80 { + color: rgba(0, 0, 0, 0.8); +} +.swagger-ui .black-70 { + color: rgba(0, 0, 0, 0.7); +} +.swagger-ui .black-60 { + color: rgba(0, 0, 0, 0.6); +} +.swagger-ui .black-50 { + color: rgba(0, 0, 0, 0.5); +} +.swagger-ui .black-40 { + color: rgba(0, 0, 0, 0.4); +} +.swagger-ui .black-30 { + color: rgba(0, 0, 0, 0.3); +} +.swagger-ui .black-20 { + color: rgba(0, 0, 0, 0.2); +} +.swagger-ui .black-10 { + color: rgba(0, 0, 0, 0.1); +} +.swagger-ui .black-05 { + color: rgba(0, 0, 0, 0.05); +} +.swagger-ui .white-90 { + color: hsla(0, 0%, 100%, 0.9); +} +.swagger-ui .white-80 { + color: hsla(0, 0%, 100%, 0.8); +} +.swagger-ui .white-70 { + color: hsla(0, 0%, 100%, 0.7); +} +.swagger-ui .white-60 { + color: hsla(0, 0%, 100%, 0.6); +} +.swagger-ui .white-50 { + color: hsla(0, 0%, 100%, 0.5); +} +.swagger-ui .white-40 { + color: hsla(0, 0%, 100%, 0.4); +} +.swagger-ui .white-30 { + color: hsla(0, 0%, 100%, 0.3); +} +.swagger-ui .white-20 { + color: hsla(0, 0%, 100%, 0.2); +} +.swagger-ui .white-10 { + color: hsla(0, 0%, 100%, 0.1); +} +.swagger-ui .black { + color: #000; +} +.swagger-ui .near-black { + color: #111; +} +.swagger-ui .dark-gray { + color: #333; +} +.swagger-ui .mid-gray { + color: #555; +} +.swagger-ui .gray { + color: #777; +} +.swagger-ui .silver { + color: #999; +} +.swagger-ui .light-silver { + color: #aaa; +} +.swagger-ui .moon-gray { + color: #ccc; +} +.swagger-ui .light-gray { + color: #eee; +} +.swagger-ui .near-white { + color: #f4f4f4; +} +.swagger-ui .white { + color: #fff; +} +.swagger-ui .dark-red { + color: #e7040f; +} +.swagger-ui .red { + color: #ff4136; +} +.swagger-ui .light-red { + color: #ff725c; +} +.swagger-ui .orange { + color: #ff6300; +} +.swagger-ui .gold { + color: #ffb700; +} +.swagger-ui .yellow { + color: gold; +} +.swagger-ui .light-yellow { + color: #fbf1a9; +} +.swagger-ui .purple { + color: #5e2ca5; +} +.swagger-ui .light-purple { + color: #a463f2; +} +.swagger-ui .dark-pink { + color: #d5008f; +} +.swagger-ui .hot-pink { + color: #ff41b4; +} +.swagger-ui .pink { + color: #ff80cc; +} +.swagger-ui .light-pink { + color: #ffa3d7; +} +.swagger-ui .dark-green { + color: #137752; +} +.swagger-ui .green { + color: #19a974; +} +.swagger-ui .light-green { + color: #9eebcf; +} +.swagger-ui .navy { + color: #001b44; +} +.swagger-ui .dark-blue { + color: #00449e; +} +.swagger-ui .blue { + color: #357edd; +} +.swagger-ui .light-blue { + color: #96ccff; +} +.swagger-ui .lightest-blue { + color: #cdecff; +} +.swagger-ui .washed-blue { + color: #f6fffe; +} +.swagger-ui .washed-green { + color: #e8fdf5; +} +.swagger-ui .washed-yellow { + color: #fffceb; +} +.swagger-ui .washed-red { + color: #ffdfdf; +} +.swagger-ui .color-inherit { + color: inherit; +} +.swagger-ui .bg-black-90 { + background-color: rgba(0, 0, 0, 0.9); +} +.swagger-ui .bg-black-80 { + background-color: rgba(0, 0, 0, 0.8); +} +.swagger-ui .bg-black-70 { + background-color: rgba(0, 0, 0, 0.7); +} +.swagger-ui .bg-black-60 { + background-color: rgba(0, 0, 0, 0.6); +} +.swagger-ui .bg-black-50 { + background-color: rgba(0, 0, 0, 0.5); +} +.swagger-ui .bg-black-40 { + background-color: rgba(0, 0, 0, 0.4); +} +.swagger-ui .bg-black-30 { + background-color: rgba(0, 0, 0, 0.3); +} +.swagger-ui .bg-black-20 { + background-color: rgba(0, 0, 0, 0.2); +} +.swagger-ui .bg-black-10 { + background-color: rgba(0, 0, 0, 0.1); +} +.swagger-ui .bg-black-05 { + background-color: rgba(0, 0, 0, 0.05); +} +.swagger-ui .bg-white-90 { + background-color: hsla(0, 0%, 100%, 0.9); +} +.swagger-ui .bg-white-80 { + background-color: hsla(0, 0%, 100%, 0.8); +} +.swagger-ui .bg-white-70 { + background-color: hsla(0, 0%, 100%, 0.7); +} +.swagger-ui .bg-white-60 { + background-color: hsla(0, 0%, 100%, 0.6); +} +.swagger-ui .bg-white-50 { + background-color: hsla(0, 0%, 100%, 0.5); +} +.swagger-ui .bg-white-40 { + background-color: hsla(0, 0%, 100%, 0.4); +} +.swagger-ui .bg-white-30 { + background-color: hsla(0, 0%, 100%, 0.3); +} +.swagger-ui .bg-white-20 { + background-color: hsla(0, 0%, 100%, 0.2); +} +.swagger-ui .bg-white-10 { + background-color: hsla(0, 0%, 100%, 0.1); +} +.swagger-ui .bg-black { + background-color: #000; +} +.swagger-ui .bg-near-black { + background-color: #111; +} +.swagger-ui .bg-dark-gray { + background-color: #333; +} +.swagger-ui .bg-mid-gray { + background-color: #555; +} +.swagger-ui .bg-gray { + background-color: #777; +} +.swagger-ui .bg-silver { + background-color: #999; +} +.swagger-ui .bg-light-silver { + background-color: #aaa; +} +.swagger-ui .bg-moon-gray { + background-color: #ccc; +} +.swagger-ui .bg-light-gray { + background-color: #eee; +} +.swagger-ui .bg-near-white { + background-color: #f4f4f4; +} +.swagger-ui .bg-white { + background-color: #fff; +} +.swagger-ui .bg-transparent { + background-color: transparent; +} +.swagger-ui .bg-dark-red { + background-color: #e7040f; +} +.swagger-ui .bg-red { + background-color: #ff4136; +} +.swagger-ui .bg-light-red { + background-color: #ff725c; +} +.swagger-ui .bg-orange { + background-color: #ff6300; +} +.swagger-ui .bg-gold { + background-color: #ffb700; +} +.swagger-ui .bg-yellow { + background-color: gold; +} +.swagger-ui .bg-light-yellow { + background-color: #fbf1a9; +} +.swagger-ui .bg-purple { + background-color: #5e2ca5; +} +.swagger-ui .bg-light-purple { + background-color: #a463f2; +} +.swagger-ui .bg-dark-pink { + background-color: #d5008f; +} +.swagger-ui .bg-hot-pink { + background-color: #ff41b4; +} +.swagger-ui .bg-pink { + background-color: #ff80cc; +} +.swagger-ui .bg-light-pink { + background-color: #ffa3d7; +} +.swagger-ui .bg-dark-green { + background-color: #137752; +} +.swagger-ui .bg-green { + background-color: #19a974; +} +.swagger-ui .bg-light-green { + background-color: #9eebcf; +} +.swagger-ui .bg-navy { + background-color: #001b44; +} +.swagger-ui .bg-dark-blue { + background-color: #00449e; +} +.swagger-ui .bg-blue { + background-color: #357edd; +} +.swagger-ui .bg-light-blue { + background-color: #96ccff; +} +.swagger-ui .bg-lightest-blue { + background-color: #cdecff; +} +.swagger-ui .bg-washed-blue { + background-color: #f6fffe; +} +.swagger-ui .bg-washed-green { + background-color: #e8fdf5; +} +.swagger-ui .bg-washed-yellow { + background-color: #fffceb; +} +.swagger-ui .bg-washed-red { + background-color: #ffdfdf; +} +.swagger-ui .bg-inherit { + background-color: inherit; +} +.swagger-ui .hover-black:focus, +.swagger-ui .hover-black:hover { + color: #000; +} +.swagger-ui .hover-near-black:focus, +.swagger-ui .hover-near-black:hover { + color: #111; +} +.swagger-ui .hover-dark-gray:focus, +.swagger-ui .hover-dark-gray:hover { + color: #333; +} +.swagger-ui .hover-mid-gray:focus, +.swagger-ui .hover-mid-gray:hover { + color: #555; +} +.swagger-ui .hover-gray:focus, +.swagger-ui .hover-gray:hover { + color: #777; +} +.swagger-ui .hover-silver:focus, +.swagger-ui .hover-silver:hover { + color: #999; +} +.swagger-ui .hover-light-silver:focus, +.swagger-ui .hover-light-silver:hover { + color: #aaa; +} +.swagger-ui .hover-moon-gray:focus, +.swagger-ui .hover-moon-gray:hover { + color: #ccc; +} +.swagger-ui .hover-light-gray:focus, +.swagger-ui .hover-light-gray:hover { + color: #eee; +} +.swagger-ui .hover-near-white:focus, +.swagger-ui .hover-near-white:hover { + color: #f4f4f4; +} +.swagger-ui .hover-white:focus, +.swagger-ui .hover-white:hover { + color: #fff; +} +.swagger-ui .hover-black-90:focus, +.swagger-ui .hover-black-90:hover { + color: rgba(0, 0, 0, 0.9); +} +.swagger-ui .hover-black-80:focus, +.swagger-ui .hover-black-80:hover { + color: rgba(0, 0, 0, 0.8); +} +.swagger-ui .hover-black-70:focus, +.swagger-ui .hover-black-70:hover { + color: rgba(0, 0, 0, 0.7); +} +.swagger-ui .hover-black-60:focus, +.swagger-ui .hover-black-60:hover { + color: rgba(0, 0, 0, 0.6); +} +.swagger-ui .hover-black-50:focus, +.swagger-ui .hover-black-50:hover { + color: rgba(0, 0, 0, 0.5); +} +.swagger-ui .hover-black-40:focus, +.swagger-ui .hover-black-40:hover { + color: rgba(0, 0, 0, 0.4); +} +.swagger-ui .hover-black-30:focus, +.swagger-ui .hover-black-30:hover { + color: rgba(0, 0, 0, 0.3); +} +.swagger-ui .hover-black-20:focus, +.swagger-ui .hover-black-20:hover { + color: rgba(0, 0, 0, 0.2); +} +.swagger-ui .hover-black-10:focus, +.swagger-ui .hover-black-10:hover { + color: rgba(0, 0, 0, 0.1); +} +.swagger-ui .hover-white-90:focus, +.swagger-ui .hover-white-90:hover { + color: hsla(0, 0%, 100%, 0.9); +} +.swagger-ui .hover-white-80:focus, +.swagger-ui .hover-white-80:hover { + color: hsla(0, 0%, 100%, 0.8); +} +.swagger-ui .hover-white-70:focus, +.swagger-ui .hover-white-70:hover { + color: hsla(0, 0%, 100%, 0.7); +} +.swagger-ui .hover-white-60:focus, +.swagger-ui .hover-white-60:hover { + color: hsla(0, 0%, 100%, 0.6); +} +.swagger-ui .hover-white-50:focus, +.swagger-ui .hover-white-50:hover { + color: hsla(0, 0%, 100%, 0.5); +} +.swagger-ui .hover-white-40:focus, +.swagger-ui .hover-white-40:hover { + color: hsla(0, 0%, 100%, 0.4); +} +.swagger-ui .hover-white-30:focus, +.swagger-ui .hover-white-30:hover { + color: hsla(0, 0%, 100%, 0.3); +} +.swagger-ui .hover-white-20:focus, +.swagger-ui .hover-white-20:hover { + color: hsla(0, 0%, 100%, 0.2); +} +.swagger-ui .hover-white-10:focus, +.swagger-ui .hover-white-10:hover { + color: hsla(0, 0%, 100%, 0.1); +} +.swagger-ui .hover-inherit:focus, +.swagger-ui .hover-inherit:hover { + color: inherit; +} +.swagger-ui .hover-bg-black:focus, +.swagger-ui .hover-bg-black:hover { + background-color: #000; +} +.swagger-ui .hover-bg-near-black:focus, +.swagger-ui .hover-bg-near-black:hover { + background-color: #111; +} +.swagger-ui .hover-bg-dark-gray:focus, +.swagger-ui .hover-bg-dark-gray:hover { + background-color: #333; +} +.swagger-ui .hover-bg-mid-gray:focus, +.swagger-ui .hover-bg-mid-gray:hover { + background-color: #555; +} +.swagger-ui .hover-bg-gray:focus, +.swagger-ui .hover-bg-gray:hover { + background-color: #777; +} +.swagger-ui .hover-bg-silver:focus, +.swagger-ui .hover-bg-silver:hover { + background-color: #999; +} +.swagger-ui .hover-bg-light-silver:focus, +.swagger-ui .hover-bg-light-silver:hover { + background-color: #aaa; +} +.swagger-ui .hover-bg-moon-gray:focus, +.swagger-ui .hover-bg-moon-gray:hover { + background-color: #ccc; +} +.swagger-ui .hover-bg-light-gray:focus, +.swagger-ui .hover-bg-light-gray:hover { + background-color: #eee; +} +.swagger-ui .hover-bg-near-white:focus, +.swagger-ui .hover-bg-near-white:hover { + background-color: #f4f4f4; +} +.swagger-ui .hover-bg-white:focus, +.swagger-ui .hover-bg-white:hover { + background-color: #fff; +} +.swagger-ui .hover-bg-transparent:focus, +.swagger-ui .hover-bg-transparent:hover { + background-color: transparent; +} +.swagger-ui .hover-bg-black-90:focus, +.swagger-ui .hover-bg-black-90:hover { + background-color: rgba(0, 0, 0, 0.9); +} +.swagger-ui .hover-bg-black-80:focus, +.swagger-ui .hover-bg-black-80:hover { + background-color: rgba(0, 0, 0, 0.8); +} +.swagger-ui .hover-bg-black-70:focus, +.swagger-ui .hover-bg-black-70:hover { + background-color: rgba(0, 0, 0, 0.7); +} +.swagger-ui .hover-bg-black-60:focus, +.swagger-ui .hover-bg-black-60:hover { + background-color: rgba(0, 0, 0, 0.6); +} +.swagger-ui .hover-bg-black-50:focus, +.swagger-ui .hover-bg-black-50:hover { + background-color: rgba(0, 0, 0, 0.5); +} +.swagger-ui .hover-bg-black-40:focus, +.swagger-ui .hover-bg-black-40:hover { + background-color: rgba(0, 0, 0, 0.4); +} +.swagger-ui .hover-bg-black-30:focus, +.swagger-ui .hover-bg-black-30:hover { + background-color: rgba(0, 0, 0, 0.3); +} +.swagger-ui .hover-bg-black-20:focus, +.swagger-ui .hover-bg-black-20:hover { + background-color: rgba(0, 0, 0, 0.2); +} +.swagger-ui .hover-bg-black-10:focus, +.swagger-ui .hover-bg-black-10:hover { + background-color: rgba(0, 0, 0, 0.1); +} +.swagger-ui .hover-bg-white-90:focus, +.swagger-ui .hover-bg-white-90:hover { + background-color: hsla(0, 0%, 100%, 0.9); +} +.swagger-ui .hover-bg-white-80:focus, +.swagger-ui .hover-bg-white-80:hover { + background-color: hsla(0, 0%, 100%, 0.8); +} +.swagger-ui .hover-bg-white-70:focus, +.swagger-ui .hover-bg-white-70:hover { + background-color: hsla(0, 0%, 100%, 0.7); +} +.swagger-ui .hover-bg-white-60:focus, +.swagger-ui .hover-bg-white-60:hover { + background-color: hsla(0, 0%, 100%, 0.6); +} +.swagger-ui .hover-bg-white-50:focus, +.swagger-ui .hover-bg-white-50:hover { + background-color: hsla(0, 0%, 100%, 0.5); +} +.swagger-ui .hover-bg-white-40:focus, +.swagger-ui .hover-bg-white-40:hover { + background-color: hsla(0, 0%, 100%, 0.4); +} +.swagger-ui .hover-bg-white-30:focus, +.swagger-ui .hover-bg-white-30:hover { + background-color: hsla(0, 0%, 100%, 0.3); +} +.swagger-ui .hover-bg-white-20:focus, +.swagger-ui .hover-bg-white-20:hover { + background-color: hsla(0, 0%, 100%, 0.2); +} +.swagger-ui .hover-bg-white-10:focus, +.swagger-ui .hover-bg-white-10:hover { + background-color: hsla(0, 0%, 100%, 0.1); +} +.swagger-ui .hover-dark-red:focus, +.swagger-ui .hover-dark-red:hover { + color: #e7040f; +} +.swagger-ui .hover-red:focus, +.swagger-ui .hover-red:hover { + color: #ff4136; +} +.swagger-ui .hover-light-red:focus, +.swagger-ui .hover-light-red:hover { + color: #ff725c; +} +.swagger-ui .hover-orange:focus, +.swagger-ui .hover-orange:hover { + color: #ff6300; +} +.swagger-ui .hover-gold:focus, +.swagger-ui .hover-gold:hover { + color: #ffb700; +} +.swagger-ui .hover-yellow:focus, +.swagger-ui .hover-yellow:hover { + color: gold; +} +.swagger-ui .hover-light-yellow:focus, +.swagger-ui .hover-light-yellow:hover { + color: #fbf1a9; +} +.swagger-ui .hover-purple:focus, +.swagger-ui .hover-purple:hover { + color: #5e2ca5; +} +.swagger-ui .hover-light-purple:focus, +.swagger-ui .hover-light-purple:hover { + color: #a463f2; +} +.swagger-ui .hover-dark-pink:focus, +.swagger-ui .hover-dark-pink:hover { + color: #d5008f; +} +.swagger-ui .hover-hot-pink:focus, +.swagger-ui .hover-hot-pink:hover { + color: #ff41b4; +} +.swagger-ui .hover-pink:focus, +.swagger-ui .hover-pink:hover { + color: #ff80cc; +} +.swagger-ui .hover-light-pink:focus, +.swagger-ui .hover-light-pink:hover { + color: #ffa3d7; +} +.swagger-ui .hover-dark-green:focus, +.swagger-ui .hover-dark-green:hover { + color: #137752; +} +.swagger-ui .hover-green:focus, +.swagger-ui .hover-green:hover { + color: #19a974; +} +.swagger-ui .hover-light-green:focus, +.swagger-ui .hover-light-green:hover { + color: #9eebcf; +} +.swagger-ui .hover-navy:focus, +.swagger-ui .hover-navy:hover { + color: #001b44; +} +.swagger-ui .hover-dark-blue:focus, +.swagger-ui .hover-dark-blue:hover { + color: #00449e; +} +.swagger-ui .hover-blue:focus, +.swagger-ui .hover-blue:hover { + color: #357edd; +} +.swagger-ui .hover-light-blue:focus, +.swagger-ui .hover-light-blue:hover { + color: #96ccff; +} +.swagger-ui .hover-lightest-blue:focus, +.swagger-ui .hover-lightest-blue:hover { + color: #cdecff; +} +.swagger-ui .hover-washed-blue:focus, +.swagger-ui .hover-washed-blue:hover { + color: #f6fffe; +} +.swagger-ui .hover-washed-green:focus, +.swagger-ui .hover-washed-green:hover { + color: #e8fdf5; +} +.swagger-ui .hover-washed-yellow:focus, +.swagger-ui .hover-washed-yellow:hover { + color: #fffceb; +} +.swagger-ui .hover-washed-red:focus, +.swagger-ui .hover-washed-red:hover { + color: #ffdfdf; +} +.swagger-ui .hover-bg-dark-red:focus, +.swagger-ui .hover-bg-dark-red:hover { + background-color: #e7040f; +} +.swagger-ui .hover-bg-red:focus, +.swagger-ui .hover-bg-red:hover { + background-color: #ff4136; +} +.swagger-ui .hover-bg-light-red:focus, +.swagger-ui .hover-bg-light-red:hover { + background-color: #ff725c; +} +.swagger-ui .hover-bg-orange:focus, +.swagger-ui .hover-bg-orange:hover { + background-color: #ff6300; +} +.swagger-ui .hover-bg-gold:focus, +.swagger-ui .hover-bg-gold:hover { + background-color: #ffb700; +} +.swagger-ui .hover-bg-yellow:focus, +.swagger-ui .hover-bg-yellow:hover { + background-color: gold; +} +.swagger-ui .hover-bg-light-yellow:focus, +.swagger-ui .hover-bg-light-yellow:hover { + background-color: #fbf1a9; +} +.swagger-ui .hover-bg-purple:focus, +.swagger-ui .hover-bg-purple:hover { + background-color: #5e2ca5; +} +.swagger-ui .hover-bg-light-purple:focus, +.swagger-ui .hover-bg-light-purple:hover { + background-color: #a463f2; +} +.swagger-ui .hover-bg-dark-pink:focus, +.swagger-ui .hover-bg-dark-pink:hover { + background-color: #d5008f; +} +.swagger-ui .hover-bg-hot-pink:focus, +.swagger-ui .hover-bg-hot-pink:hover { + background-color: #ff41b4; +} +.swagger-ui .hover-bg-pink:focus, +.swagger-ui .hover-bg-pink:hover { + background-color: #ff80cc; +} +.swagger-ui .hover-bg-light-pink:focus, +.swagger-ui .hover-bg-light-pink:hover { + background-color: #ffa3d7; +} +.swagger-ui .hover-bg-dark-green:focus, +.swagger-ui .hover-bg-dark-green:hover { + background-color: #137752; +} +.swagger-ui .hover-bg-green:focus, +.swagger-ui .hover-bg-green:hover { + background-color: #19a974; +} +.swagger-ui .hover-bg-light-green:focus, +.swagger-ui .hover-bg-light-green:hover { + background-color: #9eebcf; +} +.swagger-ui .hover-bg-navy:focus, +.swagger-ui .hover-bg-navy:hover { + background-color: #001b44; +} +.swagger-ui .hover-bg-dark-blue:focus, +.swagger-ui .hover-bg-dark-blue:hover { + background-color: #00449e; +} +.swagger-ui .hover-bg-blue:focus, +.swagger-ui .hover-bg-blue:hover { + background-color: #357edd; +} +.swagger-ui .hover-bg-light-blue:focus, +.swagger-ui .hover-bg-light-blue:hover { + background-color: #96ccff; +} +.swagger-ui .hover-bg-lightest-blue:focus, +.swagger-ui .hover-bg-lightest-blue:hover { + background-color: #cdecff; +} +.swagger-ui .hover-bg-washed-blue:focus, +.swagger-ui .hover-bg-washed-blue:hover { + background-color: #f6fffe; +} +.swagger-ui .hover-bg-washed-green:focus, +.swagger-ui .hover-bg-washed-green:hover { + background-color: #e8fdf5; +} +.swagger-ui .hover-bg-washed-yellow:focus, +.swagger-ui .hover-bg-washed-yellow:hover { + background-color: #fffceb; +} +.swagger-ui .hover-bg-washed-red:focus, +.swagger-ui .hover-bg-washed-red:hover { + background-color: #ffdfdf; +} +.swagger-ui .hover-bg-inherit:focus, +.swagger-ui .hover-bg-inherit:hover { + background-color: inherit; +} +.swagger-ui .pa0 { + padding: 0; +} +.swagger-ui .pa1 { + padding: 0.25rem; +} +.swagger-ui .pa2 { + padding: 0.5rem; +} +.swagger-ui .pa3 { + padding: 1rem; +} +.swagger-ui .pa4 { + padding: 2rem; +} +.swagger-ui .pa5 { + padding: 4rem; +} +.swagger-ui .pa6 { + padding: 8rem; +} +.swagger-ui .pa7 { + padding: 16rem; +} +.swagger-ui .pl0 { + padding-left: 0; +} +.swagger-ui .pl1 { + padding-left: 0.25rem; +} +.swagger-ui .pl2 { + padding-left: 0.5rem; +} +.swagger-ui .pl3 { + padding-left: 1rem; +} +.swagger-ui .pl4 { + padding-left: 2rem; +} +.swagger-ui .pl5 { + padding-left: 4rem; +} +.swagger-ui .pl6 { + padding-left: 8rem; +} +.swagger-ui .pl7 { + padding-left: 16rem; +} +.swagger-ui .pr0 { + padding-right: 0; +} +.swagger-ui .pr1 { + padding-right: 0.25rem; +} +.swagger-ui .pr2 { + padding-right: 0.5rem; +} +.swagger-ui .pr3 { + padding-right: 1rem; +} +.swagger-ui .pr4 { + padding-right: 2rem; +} +.swagger-ui .pr5 { + padding-right: 4rem; +} +.swagger-ui .pr6 { + padding-right: 8rem; +} +.swagger-ui .pr7 { + padding-right: 16rem; +} +.swagger-ui .pb0 { + padding-bottom: 0; +} +.swagger-ui .pb1 { + padding-bottom: 0.25rem; +} +.swagger-ui .pb2 { + padding-bottom: 0.5rem; +} +.swagger-ui .pb3 { + padding-bottom: 1rem; +} +.swagger-ui .pb4 { + padding-bottom: 2rem; +} +.swagger-ui .pb5 { + padding-bottom: 4rem; +} +.swagger-ui .pb6 { + padding-bottom: 8rem; +} +.swagger-ui .pb7 { + padding-bottom: 16rem; +} +.swagger-ui .pt0 { + padding-top: 0; +} +.swagger-ui .pt1 { + padding-top: 0.25rem; +} +.swagger-ui .pt2 { + padding-top: 0.5rem; +} +.swagger-ui .pt3 { + padding-top: 1rem; +} +.swagger-ui .pt4 { + padding-top: 2rem; +} +.swagger-ui .pt5 { + padding-top: 4rem; +} +.swagger-ui .pt6 { + padding-top: 8rem; +} +.swagger-ui .pt7 { + padding-top: 16rem; +} +.swagger-ui .pv0 { + padding-bottom: 0; + padding-top: 0; +} +.swagger-ui .pv1 { + padding-bottom: 0.25rem; + padding-top: 0.25rem; +} +.swagger-ui .pv2 { + padding-bottom: 0.5rem; + padding-top: 0.5rem; +} +.swagger-ui .pv3 { + padding-bottom: 1rem; + padding-top: 1rem; +} +.swagger-ui .pv4 { + padding-bottom: 2rem; + padding-top: 2rem; +} +.swagger-ui .pv5 { + padding-bottom: 4rem; + padding-top: 4rem; +} +.swagger-ui .pv6 { + padding-bottom: 8rem; + padding-top: 8rem; +} +.swagger-ui .pv7 { + padding-bottom: 16rem; + padding-top: 16rem; +} +.swagger-ui .ph0 { + padding-left: 0; + padding-right: 0; +} +.swagger-ui .ph1 { + padding-left: 0.25rem; + padding-right: 0.25rem; +} +.swagger-ui .ph2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} +.swagger-ui .ph3 { + padding-left: 1rem; + padding-right: 1rem; +} +.swagger-ui .ph4 { + padding-left: 2rem; + padding-right: 2rem; +} +.swagger-ui .ph5 { + padding-left: 4rem; + padding-right: 4rem; +} +.swagger-ui .ph6 { + padding-left: 8rem; + padding-right: 8rem; +} +.swagger-ui .ph7 { + padding-left: 16rem; + padding-right: 16rem; +} +.swagger-ui .ma0 { + margin: 0; +} +.swagger-ui .ma1 { + margin: 0.25rem; +} +.swagger-ui .ma2 { + margin: 0.5rem; +} +.swagger-ui .ma3 { + margin: 1rem; +} +.swagger-ui .ma4 { + margin: 2rem; +} +.swagger-ui .ma5 { + margin: 4rem; +} +.swagger-ui .ma6 { + margin: 8rem; +} +.swagger-ui .ma7 { + margin: 16rem; +} +.swagger-ui .ml0 { + margin-left: 0; +} +.swagger-ui .ml1 { + margin-left: 0.25rem; +} +.swagger-ui .ml2 { + margin-left: 0.5rem; +} +.swagger-ui .ml3 { + margin-left: 1rem; +} +.swagger-ui .ml4 { + margin-left: 2rem; +} +.swagger-ui .ml5 { + margin-left: 4rem; +} +.swagger-ui .ml6 { + margin-left: 8rem; +} +.swagger-ui .ml7 { + margin-left: 16rem; +} +.swagger-ui .mr0 { + margin-right: 0; +} +.swagger-ui .mr1 { + margin-right: 0.25rem; +} +.swagger-ui .mr2 { + margin-right: 0.5rem; +} +.swagger-ui .mr3 { + margin-right: 1rem; +} +.swagger-ui .mr4 { + margin-right: 2rem; +} +.swagger-ui .mr5 { + margin-right: 4rem; +} +.swagger-ui .mr6 { + margin-right: 8rem; +} +.swagger-ui .mr7 { + margin-right: 16rem; +} +.swagger-ui .mb0 { + margin-bottom: 0; +} +.swagger-ui .mb1 { + margin-bottom: 0.25rem; +} +.swagger-ui .mb2 { + margin-bottom: 0.5rem; +} +.swagger-ui .mb3 { + margin-bottom: 1rem; +} +.swagger-ui .mb4 { + margin-bottom: 2rem; +} +.swagger-ui .mb5 { + margin-bottom: 4rem; +} +.swagger-ui .mb6 { + margin-bottom: 8rem; +} +.swagger-ui .mb7 { + margin-bottom: 16rem; +} +.swagger-ui .mt0 { + margin-top: 0; +} +.swagger-ui .mt1 { + margin-top: 0.25rem; +} +.swagger-ui .mt2 { + margin-top: 0.5rem; +} +.swagger-ui .mt3 { + margin-top: 1rem; +} +.swagger-ui .mt4 { + margin-top: 2rem; +} +.swagger-ui .mt5 { + margin-top: 4rem; +} +.swagger-ui .mt6 { + margin-top: 8rem; +} +.swagger-ui .mt7 { + margin-top: 16rem; +} +.swagger-ui .mv0 { + margin-bottom: 0; + margin-top: 0; +} +.swagger-ui .mv1 { + margin-bottom: 0.25rem; + margin-top: 0.25rem; +} +.swagger-ui .mv2 { + margin-bottom: 0.5rem; + margin-top: 0.5rem; +} +.swagger-ui .mv3 { + margin-bottom: 1rem; + margin-top: 1rem; +} +.swagger-ui .mv4 { + margin-bottom: 2rem; + margin-top: 2rem; +} +.swagger-ui .mv5 { + margin-bottom: 4rem; + margin-top: 4rem; +} +.swagger-ui .mv6 { + margin-bottom: 8rem; + margin-top: 8rem; +} +.swagger-ui .mv7 { + margin-bottom: 16rem; + margin-top: 16rem; +} +.swagger-ui .mh0 { + margin-left: 0; + margin-right: 0; +} +.swagger-ui .mh1 { + margin-left: 0.25rem; + margin-right: 0.25rem; +} +.swagger-ui .mh2 { + margin-left: 0.5rem; + margin-right: 0.5rem; +} +.swagger-ui .mh3 { + margin-left: 1rem; + margin-right: 1rem; +} +.swagger-ui .mh4 { + margin-left: 2rem; + margin-right: 2rem; +} +.swagger-ui .mh5 { + margin-left: 4rem; + margin-right: 4rem; +} +.swagger-ui .mh6 { + margin-left: 8rem; + margin-right: 8rem; +} +.swagger-ui .mh7 { + margin-left: 16rem; + margin-right: 16rem; +} +@media screen and (min-width: 30em) { + .swagger-ui .pa0-ns { + padding: 0; + } + .swagger-ui .pa1-ns { + padding: 0.25rem; + } + .swagger-ui .pa2-ns { + padding: 0.5rem; + } + .swagger-ui .pa3-ns { + padding: 1rem; + } + .swagger-ui .pa4-ns { + padding: 2rem; + } + .swagger-ui .pa5-ns { + padding: 4rem; + } + .swagger-ui .pa6-ns { + padding: 8rem; + } + .swagger-ui .pa7-ns { + padding: 16rem; + } + .swagger-ui .pl0-ns { + padding-left: 0; + } + .swagger-ui .pl1-ns { + padding-left: 0.25rem; + } + .swagger-ui .pl2-ns { + padding-left: 0.5rem; + } + .swagger-ui .pl3-ns { + padding-left: 1rem; + } + .swagger-ui .pl4-ns { + padding-left: 2rem; + } + .swagger-ui .pl5-ns { + padding-left: 4rem; + } + .swagger-ui .pl6-ns { + padding-left: 8rem; + } + .swagger-ui .pl7-ns { + padding-left: 16rem; + } + .swagger-ui .pr0-ns { + padding-right: 0; + } + .swagger-ui .pr1-ns { + padding-right: 0.25rem; + } + .swagger-ui .pr2-ns { + padding-right: 0.5rem; + } + .swagger-ui .pr3-ns { + padding-right: 1rem; + } + .swagger-ui .pr4-ns { + padding-right: 2rem; + } + .swagger-ui .pr5-ns { + padding-right: 4rem; + } + .swagger-ui .pr6-ns { + padding-right: 8rem; + } + .swagger-ui .pr7-ns { + padding-right: 16rem; + } + .swagger-ui .pb0-ns { + padding-bottom: 0; + } + .swagger-ui .pb1-ns { + padding-bottom: 0.25rem; + } + .swagger-ui .pb2-ns { + padding-bottom: 0.5rem; + } + .swagger-ui .pb3-ns { + padding-bottom: 1rem; + } + .swagger-ui .pb4-ns { + padding-bottom: 2rem; + } + .swagger-ui .pb5-ns { + padding-bottom: 4rem; + } + .swagger-ui .pb6-ns { + padding-bottom: 8rem; + } + .swagger-ui .pb7-ns { + padding-bottom: 16rem; + } + .swagger-ui .pt0-ns { + padding-top: 0; + } + .swagger-ui .pt1-ns { + padding-top: 0.25rem; + } + .swagger-ui .pt2-ns { + padding-top: 0.5rem; + } + .swagger-ui .pt3-ns { + padding-top: 1rem; + } + .swagger-ui .pt4-ns { + padding-top: 2rem; + } + .swagger-ui .pt5-ns { + padding-top: 4rem; + } + .swagger-ui .pt6-ns { + padding-top: 8rem; + } + .swagger-ui .pt7-ns { + padding-top: 16rem; + } + .swagger-ui .pv0-ns { + padding-bottom: 0; + padding-top: 0; + } + .swagger-ui .pv1-ns { + padding-bottom: 0.25rem; + padding-top: 0.25rem; + } + .swagger-ui .pv2-ns { + padding-bottom: 0.5rem; + padding-top: 0.5rem; + } + .swagger-ui .pv3-ns { + padding-bottom: 1rem; + padding-top: 1rem; + } + .swagger-ui .pv4-ns { + padding-bottom: 2rem; + padding-top: 2rem; + } + .swagger-ui .pv5-ns { + padding-bottom: 4rem; + padding-top: 4rem; + } + .swagger-ui .pv6-ns { + padding-bottom: 8rem; + padding-top: 8rem; + } + .swagger-ui .pv7-ns { + padding-bottom: 16rem; + padding-top: 16rem; + } + .swagger-ui .ph0-ns { + padding-left: 0; + padding-right: 0; + } + .swagger-ui .ph1-ns { + padding-left: 0.25rem; + padding-right: 0.25rem; + } + .swagger-ui .ph2-ns { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + .swagger-ui .ph3-ns { + padding-left: 1rem; + padding-right: 1rem; + } + .swagger-ui .ph4-ns { + padding-left: 2rem; + padding-right: 2rem; + } + .swagger-ui .ph5-ns { + padding-left: 4rem; + padding-right: 4rem; + } + .swagger-ui .ph6-ns { + padding-left: 8rem; + padding-right: 8rem; + } + .swagger-ui .ph7-ns { + padding-left: 16rem; + padding-right: 16rem; + } + .swagger-ui .ma0-ns { + margin: 0; + } + .swagger-ui .ma1-ns { + margin: 0.25rem; + } + .swagger-ui .ma2-ns { + margin: 0.5rem; + } + .swagger-ui .ma3-ns { + margin: 1rem; + } + .swagger-ui .ma4-ns { + margin: 2rem; + } + .swagger-ui .ma5-ns { + margin: 4rem; + } + .swagger-ui .ma6-ns { + margin: 8rem; + } + .swagger-ui .ma7-ns { + margin: 16rem; + } + .swagger-ui .ml0-ns { + margin-left: 0; + } + .swagger-ui .ml1-ns { + margin-left: 0.25rem; + } + .swagger-ui .ml2-ns { + margin-left: 0.5rem; + } + .swagger-ui .ml3-ns { + margin-left: 1rem; + } + .swagger-ui .ml4-ns { + margin-left: 2rem; + } + .swagger-ui .ml5-ns { + margin-left: 4rem; + } + .swagger-ui .ml6-ns { + margin-left: 8rem; + } + .swagger-ui .ml7-ns { + margin-left: 16rem; + } + .swagger-ui .mr0-ns { + margin-right: 0; + } + .swagger-ui .mr1-ns { + margin-right: 0.25rem; + } + .swagger-ui .mr2-ns { + margin-right: 0.5rem; + } + .swagger-ui .mr3-ns { + margin-right: 1rem; + } + .swagger-ui .mr4-ns { + margin-right: 2rem; + } + .swagger-ui .mr5-ns { + margin-right: 4rem; + } + .swagger-ui .mr6-ns { + margin-right: 8rem; + } + .swagger-ui .mr7-ns { + margin-right: 16rem; + } + .swagger-ui .mb0-ns { + margin-bottom: 0; + } + .swagger-ui .mb1-ns { + margin-bottom: 0.25rem; + } + .swagger-ui .mb2-ns { + margin-bottom: 0.5rem; + } + .swagger-ui .mb3-ns { + margin-bottom: 1rem; + } + .swagger-ui .mb4-ns { + margin-bottom: 2rem; + } + .swagger-ui .mb5-ns { + margin-bottom: 4rem; + } + .swagger-ui .mb6-ns { + margin-bottom: 8rem; + } + .swagger-ui .mb7-ns { + margin-bottom: 16rem; + } + .swagger-ui .mt0-ns { + margin-top: 0; + } + .swagger-ui .mt1-ns { + margin-top: 0.25rem; + } + .swagger-ui .mt2-ns { + margin-top: 0.5rem; + } + .swagger-ui .mt3-ns { + margin-top: 1rem; + } + .swagger-ui .mt4-ns { + margin-top: 2rem; + } + .swagger-ui .mt5-ns { + margin-top: 4rem; + } + .swagger-ui .mt6-ns { + margin-top: 8rem; + } + .swagger-ui .mt7-ns { + margin-top: 16rem; + } + .swagger-ui .mv0-ns { + margin-bottom: 0; + margin-top: 0; + } + .swagger-ui .mv1-ns { + margin-bottom: 0.25rem; + margin-top: 0.25rem; + } + .swagger-ui .mv2-ns { + margin-bottom: 0.5rem; + margin-top: 0.5rem; + } + .swagger-ui .mv3-ns { + margin-bottom: 1rem; + margin-top: 1rem; + } + .swagger-ui .mv4-ns { + margin-bottom: 2rem; + margin-top: 2rem; + } + .swagger-ui .mv5-ns { + margin-bottom: 4rem; + margin-top: 4rem; + } + .swagger-ui .mv6-ns { + margin-bottom: 8rem; + margin-top: 8rem; + } + .swagger-ui .mv7-ns { + margin-bottom: 16rem; + margin-top: 16rem; + } + .swagger-ui .mh0-ns { + margin-left: 0; + margin-right: 0; + } + .swagger-ui .mh1-ns { + margin-left: 0.25rem; + margin-right: 0.25rem; + } + .swagger-ui .mh2-ns { + margin-left: 0.5rem; + margin-right: 0.5rem; + } + .swagger-ui .mh3-ns { + margin-left: 1rem; + margin-right: 1rem; + } + .swagger-ui .mh4-ns { + margin-left: 2rem; + margin-right: 2rem; + } + .swagger-ui .mh5-ns { + margin-left: 4rem; + margin-right: 4rem; + } + .swagger-ui .mh6-ns { + margin-left: 8rem; + margin-right: 8rem; + } + .swagger-ui .mh7-ns { + margin-left: 16rem; + margin-right: 16rem; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .pa0-m { + padding: 0; + } + .swagger-ui .pa1-m { + padding: 0.25rem; + } + .swagger-ui .pa2-m { + padding: 0.5rem; + } + .swagger-ui .pa3-m { + padding: 1rem; + } + .swagger-ui .pa4-m { + padding: 2rem; + } + .swagger-ui .pa5-m { + padding: 4rem; + } + .swagger-ui .pa6-m { + padding: 8rem; + } + .swagger-ui .pa7-m { + padding: 16rem; + } + .swagger-ui .pl0-m { + padding-left: 0; + } + .swagger-ui .pl1-m { + padding-left: 0.25rem; + } + .swagger-ui .pl2-m { + padding-left: 0.5rem; + } + .swagger-ui .pl3-m { + padding-left: 1rem; + } + .swagger-ui .pl4-m { + padding-left: 2rem; + } + .swagger-ui .pl5-m { + padding-left: 4rem; + } + .swagger-ui .pl6-m { + padding-left: 8rem; + } + .swagger-ui .pl7-m { + padding-left: 16rem; + } + .swagger-ui .pr0-m { + padding-right: 0; + } + .swagger-ui .pr1-m { + padding-right: 0.25rem; + } + .swagger-ui .pr2-m { + padding-right: 0.5rem; + } + .swagger-ui .pr3-m { + padding-right: 1rem; + } + .swagger-ui .pr4-m { + padding-right: 2rem; + } + .swagger-ui .pr5-m { + padding-right: 4rem; + } + .swagger-ui .pr6-m { + padding-right: 8rem; + } + .swagger-ui .pr7-m { + padding-right: 16rem; + } + .swagger-ui .pb0-m { + padding-bottom: 0; + } + .swagger-ui .pb1-m { + padding-bottom: 0.25rem; + } + .swagger-ui .pb2-m { + padding-bottom: 0.5rem; + } + .swagger-ui .pb3-m { + padding-bottom: 1rem; + } + .swagger-ui .pb4-m { + padding-bottom: 2rem; + } + .swagger-ui .pb5-m { + padding-bottom: 4rem; + } + .swagger-ui .pb6-m { + padding-bottom: 8rem; + } + .swagger-ui .pb7-m { + padding-bottom: 16rem; + } + .swagger-ui .pt0-m { + padding-top: 0; + } + .swagger-ui .pt1-m { + padding-top: 0.25rem; + } + .swagger-ui .pt2-m { + padding-top: 0.5rem; + } + .swagger-ui .pt3-m { + padding-top: 1rem; + } + .swagger-ui .pt4-m { + padding-top: 2rem; + } + .swagger-ui .pt5-m { + padding-top: 4rem; + } + .swagger-ui .pt6-m { + padding-top: 8rem; + } + .swagger-ui .pt7-m { + padding-top: 16rem; + } + .swagger-ui .pv0-m { + padding-bottom: 0; + padding-top: 0; + } + .swagger-ui .pv1-m { + padding-bottom: 0.25rem; + padding-top: 0.25rem; + } + .swagger-ui .pv2-m { + padding-bottom: 0.5rem; + padding-top: 0.5rem; + } + .swagger-ui .pv3-m { + padding-bottom: 1rem; + padding-top: 1rem; + } + .swagger-ui .pv4-m { + padding-bottom: 2rem; + padding-top: 2rem; + } + .swagger-ui .pv5-m { + padding-bottom: 4rem; + padding-top: 4rem; + } + .swagger-ui .pv6-m { + padding-bottom: 8rem; + padding-top: 8rem; + } + .swagger-ui .pv7-m { + padding-bottom: 16rem; + padding-top: 16rem; + } + .swagger-ui .ph0-m { + padding-left: 0; + padding-right: 0; + } + .swagger-ui .ph1-m { + padding-left: 0.25rem; + padding-right: 0.25rem; + } + .swagger-ui .ph2-m { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + .swagger-ui .ph3-m { + padding-left: 1rem; + padding-right: 1rem; + } + .swagger-ui .ph4-m { + padding-left: 2rem; + padding-right: 2rem; + } + .swagger-ui .ph5-m { + padding-left: 4rem; + padding-right: 4rem; + } + .swagger-ui .ph6-m { + padding-left: 8rem; + padding-right: 8rem; + } + .swagger-ui .ph7-m { + padding-left: 16rem; + padding-right: 16rem; + } + .swagger-ui .ma0-m { + margin: 0; + } + .swagger-ui .ma1-m { + margin: 0.25rem; + } + .swagger-ui .ma2-m { + margin: 0.5rem; + } + .swagger-ui .ma3-m { + margin: 1rem; + } + .swagger-ui .ma4-m { + margin: 2rem; + } + .swagger-ui .ma5-m { + margin: 4rem; + } + .swagger-ui .ma6-m { + margin: 8rem; + } + .swagger-ui .ma7-m { + margin: 16rem; + } + .swagger-ui .ml0-m { + margin-left: 0; + } + .swagger-ui .ml1-m { + margin-left: 0.25rem; + } + .swagger-ui .ml2-m { + margin-left: 0.5rem; + } + .swagger-ui .ml3-m { + margin-left: 1rem; + } + .swagger-ui .ml4-m { + margin-left: 2rem; + } + .swagger-ui .ml5-m { + margin-left: 4rem; + } + .swagger-ui .ml6-m { + margin-left: 8rem; + } + .swagger-ui .ml7-m { + margin-left: 16rem; + } + .swagger-ui .mr0-m { + margin-right: 0; + } + .swagger-ui .mr1-m { + margin-right: 0.25rem; + } + .swagger-ui .mr2-m { + margin-right: 0.5rem; + } + .swagger-ui .mr3-m { + margin-right: 1rem; + } + .swagger-ui .mr4-m { + margin-right: 2rem; + } + .swagger-ui .mr5-m { + margin-right: 4rem; + } + .swagger-ui .mr6-m { + margin-right: 8rem; + } + .swagger-ui .mr7-m { + margin-right: 16rem; + } + .swagger-ui .mb0-m { + margin-bottom: 0; + } + .swagger-ui .mb1-m { + margin-bottom: 0.25rem; + } + .swagger-ui .mb2-m { + margin-bottom: 0.5rem; + } + .swagger-ui .mb3-m { + margin-bottom: 1rem; + } + .swagger-ui .mb4-m { + margin-bottom: 2rem; + } + .swagger-ui .mb5-m { + margin-bottom: 4rem; + } + .swagger-ui .mb6-m { + margin-bottom: 8rem; + } + .swagger-ui .mb7-m { + margin-bottom: 16rem; + } + .swagger-ui .mt0-m { + margin-top: 0; + } + .swagger-ui .mt1-m { + margin-top: 0.25rem; + } + .swagger-ui .mt2-m { + margin-top: 0.5rem; + } + .swagger-ui .mt3-m { + margin-top: 1rem; + } + .swagger-ui .mt4-m { + margin-top: 2rem; + } + .swagger-ui .mt5-m { + margin-top: 4rem; + } + .swagger-ui .mt6-m { + margin-top: 8rem; + } + .swagger-ui .mt7-m { + margin-top: 16rem; + } + .swagger-ui .mv0-m { + margin-bottom: 0; + margin-top: 0; + } + .swagger-ui .mv1-m { + margin-bottom: 0.25rem; + margin-top: 0.25rem; + } + .swagger-ui .mv2-m { + margin-bottom: 0.5rem; + margin-top: 0.5rem; + } + .swagger-ui .mv3-m { + margin-bottom: 1rem; + margin-top: 1rem; + } + .swagger-ui .mv4-m { + margin-bottom: 2rem; + margin-top: 2rem; + } + .swagger-ui .mv5-m { + margin-bottom: 4rem; + margin-top: 4rem; + } + .swagger-ui .mv6-m { + margin-bottom: 8rem; + margin-top: 8rem; + } + .swagger-ui .mv7-m { + margin-bottom: 16rem; + margin-top: 16rem; + } + .swagger-ui .mh0-m { + margin-left: 0; + margin-right: 0; + } + .swagger-ui .mh1-m { + margin-left: 0.25rem; + margin-right: 0.25rem; + } + .swagger-ui .mh2-m { + margin-left: 0.5rem; + margin-right: 0.5rem; + } + .swagger-ui .mh3-m { + margin-left: 1rem; + margin-right: 1rem; + } + .swagger-ui .mh4-m { + margin-left: 2rem; + margin-right: 2rem; + } + .swagger-ui .mh5-m { + margin-left: 4rem; + margin-right: 4rem; + } + .swagger-ui .mh6-m { + margin-left: 8rem; + margin-right: 8rem; + } + .swagger-ui .mh7-m { + margin-left: 16rem; + margin-right: 16rem; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .pa0-l { + padding: 0; + } + .swagger-ui .pa1-l { + padding: 0.25rem; + } + .swagger-ui .pa2-l { + padding: 0.5rem; + } + .swagger-ui .pa3-l { + padding: 1rem; + } + .swagger-ui .pa4-l { + padding: 2rem; + } + .swagger-ui .pa5-l { + padding: 4rem; + } + .swagger-ui .pa6-l { + padding: 8rem; + } + .swagger-ui .pa7-l { + padding: 16rem; + } + .swagger-ui .pl0-l { + padding-left: 0; + } + .swagger-ui .pl1-l { + padding-left: 0.25rem; + } + .swagger-ui .pl2-l { + padding-left: 0.5rem; + } + .swagger-ui .pl3-l { + padding-left: 1rem; + } + .swagger-ui .pl4-l { + padding-left: 2rem; + } + .swagger-ui .pl5-l { + padding-left: 4rem; + } + .swagger-ui .pl6-l { + padding-left: 8rem; + } + .swagger-ui .pl7-l { + padding-left: 16rem; + } + .swagger-ui .pr0-l { + padding-right: 0; + } + .swagger-ui .pr1-l { + padding-right: 0.25rem; + } + .swagger-ui .pr2-l { + padding-right: 0.5rem; + } + .swagger-ui .pr3-l { + padding-right: 1rem; + } + .swagger-ui .pr4-l { + padding-right: 2rem; + } + .swagger-ui .pr5-l { + padding-right: 4rem; + } + .swagger-ui .pr6-l { + padding-right: 8rem; + } + .swagger-ui .pr7-l { + padding-right: 16rem; + } + .swagger-ui .pb0-l { + padding-bottom: 0; + } + .swagger-ui .pb1-l { + padding-bottom: 0.25rem; + } + .swagger-ui .pb2-l { + padding-bottom: 0.5rem; + } + .swagger-ui .pb3-l { + padding-bottom: 1rem; + } + .swagger-ui .pb4-l { + padding-bottom: 2rem; + } + .swagger-ui .pb5-l { + padding-bottom: 4rem; + } + .swagger-ui .pb6-l { + padding-bottom: 8rem; + } + .swagger-ui .pb7-l { + padding-bottom: 16rem; + } + .swagger-ui .pt0-l { + padding-top: 0; + } + .swagger-ui .pt1-l { + padding-top: 0.25rem; + } + .swagger-ui .pt2-l { + padding-top: 0.5rem; + } + .swagger-ui .pt3-l { + padding-top: 1rem; + } + .swagger-ui .pt4-l { + padding-top: 2rem; + } + .swagger-ui .pt5-l { + padding-top: 4rem; + } + .swagger-ui .pt6-l { + padding-top: 8rem; + } + .swagger-ui .pt7-l { + padding-top: 16rem; + } + .swagger-ui .pv0-l { + padding-bottom: 0; + padding-top: 0; + } + .swagger-ui .pv1-l { + padding-bottom: 0.25rem; + padding-top: 0.25rem; + } + .swagger-ui .pv2-l { + padding-bottom: 0.5rem; + padding-top: 0.5rem; + } + .swagger-ui .pv3-l { + padding-bottom: 1rem; + padding-top: 1rem; + } + .swagger-ui .pv4-l { + padding-bottom: 2rem; + padding-top: 2rem; + } + .swagger-ui .pv5-l { + padding-bottom: 4rem; + padding-top: 4rem; + } + .swagger-ui .pv6-l { + padding-bottom: 8rem; + padding-top: 8rem; + } + .swagger-ui .pv7-l { + padding-bottom: 16rem; + padding-top: 16rem; + } + .swagger-ui .ph0-l { + padding-left: 0; + padding-right: 0; + } + .swagger-ui .ph1-l { + padding-left: 0.25rem; + padding-right: 0.25rem; + } + .swagger-ui .ph2-l { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + .swagger-ui .ph3-l { + padding-left: 1rem; + padding-right: 1rem; + } + .swagger-ui .ph4-l { + padding-left: 2rem; + padding-right: 2rem; + } + .swagger-ui .ph5-l { + padding-left: 4rem; + padding-right: 4rem; + } + .swagger-ui .ph6-l { + padding-left: 8rem; + padding-right: 8rem; + } + .swagger-ui .ph7-l { + padding-left: 16rem; + padding-right: 16rem; + } + .swagger-ui .ma0-l { + margin: 0; + } + .swagger-ui .ma1-l { + margin: 0.25rem; + } + .swagger-ui .ma2-l { + margin: 0.5rem; + } + .swagger-ui .ma3-l { + margin: 1rem; + } + .swagger-ui .ma4-l { + margin: 2rem; + } + .swagger-ui .ma5-l { + margin: 4rem; + } + .swagger-ui .ma6-l { + margin: 8rem; + } + .swagger-ui .ma7-l { + margin: 16rem; + } + .swagger-ui .ml0-l { + margin-left: 0; + } + .swagger-ui .ml1-l { + margin-left: 0.25rem; + } + .swagger-ui .ml2-l { + margin-left: 0.5rem; + } + .swagger-ui .ml3-l { + margin-left: 1rem; + } + .swagger-ui .ml4-l { + margin-left: 2rem; + } + .swagger-ui .ml5-l { + margin-left: 4rem; + } + .swagger-ui .ml6-l { + margin-left: 8rem; + } + .swagger-ui .ml7-l { + margin-left: 16rem; + } + .swagger-ui .mr0-l { + margin-right: 0; + } + .swagger-ui .mr1-l { + margin-right: 0.25rem; + } + .swagger-ui .mr2-l { + margin-right: 0.5rem; + } + .swagger-ui .mr3-l { + margin-right: 1rem; + } + .swagger-ui .mr4-l { + margin-right: 2rem; + } + .swagger-ui .mr5-l { + margin-right: 4rem; + } + .swagger-ui .mr6-l { + margin-right: 8rem; + } + .swagger-ui .mr7-l { + margin-right: 16rem; + } + .swagger-ui .mb0-l { + margin-bottom: 0; + } + .swagger-ui .mb1-l { + margin-bottom: 0.25rem; + } + .swagger-ui .mb2-l { + margin-bottom: 0.5rem; + } + .swagger-ui .mb3-l { + margin-bottom: 1rem; + } + .swagger-ui .mb4-l { + margin-bottom: 2rem; + } + .swagger-ui .mb5-l { + margin-bottom: 4rem; + } + .swagger-ui .mb6-l { + margin-bottom: 8rem; + } + .swagger-ui .mb7-l { + margin-bottom: 16rem; + } + .swagger-ui .mt0-l { + margin-top: 0; + } + .swagger-ui .mt1-l { + margin-top: 0.25rem; + } + .swagger-ui .mt2-l { + margin-top: 0.5rem; + } + .swagger-ui .mt3-l { + margin-top: 1rem; + } + .swagger-ui .mt4-l { + margin-top: 2rem; + } + .swagger-ui .mt5-l { + margin-top: 4rem; + } + .swagger-ui .mt6-l { + margin-top: 8rem; + } + .swagger-ui .mt7-l { + margin-top: 16rem; + } + .swagger-ui .mv0-l { + margin-bottom: 0; + margin-top: 0; + } + .swagger-ui .mv1-l { + margin-bottom: 0.25rem; + margin-top: 0.25rem; + } + .swagger-ui .mv2-l { + margin-bottom: 0.5rem; + margin-top: 0.5rem; + } + .swagger-ui .mv3-l { + margin-bottom: 1rem; + margin-top: 1rem; + } + .swagger-ui .mv4-l { + margin-bottom: 2rem; + margin-top: 2rem; + } + .swagger-ui .mv5-l { + margin-bottom: 4rem; + margin-top: 4rem; + } + .swagger-ui .mv6-l { + margin-bottom: 8rem; + margin-top: 8rem; + } + .swagger-ui .mv7-l { + margin-bottom: 16rem; + margin-top: 16rem; + } + .swagger-ui .mh0-l { + margin-left: 0; + margin-right: 0; + } + .swagger-ui .mh1-l { + margin-left: 0.25rem; + margin-right: 0.25rem; + } + .swagger-ui .mh2-l { + margin-left: 0.5rem; + margin-right: 0.5rem; + } + .swagger-ui .mh3-l { + margin-left: 1rem; + margin-right: 1rem; + } + .swagger-ui .mh4-l { + margin-left: 2rem; + margin-right: 2rem; + } + .swagger-ui .mh5-l { + margin-left: 4rem; + margin-right: 4rem; + } + .swagger-ui .mh6-l { + margin-left: 8rem; + margin-right: 8rem; + } + .swagger-ui .mh7-l { + margin-left: 16rem; + margin-right: 16rem; + } +} +.swagger-ui .na1 { + margin: -0.25rem; +} +.swagger-ui .na2 { + margin: -0.5rem; +} +.swagger-ui .na3 { + margin: -1rem; +} +.swagger-ui .na4 { + margin: -2rem; +} +.swagger-ui .na5 { + margin: -4rem; +} +.swagger-ui .na6 { + margin: -8rem; +} +.swagger-ui .na7 { + margin: -16rem; +} +.swagger-ui .nl1 { + margin-left: -0.25rem; +} +.swagger-ui .nl2 { + margin-left: -0.5rem; +} +.swagger-ui .nl3 { + margin-left: -1rem; +} +.swagger-ui .nl4 { + margin-left: -2rem; +} +.swagger-ui .nl5 { + margin-left: -4rem; +} +.swagger-ui .nl6 { + margin-left: -8rem; +} +.swagger-ui .nl7 { + margin-left: -16rem; +} +.swagger-ui .nr1 { + margin-right: -0.25rem; +} +.swagger-ui .nr2 { + margin-right: -0.5rem; +} +.swagger-ui .nr3 { + margin-right: -1rem; +} +.swagger-ui .nr4 { + margin-right: -2rem; +} +.swagger-ui .nr5 { + margin-right: -4rem; +} +.swagger-ui .nr6 { + margin-right: -8rem; +} +.swagger-ui .nr7 { + margin-right: -16rem; +} +.swagger-ui .nb1 { + margin-bottom: -0.25rem; +} +.swagger-ui .nb2 { + margin-bottom: -0.5rem; +} +.swagger-ui .nb3 { + margin-bottom: -1rem; +} +.swagger-ui .nb4 { + margin-bottom: -2rem; +} +.swagger-ui .nb5 { + margin-bottom: -4rem; +} +.swagger-ui .nb6 { + margin-bottom: -8rem; +} +.swagger-ui .nb7 { + margin-bottom: -16rem; +} +.swagger-ui .nt1 { + margin-top: -0.25rem; +} +.swagger-ui .nt2 { + margin-top: -0.5rem; +} +.swagger-ui .nt3 { + margin-top: -1rem; +} +.swagger-ui .nt4 { + margin-top: -2rem; +} +.swagger-ui .nt5 { + margin-top: -4rem; +} +.swagger-ui .nt6 { + margin-top: -8rem; +} +.swagger-ui .nt7 { + margin-top: -16rem; +} +@media screen and (min-width: 30em) { + .swagger-ui .na1-ns { + margin: -0.25rem; + } + .swagger-ui .na2-ns { + margin: -0.5rem; + } + .swagger-ui .na3-ns { + margin: -1rem; + } + .swagger-ui .na4-ns { + margin: -2rem; + } + .swagger-ui .na5-ns { + margin: -4rem; + } + .swagger-ui .na6-ns { + margin: -8rem; + } + .swagger-ui .na7-ns { + margin: -16rem; + } + .swagger-ui .nl1-ns { + margin-left: -0.25rem; + } + .swagger-ui .nl2-ns { + margin-left: -0.5rem; + } + .swagger-ui .nl3-ns { + margin-left: -1rem; + } + .swagger-ui .nl4-ns { + margin-left: -2rem; + } + .swagger-ui .nl5-ns { + margin-left: -4rem; + } + .swagger-ui .nl6-ns { + margin-left: -8rem; + } + .swagger-ui .nl7-ns { + margin-left: -16rem; + } + .swagger-ui .nr1-ns { + margin-right: -0.25rem; + } + .swagger-ui .nr2-ns { + margin-right: -0.5rem; + } + .swagger-ui .nr3-ns { + margin-right: -1rem; + } + .swagger-ui .nr4-ns { + margin-right: -2rem; + } + .swagger-ui .nr5-ns { + margin-right: -4rem; + } + .swagger-ui .nr6-ns { + margin-right: -8rem; + } + .swagger-ui .nr7-ns { + margin-right: -16rem; + } + .swagger-ui .nb1-ns { + margin-bottom: -0.25rem; + } + .swagger-ui .nb2-ns { + margin-bottom: -0.5rem; + } + .swagger-ui .nb3-ns { + margin-bottom: -1rem; + } + .swagger-ui .nb4-ns { + margin-bottom: -2rem; + } + .swagger-ui .nb5-ns { + margin-bottom: -4rem; + } + .swagger-ui .nb6-ns { + margin-bottom: -8rem; + } + .swagger-ui .nb7-ns { + margin-bottom: -16rem; + } + .swagger-ui .nt1-ns { + margin-top: -0.25rem; + } + .swagger-ui .nt2-ns { + margin-top: -0.5rem; + } + .swagger-ui .nt3-ns { + margin-top: -1rem; + } + .swagger-ui .nt4-ns { + margin-top: -2rem; + } + .swagger-ui .nt5-ns { + margin-top: -4rem; + } + .swagger-ui .nt6-ns { + margin-top: -8rem; + } + .swagger-ui .nt7-ns { + margin-top: -16rem; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .na1-m { + margin: -0.25rem; + } + .swagger-ui .na2-m { + margin: -0.5rem; + } + .swagger-ui .na3-m { + margin: -1rem; + } + .swagger-ui .na4-m { + margin: -2rem; + } + .swagger-ui .na5-m { + margin: -4rem; + } + .swagger-ui .na6-m { + margin: -8rem; + } + .swagger-ui .na7-m { + margin: -16rem; + } + .swagger-ui .nl1-m { + margin-left: -0.25rem; + } + .swagger-ui .nl2-m { + margin-left: -0.5rem; + } + .swagger-ui .nl3-m { + margin-left: -1rem; + } + .swagger-ui .nl4-m { + margin-left: -2rem; + } + .swagger-ui .nl5-m { + margin-left: -4rem; + } + .swagger-ui .nl6-m { + margin-left: -8rem; + } + .swagger-ui .nl7-m { + margin-left: -16rem; + } + .swagger-ui .nr1-m { + margin-right: -0.25rem; + } + .swagger-ui .nr2-m { + margin-right: -0.5rem; + } + .swagger-ui .nr3-m { + margin-right: -1rem; + } + .swagger-ui .nr4-m { + margin-right: -2rem; + } + .swagger-ui .nr5-m { + margin-right: -4rem; + } + .swagger-ui .nr6-m { + margin-right: -8rem; + } + .swagger-ui .nr7-m { + margin-right: -16rem; + } + .swagger-ui .nb1-m { + margin-bottom: -0.25rem; + } + .swagger-ui .nb2-m { + margin-bottom: -0.5rem; + } + .swagger-ui .nb3-m { + margin-bottom: -1rem; + } + .swagger-ui .nb4-m { + margin-bottom: -2rem; + } + .swagger-ui .nb5-m { + margin-bottom: -4rem; + } + .swagger-ui .nb6-m { + margin-bottom: -8rem; + } + .swagger-ui .nb7-m { + margin-bottom: -16rem; + } + .swagger-ui .nt1-m { + margin-top: -0.25rem; + } + .swagger-ui .nt2-m { + margin-top: -0.5rem; + } + .swagger-ui .nt3-m { + margin-top: -1rem; + } + .swagger-ui .nt4-m { + margin-top: -2rem; + } + .swagger-ui .nt5-m { + margin-top: -4rem; + } + .swagger-ui .nt6-m { + margin-top: -8rem; + } + .swagger-ui .nt7-m { + margin-top: -16rem; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .na1-l { + margin: -0.25rem; + } + .swagger-ui .na2-l { + margin: -0.5rem; + } + .swagger-ui .na3-l { + margin: -1rem; + } + .swagger-ui .na4-l { + margin: -2rem; + } + .swagger-ui .na5-l { + margin: -4rem; + } + .swagger-ui .na6-l { + margin: -8rem; + } + .swagger-ui .na7-l { + margin: -16rem; + } + .swagger-ui .nl1-l { + margin-left: -0.25rem; + } + .swagger-ui .nl2-l { + margin-left: -0.5rem; + } + .swagger-ui .nl3-l { + margin-left: -1rem; + } + .swagger-ui .nl4-l { + margin-left: -2rem; + } + .swagger-ui .nl5-l { + margin-left: -4rem; + } + .swagger-ui .nl6-l { + margin-left: -8rem; + } + .swagger-ui .nl7-l { + margin-left: -16rem; + } + .swagger-ui .nr1-l { + margin-right: -0.25rem; + } + .swagger-ui .nr2-l { + margin-right: -0.5rem; + } + .swagger-ui .nr3-l { + margin-right: -1rem; + } + .swagger-ui .nr4-l { + margin-right: -2rem; + } + .swagger-ui .nr5-l { + margin-right: -4rem; + } + .swagger-ui .nr6-l { + margin-right: -8rem; + } + .swagger-ui .nr7-l { + margin-right: -16rem; + } + .swagger-ui .nb1-l { + margin-bottom: -0.25rem; + } + .swagger-ui .nb2-l { + margin-bottom: -0.5rem; + } + .swagger-ui .nb3-l { + margin-bottom: -1rem; + } + .swagger-ui .nb4-l { + margin-bottom: -2rem; + } + .swagger-ui .nb5-l { + margin-bottom: -4rem; + } + .swagger-ui .nb6-l { + margin-bottom: -8rem; + } + .swagger-ui .nb7-l { + margin-bottom: -16rem; + } + .swagger-ui .nt1-l { + margin-top: -0.25rem; + } + .swagger-ui .nt2-l { + margin-top: -0.5rem; + } + .swagger-ui .nt3-l { + margin-top: -1rem; + } + .swagger-ui .nt4-l { + margin-top: -2rem; + } + .swagger-ui .nt5-l { + margin-top: -4rem; + } + .swagger-ui .nt6-l { + margin-top: -8rem; + } + .swagger-ui .nt7-l { + margin-top: -16rem; + } +} +.swagger-ui .collapse { + border-collapse: collapse; + border-spacing: 0; +} +.swagger-ui .striped--light-silver:nth-child(odd) { + background-color: #aaa; +} +.swagger-ui .striped--moon-gray:nth-child(odd) { + background-color: #ccc; +} +.swagger-ui .striped--light-gray:nth-child(odd) { + background-color: #eee; +} +.swagger-ui .striped--near-white:nth-child(odd) { + background-color: #f4f4f4; +} +.swagger-ui .stripe-light:nth-child(odd) { + background-color: hsla(0, 0%, 100%, 0.1); +} +.swagger-ui .stripe-dark:nth-child(odd) { + background-color: rgba(0, 0, 0, 0.1); +} +.swagger-ui .strike { + -webkit-text-decoration: line-through; + text-decoration: line-through; +} +.swagger-ui .underline { + -webkit-text-decoration: underline; + text-decoration: underline; +} +.swagger-ui .no-underline { + -webkit-text-decoration: none; + text-decoration: none; +} +@media screen and (min-width: 30em) { + .swagger-ui .strike-ns { + -webkit-text-decoration: line-through; + text-decoration: line-through; + } + .swagger-ui .underline-ns { + -webkit-text-decoration: underline; + text-decoration: underline; + } + .swagger-ui .no-underline-ns { + -webkit-text-decoration: none; + text-decoration: none; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .strike-m { + -webkit-text-decoration: line-through; + text-decoration: line-through; + } + .swagger-ui .underline-m { + -webkit-text-decoration: underline; + text-decoration: underline; + } + .swagger-ui .no-underline-m { + -webkit-text-decoration: none; + text-decoration: none; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .strike-l { + -webkit-text-decoration: line-through; + text-decoration: line-through; + } + .swagger-ui .underline-l { + -webkit-text-decoration: underline; + text-decoration: underline; + } + .swagger-ui .no-underline-l { + -webkit-text-decoration: none; + text-decoration: none; + } +} +.swagger-ui .tl { + text-align: left; +} +.swagger-ui .tr { + text-align: right; +} +.swagger-ui .tc { + text-align: center; +} +.swagger-ui .tj { + text-align: justify; +} +@media screen and (min-width: 30em) { + .swagger-ui .tl-ns { + text-align: left; + } + .swagger-ui .tr-ns { + text-align: right; + } + .swagger-ui .tc-ns { + text-align: center; + } + .swagger-ui .tj-ns { + text-align: justify; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .tl-m { + text-align: left; + } + .swagger-ui .tr-m { + text-align: right; + } + .swagger-ui .tc-m { + text-align: center; + } + .swagger-ui .tj-m { + text-align: justify; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .tl-l { + text-align: left; + } + .swagger-ui .tr-l { + text-align: right; + } + .swagger-ui .tc-l { + text-align: center; + } + .swagger-ui .tj-l { + text-align: justify; + } +} +.swagger-ui .ttc { + text-transform: capitalize; +} +.swagger-ui .ttl { + text-transform: lowercase; +} +.swagger-ui .ttu { + text-transform: uppercase; +} +.swagger-ui .ttn { + text-transform: none; +} +@media screen and (min-width: 30em) { + .swagger-ui .ttc-ns { + text-transform: capitalize; + } + .swagger-ui .ttl-ns { + text-transform: lowercase; + } + .swagger-ui .ttu-ns { + text-transform: uppercase; + } + .swagger-ui .ttn-ns { + text-transform: none; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .ttc-m { + text-transform: capitalize; + } + .swagger-ui .ttl-m { + text-transform: lowercase; + } + .swagger-ui .ttu-m { + text-transform: uppercase; + } + .swagger-ui .ttn-m { + text-transform: none; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .ttc-l { + text-transform: capitalize; + } + .swagger-ui .ttl-l { + text-transform: lowercase; + } + .swagger-ui .ttu-l { + text-transform: uppercase; + } + .swagger-ui .ttn-l { + text-transform: none; + } +} +.swagger-ui .f-6, +.swagger-ui .f-headline { + font-size: 6rem; +} +.swagger-ui .f-5, +.swagger-ui .f-subheadline { + font-size: 5rem; +} +.swagger-ui .f1 { + font-size: 3rem; +} +.swagger-ui .f2 { + font-size: 2.25rem; +} +.swagger-ui .f3 { + font-size: 1.5rem; +} +.swagger-ui .f4 { + font-size: 1.25rem; +} +.swagger-ui .f5 { + font-size: 1rem; +} +.swagger-ui .f6 { + font-size: 0.875rem; +} +.swagger-ui .f7 { + font-size: 0.75rem; +} +@media screen and (min-width: 30em) { + .swagger-ui .f-6-ns, + .swagger-ui .f-headline-ns { + font-size: 6rem; + } + .swagger-ui .f-5-ns, + .swagger-ui .f-subheadline-ns { + font-size: 5rem; + } + .swagger-ui .f1-ns { + font-size: 3rem; + } + .swagger-ui .f2-ns { + font-size: 2.25rem; + } + .swagger-ui .f3-ns { + font-size: 1.5rem; + } + .swagger-ui .f4-ns { + font-size: 1.25rem; + } + .swagger-ui .f5-ns { + font-size: 1rem; + } + .swagger-ui .f6-ns { + font-size: 0.875rem; + } + .swagger-ui .f7-ns { + font-size: 0.75rem; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .f-6-m, + .swagger-ui .f-headline-m { + font-size: 6rem; + } + .swagger-ui .f-5-m, + .swagger-ui .f-subheadline-m { + font-size: 5rem; + } + .swagger-ui .f1-m { + font-size: 3rem; + } + .swagger-ui .f2-m { + font-size: 2.25rem; + } + .swagger-ui .f3-m { + font-size: 1.5rem; + } + .swagger-ui .f4-m { + font-size: 1.25rem; + } + .swagger-ui .f5-m { + font-size: 1rem; + } + .swagger-ui .f6-m { + font-size: 0.875rem; + } + .swagger-ui .f7-m { + font-size: 0.75rem; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .f-6-l, + .swagger-ui .f-headline-l { + font-size: 6rem; + } + .swagger-ui .f-5-l, + .swagger-ui .f-subheadline-l { + font-size: 5rem; + } + .swagger-ui .f1-l { + font-size: 3rem; + } + .swagger-ui .f2-l { + font-size: 2.25rem; + } + .swagger-ui .f3-l { + font-size: 1.5rem; + } + .swagger-ui .f4-l { + font-size: 1.25rem; + } + .swagger-ui .f5-l { + font-size: 1rem; + } + .swagger-ui .f6-l { + font-size: 0.875rem; + } + .swagger-ui .f7-l { + font-size: 0.75rem; + } +} +.swagger-ui .measure { + max-width: 30em; +} +.swagger-ui .measure-wide { + max-width: 34em; +} +.swagger-ui .measure-narrow { + max-width: 20em; +} +.swagger-ui .indent { + margin-bottom: 0; + margin-top: 0; + text-indent: 1em; +} +.swagger-ui .small-caps { + font-feature-settings: 'smcp'; + font-variant: small-caps; +} +.swagger-ui .truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +@media screen and (min-width: 30em) { + .swagger-ui .measure-ns { + max-width: 30em; + } + .swagger-ui .measure-wide-ns { + max-width: 34em; + } + .swagger-ui .measure-narrow-ns { + max-width: 20em; + } + .swagger-ui .indent-ns { + margin-bottom: 0; + margin-top: 0; + text-indent: 1em; + } + .swagger-ui .small-caps-ns { + font-feature-settings: 'smcp'; + font-variant: small-caps; + } + .swagger-ui .truncate-ns { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .measure-m { + max-width: 30em; + } + .swagger-ui .measure-wide-m { + max-width: 34em; + } + .swagger-ui .measure-narrow-m { + max-width: 20em; + } + .swagger-ui .indent-m { + margin-bottom: 0; + margin-top: 0; + text-indent: 1em; + } + .swagger-ui .small-caps-m { + font-feature-settings: 'smcp'; + font-variant: small-caps; + } + .swagger-ui .truncate-m { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .measure-l { + max-width: 30em; + } + .swagger-ui .measure-wide-l { + max-width: 34em; + } + .swagger-ui .measure-narrow-l { + max-width: 20em; + } + .swagger-ui .indent-l { + margin-bottom: 0; + margin-top: 0; + text-indent: 1em; + } + .swagger-ui .small-caps-l { + font-feature-settings: 'smcp'; + font-variant: small-caps; + } + .swagger-ui .truncate-l { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} +.swagger-ui .overflow-container { + overflow-y: scroll; +} +.swagger-ui .center { + margin-left: auto; + margin-right: auto; +} +.swagger-ui .mr-auto { + margin-right: auto; +} +.swagger-ui .ml-auto { + margin-left: auto; +} +@media screen and (min-width: 30em) { + .swagger-ui .center-ns { + margin-left: auto; + margin-right: auto; + } + .swagger-ui .mr-auto-ns { + margin-right: auto; + } + .swagger-ui .ml-auto-ns { + margin-left: auto; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .center-m { + margin-left: auto; + margin-right: auto; + } + .swagger-ui .mr-auto-m { + margin-right: auto; + } + .swagger-ui .ml-auto-m { + margin-left: auto; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .center-l { + margin-left: auto; + margin-right: auto; + } + .swagger-ui .mr-auto-l { + margin-right: auto; + } + .swagger-ui .ml-auto-l { + margin-left: auto; + } +} +.swagger-ui .clip { + position: fixed !important; + _position: absolute !important; + clip: rect(1px 1px 1px 1px); + clip: rect(1px, 1px, 1px, 1px); +} +@media screen and (min-width: 30em) { + .swagger-ui .clip-ns { + position: fixed !important; + _position: absolute !important; + clip: rect(1px 1px 1px 1px); + clip: rect(1px, 1px, 1px, 1px); + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .clip-m { + position: fixed !important; + _position: absolute !important; + clip: rect(1px 1px 1px 1px); + clip: rect(1px, 1px, 1px, 1px); + } +} +@media screen and (min-width: 60em) { + .swagger-ui .clip-l { + position: fixed !important; + _position: absolute !important; + clip: rect(1px 1px 1px 1px); + clip: rect(1px, 1px, 1px, 1px); + } +} +.swagger-ui .ws-normal { + white-space: normal; +} +.swagger-ui .nowrap { + white-space: nowrap; +} +.swagger-ui .pre { + white-space: pre; +} +@media screen and (min-width: 30em) { + .swagger-ui .ws-normal-ns { + white-space: normal; + } + .swagger-ui .nowrap-ns { + white-space: nowrap; + } + .swagger-ui .pre-ns { + white-space: pre; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .ws-normal-m { + white-space: normal; + } + .swagger-ui .nowrap-m { + white-space: nowrap; + } + .swagger-ui .pre-m { + white-space: pre; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .ws-normal-l { + white-space: normal; + } + .swagger-ui .nowrap-l { + white-space: nowrap; + } + .swagger-ui .pre-l { + white-space: pre; + } +} +.swagger-ui .v-base { + vertical-align: baseline; +} +.swagger-ui .v-mid { + vertical-align: middle; +} +.swagger-ui .v-top { + vertical-align: top; +} +.swagger-ui .v-btm { + vertical-align: bottom; +} +@media screen and (min-width: 30em) { + .swagger-ui .v-base-ns { + vertical-align: baseline; + } + .swagger-ui .v-mid-ns { + vertical-align: middle; + } + .swagger-ui .v-top-ns { + vertical-align: top; + } + .swagger-ui .v-btm-ns { + vertical-align: bottom; + } +} +@media screen and (min-width: 30em) and (max-width: 60em) { + .swagger-ui .v-base-m { + vertical-align: baseline; + } + .swagger-ui .v-mid-m { + vertical-align: middle; + } + .swagger-ui .v-top-m { + vertical-align: top; + } + .swagger-ui .v-btm-m { + vertical-align: bottom; + } +} +@media screen and (min-width: 60em) { + .swagger-ui .v-base-l { + vertical-align: baseline; + } + .swagger-ui .v-mid-l { + vertical-align: middle; + } + .swagger-ui .v-top-l { + vertical-align: top; + } + .swagger-ui .v-btm-l { + vertical-align: bottom; + } +} +.swagger-ui .dim { + opacity: 1; + transition: opacity 0.15s ease-in; +} +.swagger-ui .dim:focus, +.swagger-ui .dim:hover { + opacity: 0.5; + transition: opacity 0.15s ease-in; +} +.swagger-ui .dim:active { + opacity: 0.8; + transition: opacity 0.15s ease-out; +} +.swagger-ui .glow { + transition: opacity 0.15s ease-in; +} +.swagger-ui .glow:focus, +.swagger-ui .glow:hover { + opacity: 1; + transition: opacity 0.15s ease-in; +} +.swagger-ui .hide-child .child { + opacity: 0; + transition: opacity 0.15s ease-in; +} +.swagger-ui .hide-child:active .child, +.swagger-ui .hide-child:focus .child, +.swagger-ui .hide-child:hover .child { + opacity: 1; + transition: opacity 0.15s ease-in; +} +.swagger-ui .underline-hover:focus, +.swagger-ui .underline-hover:hover { + -webkit-text-decoration: underline; + text-decoration: underline; +} +.swagger-ui .grow { + -moz-osx-font-smoothing: grayscale; + backface-visibility: hidden; + transform: translateZ(0); + transition: transform 0.25s ease-out; +} +.swagger-ui .grow:focus, +.swagger-ui .grow:hover { + transform: scale(1.05); +} +.swagger-ui .grow:active { + transform: scale(0.9); +} +.swagger-ui .grow-large { + -moz-osx-font-smoothing: grayscale; + backface-visibility: hidden; + transform: translateZ(0); + transition: transform 0.25s ease-in-out; +} +.swagger-ui .grow-large:focus, +.swagger-ui .grow-large:hover { + transform: scale(1.2); +} +.swagger-ui .grow-large:active { + transform: scale(0.95); +} +.swagger-ui .pointer:hover { + cursor: pointer; +} +.swagger-ui .shadow-hover { + cursor: pointer; + position: relative; + transition: all 0.5s cubic-bezier(0.165, 0.84, 0.44, 1); +} +.swagger-ui .shadow-hover:after { + border-radius: inherit; + box-shadow: 0 0 16px 2px rgba(0, 0, 0, 0.2); + content: ''; + height: 100%; + left: 0; + opacity: 0; + position: absolute; + top: 0; + transition: opacity 0.5s cubic-bezier(0.165, 0.84, 0.44, 1); + width: 100%; + z-index: -1; +} +.swagger-ui .shadow-hover:focus:after, +.swagger-ui .shadow-hover:hover:after { + opacity: 1; +} +.swagger-ui .bg-animate, +.swagger-ui .bg-animate:focus, +.swagger-ui .bg-animate:hover { + transition: background-color 0.15s ease-in-out; +} +.swagger-ui .z-0 { + z-index: 0; +} +.swagger-ui .z-1 { + z-index: 1; +} +.swagger-ui .z-2 { + z-index: 2; +} +.swagger-ui .z-3 { + z-index: 3; +} +.swagger-ui .z-4 { + z-index: 4; +} +.swagger-ui .z-5 { + z-index: 5; +} +.swagger-ui .z-999 { + z-index: 999; +} +.swagger-ui .z-9999 { + z-index: 9999; +} +.swagger-ui .z-max { + z-index: 2147483647; +} +.swagger-ui .z-inherit { + z-index: inherit; +} +.swagger-ui .z-initial, +.swagger-ui .z-unset { + z-index: auto; +} +.swagger-ui .nested-copy-line-height ol, +.swagger-ui .nested-copy-line-height p, +.swagger-ui .nested-copy-line-height ul { + line-height: 1.5; +} +.swagger-ui .nested-headline-line-height h1, +.swagger-ui .nested-headline-line-height h2, +.swagger-ui .nested-headline-line-height h3, +.swagger-ui .nested-headline-line-height h4, +.swagger-ui .nested-headline-line-height h5, +.swagger-ui .nested-headline-line-height h6 { + line-height: 1.25; +} +.swagger-ui .nested-list-reset ol, +.swagger-ui .nested-list-reset ul { + list-style-type: none; + margin-left: 0; + padding-left: 0; +} +.swagger-ui .nested-copy-indent p + p { + margin-bottom: 0; + margin-top: 0; + text-indent: 0.1em; +} +.swagger-ui .nested-copy-seperator p + p { + margin-top: 1.5em; +} +.swagger-ui .nested-img img { + display: block; + max-width: 100%; + width: 100%; +} +.swagger-ui .nested-links a { + color: #357edd; + transition: color 0.15s ease-in; +} +.swagger-ui .nested-links a:focus, +.swagger-ui .nested-links a:hover { + color: #96ccff; + transition: color 0.15s ease-in; +} +.swagger-ui .wrapper { + box-sizing: border-box; + margin: 0 auto; + max-width: 1460px; + padding: 0 20px; + width: 100%; +} +.swagger-ui .opblock-tag-section { + display: flex; + flex-direction: column; +} +.swagger-ui .try-out.btn-group { + display: flex; + flex: 0.1 2 auto; + padding: 0; +} +.swagger-ui .try-out__btn { + margin-left: 1.25rem; +} +.swagger-ui .opblock-tag { + align-items: center; + border-bottom: 1px solid rgba(59, 65, 81, 0.3); + cursor: pointer; + display: flex; + padding: 10px 20px 10px 10px; + transition: all 0.2s; +} +.swagger-ui .opblock-tag:hover { + background: rgba(0, 0, 0, 0.02); +} +.swagger-ui .opblock-tag { + color: #3b4151; + font-family: sans-serif; + font-size: 24px; + margin: 0 0 5px; +} +.swagger-ui .opblock-tag.no-desc span { + flex: 1; +} +.swagger-ui .opblock-tag svg { + transition: all 0.4s; +} +.swagger-ui .opblock-tag small { + color: #3b4151; + flex: 2; + font-family: sans-serif; + font-size: 14px; + font-weight: 400; + padding: 0 10px; +} +.swagger-ui .opblock-tag > div { + flex: 1 1 150px; + font-weight: 400; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +@media (max-width: 640px) { + .swagger-ui .opblock-tag small, + .swagger-ui .opblock-tag > div { + flex: 1; + } +} +.swagger-ui .opblock-tag .info__externaldocs { + text-align: right; +} +.swagger-ui .parameter__type { + color: #3b4151; + font-family: monospace; + font-size: 12px; + font-weight: 600; + padding: 5px 0; +} +.swagger-ui .parameter-controls { + margin-top: 0.75em; +} +.swagger-ui .examples__title { + display: block; + font-size: 1.1em; + font-weight: 700; + margin-bottom: 0.75em; +} +.swagger-ui .examples__section { + margin-top: 1.5em; +} +.swagger-ui .examples__section-header { + font-size: 0.9rem; + font-weight: 700; + margin-bottom: 0.5rem; +} +.swagger-ui .examples-select { + display: inline-block; + margin-bottom: 0.75em; +} +.swagger-ui .examples-select .examples-select-element { + width: 100%; +} +.swagger-ui .examples-select__section-label { + font-size: 0.9rem; + font-weight: 700; + margin-right: 0.5rem; +} +.swagger-ui .example__section { + margin-top: 1.5em; +} +.swagger-ui .example__section-header { + font-size: 0.9rem; + font-weight: 700; + margin-bottom: 0.5rem; +} +.swagger-ui .view-line-link { + cursor: pointer; + margin: 0 5px; + position: relative; + top: 3px; + transition: all 0.5s; + width: 20px; +} +.swagger-ui .opblock { + border: 1px solid #000; + border-radius: 4px; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.19); + margin: 0 0 15px; +} +.swagger-ui .opblock .tab-header { + display: flex; + flex: 1; +} +.swagger-ui .opblock .tab-header .tab-item { + cursor: pointer; + padding: 0 40px; +} +.swagger-ui .opblock .tab-header .tab-item:first-of-type { + padding: 0 40px 0 0; +} +.swagger-ui .opblock .tab-header .tab-item.active h4 span { + position: relative; +} +.swagger-ui .opblock .tab-header .tab-item.active h4 span:after { + background: grey; + bottom: -15px; + content: ''; + height: 4px; + left: 50%; + position: absolute; + transform: translateX(-50%); + width: 120%; +} +.swagger-ui .opblock.is-open .opblock-summary { + border-bottom: 1px solid #000; +} +.swagger-ui .opblock .opblock-section-header { + align-items: center; + background: hsla(0, 0%, 100%, 0.8); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + display: flex; + min-height: 50px; + padding: 8px 20px; +} +.swagger-ui .opblock .opblock-section-header > label { + align-items: center; + color: #3b4151; + display: flex; + font-family: sans-serif; + font-size: 12px; + font-weight: 700; + margin: 0 0 0 auto; +} +.swagger-ui .opblock .opblock-section-header > label > span { + padding: 0 10px 0 0; +} +.swagger-ui .opblock .opblock-section-header h4 { + color: #3b4151; + flex: 1; + font-family: sans-serif; + font-size: 14px; + margin: 0; +} +.swagger-ui .opblock .opblock-summary-method { + background: #000; + border-radius: 3px; + color: #fff; + font-family: sans-serif; + font-size: 14px; + font-weight: 700; + min-width: 80px; + padding: 6px 0; + text-align: center; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); +} +@media (max-width: 768px) { + .swagger-ui .opblock .opblock-summary-method { + font-size: 12px; + } +} +.swagger-ui .opblock .opblock-summary-operation-id, +.swagger-ui .opblock .opblock-summary-path, +.swagger-ui .opblock .opblock-summary-path__deprecated { + align-items: center; + color: #3b4151; + display: flex; + font-family: monospace; + font-size: 16px; + font-weight: 600; + word-break: break-word; +} +@media (max-width: 768px) { + .swagger-ui .opblock .opblock-summary-operation-id, + .swagger-ui .opblock .opblock-summary-path, + .swagger-ui .opblock .opblock-summary-path__deprecated { + font-size: 12px; + } +} +.swagger-ui .opblock .opblock-summary-path { + flex-shrink: 1; +} +@media (max-width: 640px) { + .swagger-ui .opblock .opblock-summary-path { + max-width: 100%; + } +} +.swagger-ui .opblock .opblock-summary-path__deprecated { + -webkit-text-decoration: line-through; + text-decoration: line-through; +} +.swagger-ui .opblock .opblock-summary-operation-id { + font-size: 14px; +} +.swagger-ui .opblock .opblock-summary-description { + color: #3b4151; + font-family: sans-serif; + font-size: 13px; + word-break: break-word; +} +.swagger-ui .opblock .opblock-summary-path-description-wrapper { + align-items: center; + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 0 10px; + padding: 0 10px; + width: 100%; +} +@media (max-width: 550px) { + .swagger-ui .opblock .opblock-summary-path-description-wrapper { + align-items: flex-start; + flex-direction: column; + } +} +.swagger-ui .opblock .opblock-summary { + align-items: center; + cursor: pointer; + display: flex; + padding: 5px; +} +.swagger-ui .opblock .opblock-summary .view-line-link { + cursor: pointer; + margin: 0; + position: relative; + top: 2px; + transition: all 0.5s; + width: 0; +} +.swagger-ui .opblock .opblock-summary:hover .view-line-link { + margin: 0 5px; + width: 18px; +} +.swagger-ui .opblock .opblock-summary:hover .view-line-link.copy-to-clipboard { + width: 24px; +} +.swagger-ui .opblock.opblock-post { + background: rgba(73, 204, 144, 0.1); + border-color: #49cc90; +} +.swagger-ui .opblock.opblock-post .opblock-summary-method { + background: #49cc90; +} +.swagger-ui .opblock.opblock-post .opblock-summary { + border-color: #49cc90; +} +.swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span:after { + background: #49cc90; +} +.swagger-ui .opblock.opblock-put { + background: rgba(252, 161, 48, 0.1); + border-color: #fca130; +} +.swagger-ui .opblock.opblock-put .opblock-summary-method { + background: #fca130; +} +.swagger-ui .opblock.opblock-put .opblock-summary { + border-color: #fca130; +} +.swagger-ui .opblock.opblock-put .tab-header .tab-item.active h4 span:after { + background: #fca130; +} +.swagger-ui .opblock.opblock-delete { + background: rgba(249, 62, 62, 0.1); + border-color: #f93e3e; +} +.swagger-ui .opblock.opblock-delete .opblock-summary-method { + background: #f93e3e; +} +.swagger-ui .opblock.opblock-delete .opblock-summary { + border-color: #f93e3e; +} +.swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span:after { + background: #f93e3e; +} +.swagger-ui .opblock.opblock-get { + background: rgba(97, 175, 254, 0.1); + border-color: #61affe; +} +.swagger-ui .opblock.opblock-get .opblock-summary-method { + background: #61affe; +} +.swagger-ui .opblock.opblock-get .opblock-summary { + border-color: #61affe; +} +.swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span:after { + background: #61affe; +} +.swagger-ui .opblock.opblock-patch { + background: rgba(80, 227, 194, 0.1); + border-color: #50e3c2; +} +.swagger-ui .opblock.opblock-patch .opblock-summary-method { + background: #50e3c2; +} +.swagger-ui .opblock.opblock-patch .opblock-summary { + border-color: #50e3c2; +} +.swagger-ui .opblock.opblock-patch .tab-header .tab-item.active h4 span:after { + background: #50e3c2; +} +.swagger-ui .opblock.opblock-head { + background: rgba(144, 18, 254, 0.1); + border-color: #9012fe; +} +.swagger-ui .opblock.opblock-head .opblock-summary-method { + background: #9012fe; +} +.swagger-ui .opblock.opblock-head .opblock-summary { + border-color: #9012fe; +} +.swagger-ui .opblock.opblock-head .tab-header .tab-item.active h4 span:after { + background: #9012fe; +} +.swagger-ui .opblock.opblock-options { + background: rgba(13, 90, 167, 0.1); + border-color: #0d5aa7; +} +.swagger-ui .opblock.opblock-options .opblock-summary-method { + background: #0d5aa7; +} +.swagger-ui .opblock.opblock-options .opblock-summary { + border-color: #0d5aa7; +} +.swagger-ui .opblock.opblock-options .tab-header .tab-item.active h4 span:after { + background: #0d5aa7; +} +.swagger-ui .opblock.opblock-deprecated { + background: hsla(0, 0%, 92%, 0.1); + border-color: #ebebeb; + opacity: 0.6; +} +.swagger-ui .opblock.opblock-deprecated .opblock-summary-method { + background: #ebebeb; +} +.swagger-ui .opblock.opblock-deprecated .opblock-summary { + border-color: #ebebeb; +} +.swagger-ui .opblock.opblock-deprecated .tab-header .tab-item.active h4 span:after { + background: #ebebeb; +} +.swagger-ui .opblock .opblock-schemes { + padding: 8px 20px; +} +.swagger-ui .opblock .opblock-schemes .schemes-title { + padding: 0 10px 0 0; +} +.swagger-ui .filter .operation-filter-input { + border: 2px solid #d8dde7; + margin: 20px 0; + padding: 10px; + width: 100%; +} +.swagger-ui .download-url-wrapper .failed, +.swagger-ui .filter .failed { + color: red; +} +.swagger-ui .download-url-wrapper .loading, +.swagger-ui .filter .loading { + color: #aaa; +} +.swagger-ui .model-example { + margin-top: 1em; +} +.swagger-ui .tab { + display: flex; + list-style: none; + padding: 0; +} +.swagger-ui .tab li { + color: #3b4151; + cursor: pointer; + font-family: sans-serif; + font-size: 12px; + min-width: 60px; + padding: 0; +} +.swagger-ui .tab li:first-of-type { + padding-left: 0; + padding-right: 12px; + position: relative; +} +.swagger-ui .tab li:first-of-type:after { + background: rgba(0, 0, 0, 0.2); + content: ''; + height: 100%; + position: absolute; + right: 6px; + top: 0; + width: 1px; +} +.swagger-ui .tab li.active { + font-weight: 700; +} +.swagger-ui .tab li button.tablinks { + background: none; + border: 0; + color: inherit; + font-family: inherit; + font-weight: inherit; + padding: 0; +} +.swagger-ui .opblock-description-wrapper, +.swagger-ui .opblock-external-docs-wrapper, +.swagger-ui .opblock-title_normal { + color: #3b4151; + font-family: sans-serif; + font-size: 12px; + margin: 0 0 5px; + padding: 15px 20px; +} +.swagger-ui .opblock-description-wrapper h4, +.swagger-ui .opblock-external-docs-wrapper h4, +.swagger-ui .opblock-title_normal h4 { + color: #3b4151; + font-family: sans-serif; + font-size: 12px; + margin: 0 0 5px; +} +.swagger-ui .opblock-description-wrapper p, +.swagger-ui .opblock-external-docs-wrapper p, +.swagger-ui .opblock-title_normal p { + color: #3b4151; + font-family: sans-serif; + font-size: 14px; + margin: 0; +} +.swagger-ui .opblock-external-docs-wrapper h4 { + padding-left: 0; +} +.swagger-ui .execute-wrapper { + padding: 20px; + text-align: right; +} +.swagger-ui .execute-wrapper .btn { + padding: 8px 40px; + width: 100%; +} +.swagger-ui .body-param-options { + display: flex; + flex-direction: column; +} +.swagger-ui .body-param-options .body-param-edit { + padding: 10px 0; +} +.swagger-ui .body-param-options label { + padding: 8px 0; +} +.swagger-ui .body-param-options label select { + margin: 3px 0 0; +} +.swagger-ui .responses-inner { + padding: 20px; +} +.swagger-ui .responses-inner h4, +.swagger-ui .responses-inner h5 { + color: #3b4151; + font-family: sans-serif; + font-size: 12px; + margin: 10px 0 5px; +} +.swagger-ui .responses-inner .curl { + max-height: 400px; + min-height: 6em; + overflow-y: auto; +} +.swagger-ui .response-col_status { + color: #3b4151; + font-family: sans-serif; + font-size: 14px; +} +.swagger-ui .response-col_status .response-undocumented { + color: #909090; + font-family: monospace; + font-size: 11px; + font-weight: 600; +} +.swagger-ui .response-col_links { + color: #3b4151; + font-family: sans-serif; + font-size: 14px; + max-width: 40em; + padding-left: 2em; +} +.swagger-ui .response-col_links .response-undocumented { + color: #909090; + font-family: monospace; + font-size: 11px; + font-weight: 600; +} +.swagger-ui .response-col_links .operation-link { + margin-bottom: 1.5em; +} +.swagger-ui .response-col_links .operation-link .description { + margin-bottom: 0.5em; +} +.swagger-ui .opblock-body .opblock-loading-animation { + display: block; + margin: 3em auto; +} +.swagger-ui .opblock-body pre.microlight { + background: #333; + border-radius: 4px; + font-size: 12px; + hyphens: auto; + margin: 0; + padding: 10px; + white-space: pre-wrap; + word-break: break-all; + word-break: break-word; + word-wrap: break-word; + color: #fff; + font-family: monospace; + font-weight: 600; +} +.swagger-ui .opblock-body pre.microlight .headerline { + display: block; +} +.swagger-ui .highlight-code { + position: relative; +} +.swagger-ui .highlight-code > .microlight { + max-height: 400px; + min-height: 6em; + overflow-y: auto; +} +.swagger-ui .highlight-code > .microlight code { + white-space: pre-wrap !important; + word-break: break-all; +} +.swagger-ui .curl-command { + position: relative; +} +.swagger-ui .download-contents { + align-items: center; + background: #7d8293; + border: none; + border-radius: 4px; + bottom: 10px; + color: #fff; + display: flex; + font-family: sans-serif; + font-size: 14px; + font-weight: 600; + height: 30px; + justify-content: center; + padding: 5px; + position: absolute; + right: 10px; + text-align: center; +} +.swagger-ui .scheme-container { + background: #fff; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.15); + margin: 0 0 20px; + padding: 30px 0; +} +.swagger-ui .scheme-container .schemes { + align-items: flex-end; + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: space-between; +} +.swagger-ui .scheme-container .schemes > .schemes-server-container { + display: flex; + flex-wrap: wrap; + gap: 10px; +} +.swagger-ui .scheme-container .schemes > .schemes-server-container > label { + color: #3b4151; + display: flex; + flex-direction: column; + font-family: sans-serif; + font-size: 12px; + font-weight: 700; + margin: -20px 15px 0 0; +} +.swagger-ui .scheme-container .schemes > .schemes-server-container > label select { + min-width: 130px; + text-transform: uppercase; +} +.swagger-ui .scheme-container .schemes:not(:has(.schemes-server-container)) { + justify-content: flex-end; +} +.swagger-ui .scheme-container .schemes .auth-wrapper { + flex: none; + justify-content: start; +} +.swagger-ui .scheme-container .schemes .auth-wrapper .authorize { + display: flex; + flex-wrap: nowrap; + margin: 0; + padding-right: 20px; +} +.swagger-ui .loading-container { + align-items: center; + display: flex; + flex-direction: column; + justify-content: center; + margin-top: 1em; + min-height: 1px; + padding: 40px 0 60px; +} +.swagger-ui .loading-container .loading { + position: relative; +} +.swagger-ui .loading-container .loading:after { + color: #3b4151; + content: 'loading'; + font-family: sans-serif; + font-size: 10px; + font-weight: 700; + left: 50%; + position: absolute; + text-transform: uppercase; + top: 50%; + transform: translate(-50%, -50%); +} +.swagger-ui .loading-container .loading:before { + animation: + rotation 1s linear infinite, + opacity 0.5s; + backface-visibility: hidden; + border: 2px solid rgba(85, 85, 85, 0.1); + border-radius: 100%; + border-top-color: rgba(0, 0, 0, 0.6); + content: ''; + display: block; + height: 60px; + left: 50%; + margin: -30px; + opacity: 1; + position: absolute; + top: 50%; + width: 60px; +} +@keyframes rotation { + to { + transform: rotate(1turn); + } +} +.swagger-ui .response-controls { + display: flex; + padding-top: 1em; +} +.swagger-ui .response-control-media-type { + margin-right: 1em; +} +.swagger-ui .response-control-media-type--accept-controller select { + border-color: green; +} +.swagger-ui .response-control-media-type__accept-message { + color: green; + font-size: 0.7em; +} +.swagger-ui .response-control-examples__title, +.swagger-ui .response-control-media-type__title { + display: block; + font-size: 0.7em; + margin-bottom: 0.2em; +} +@keyframes blinker { + 50% { + opacity: 0; + } +} +.swagger-ui .hidden { + display: none; +} +.swagger-ui .no-margin { + border: none; + height: auto; + margin: 0; + padding: 0; +} +.swagger-ui .float-right { + float: right; +} +.swagger-ui .svg-assets { + height: 0; + position: absolute; + width: 0; +} +.swagger-ui section h3 { + color: #3b4151; + font-family: sans-serif; +} +.swagger-ui a.nostyle { + display: inline; +} +.swagger-ui a.nostyle, +.swagger-ui a.nostyle:visited { + color: inherit; + cursor: pointer; + text-decoration: inherit; +} +.swagger-ui .fallback { + color: #aaa; + padding: 1em; +} +.swagger-ui .version-pragma { + height: 100%; + padding: 5em 0; +} +.swagger-ui .version-pragma__message { + display: flex; + font-size: 1.2em; + height: 100%; + justify-content: center; + line-height: 1.5em; + padding: 0 0.6em; + text-align: center; +} +.swagger-ui .version-pragma__message > div { + flex: 1; + max-width: 55ch; +} +.swagger-ui .version-pragma__message code { + background-color: #dedede; + padding: 4px 4px 2px; + white-space: pre; +} +.swagger-ui .opblock-link { + font-weight: 400; +} +.swagger-ui .opblock-link.shown { + font-weight: 700; +} +.swagger-ui span.token-string { + color: #555; +} +.swagger-ui span.token-not-formatted { + color: #555; + font-weight: 700; +} +.swagger-ui .btn { + background: transparent; + border: 2px solid grey; + border-radius: 4px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + color: #3b4151; + font-family: sans-serif; + font-size: 14px; + font-weight: 700; + padding: 5px 23px; + transition: all 0.3s; +} +.swagger-ui .btn.btn-sm { + font-size: 12px; + padding: 4px 23px; +} +.swagger-ui .btn[disabled] { + cursor: not-allowed; + opacity: 0.3; +} +.swagger-ui .btn:hover { + box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); +} +.swagger-ui .btn.cancel { + background-color: transparent; + border-color: #ff6060; + color: #ff6060; + font-family: sans-serif; +} +.swagger-ui .btn.authorize { + background-color: transparent; + border-color: #49cc90; + color: #49cc90; + display: inline; + line-height: 1; +} +.swagger-ui .btn.authorize span { + float: left; + padding: 4px 20px 0 0; +} +.swagger-ui .btn.authorize svg { + fill: #49cc90; +} +.swagger-ui .btn.execute { + background-color: #4990e2; + border-color: #4990e2; + color: #fff; +} +.swagger-ui .btn-group { + display: flex; + padding: 30px; +} +.swagger-ui .btn-group .btn { + flex: 1; +} +.swagger-ui .btn-group .btn:first-child { + border-radius: 4px 0 0 4px; +} +.swagger-ui .btn-group .btn:last-child { + border-radius: 0 4px 4px 0; +} +.swagger-ui .authorization__btn { + background: none; + border: none; + padding: 0 0 0 10px; +} +.swagger-ui .authorization__btn .locked { + opacity: 1; +} +.swagger-ui .authorization__btn .unlocked { + opacity: 0.4; +} +.swagger-ui .model-box-control, +.swagger-ui .models-control, +.swagger-ui .opblock-summary-control { + all: inherit; + border-bottom: 0; + cursor: pointer; + flex: 1; + padding: 0; +} +.swagger-ui .model-box-control:focus, +.swagger-ui .models-control:focus, +.swagger-ui .opblock-summary-control:focus { + outline: auto; +} +.swagger-ui .expand-methods, +.swagger-ui .expand-operation { + background: none; + border: none; +} +.swagger-ui .expand-methods svg, +.swagger-ui .expand-operation svg { + height: 20px; + width: 20px; +} +.swagger-ui .expand-methods { + padding: 0 10px; +} +.swagger-ui .expand-methods:hover svg { + fill: #404040; +} +.swagger-ui .expand-methods svg { + transition: all 0.3s; + fill: #707070; +} +.swagger-ui button { + cursor: pointer; +} +.swagger-ui button.invalid { + animation: shake 0.4s 1; + background: #feebeb; + border-color: #f93e3e; +} +.swagger-ui .copy-to-clipboard { + align-items: center; + background: #7d8293; + border: none; + border-radius: 4px; + bottom: 10px; + display: flex; + height: 30px; + justify-content: center; + position: absolute; + right: 100px; + width: 30px; +} +.swagger-ui .copy-to-clipboard button { + background: url('data:image/svg+xml;charset=utf-8,') + 50% no-repeat; + border: none; + flex-grow: 1; + flex-shrink: 1; + height: 25px; +} +.swagger-ui .copy-to-clipboard:active { + background: #5e626f; +} +.swagger-ui .opblock-control-arrow { + background: none; + border: none; + text-align: center; +} +.swagger-ui .curl-command .copy-to-clipboard { + bottom: 5px; + height: 20px; + right: 10px; + width: 20px; +} +.swagger-ui .curl-command .copy-to-clipboard button { + height: 18px; +} +.swagger-ui .opblock .opblock-summary .view-line-link.copy-to-clipboard { + height: 26px; + position: static; +} +.swagger-ui select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background: #f7f7f7 + url('data:image/svg+xml;charset=utf-8,') + right 10px center no-repeat; + background-size: 20px; + border: 2px solid #41444e; + border-radius: 4px; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.25); + color: #3b4151; + font-family: sans-serif; + font-size: 14px; + font-weight: 700; + padding: 5px 40px 5px 10px; +} +.swagger-ui select[multiple] { + background: #f7f7f7; + margin: 5px 0; + padding: 5px; +} +.swagger-ui select.invalid { + animation: shake 0.4s 1; + background: #feebeb; + border-color: #f93e3e; +} +.swagger-ui .opblock-body select { + min-width: 230px; +} +@media (max-width: 768px) { + .swagger-ui .opblock-body select { + min-width: 180px; + } +} +@media (max-width: 640px) { + .swagger-ui .opblock-body select { + min-width: 100%; + width: 100%; + } +} +.swagger-ui label { + color: #3b4151; + font-family: sans-serif; + font-size: 12px; + font-weight: 700; + margin: 0 0 5px; +} +.swagger-ui input[type='email'], +.swagger-ui input[type='file'], +.swagger-ui input[type='password'], +.swagger-ui input[type='search'], +.swagger-ui input[type='text'] { + line-height: 1; +} +@media (max-width: 768px) { + .swagger-ui input[type='email'], + .swagger-ui input[type='file'], + .swagger-ui input[type='password'], + .swagger-ui input[type='search'], + .swagger-ui input[type='text'] { + max-width: 175px; + } +} +.swagger-ui input[type='email'], +.swagger-ui input[type='file'], +.swagger-ui input[type='password'], +.swagger-ui input[type='search'], +.swagger-ui input[type='text'], +.swagger-ui textarea { + background: #fff; + border: 1px solid #d9d9d9; + border-radius: 4px; + margin: 5px 0; + min-width: 100px; + padding: 8px 10px; +} +.swagger-ui input[type='email'].invalid, +.swagger-ui input[type='file'].invalid, +.swagger-ui input[type='password'].invalid, +.swagger-ui input[type='search'].invalid, +.swagger-ui input[type='text'].invalid, +.swagger-ui textarea.invalid { + animation: shake 0.4s 1; + background: #feebeb; + border-color: #f93e3e; +} +.swagger-ui input[disabled], +.swagger-ui select[disabled], +.swagger-ui textarea[disabled] { + background-color: #fafafa; + color: #888; + cursor: not-allowed; +} +.swagger-ui select[disabled] { + border-color: #888; +} +.swagger-ui textarea[disabled] { + background-color: #41444e; + color: #fff; +} +@keyframes shake { + 10%, + 90% { + transform: translate3d(-1px, 0, 0); + } + 20%, + 80% { + transform: translate3d(2px, 0, 0); + } + 30%, + 50%, + 70% { + transform: translate3d(-4px, 0, 0); + } + 40%, + 60% { + transform: translate3d(4px, 0, 0); + } +} +.swagger-ui textarea { + background: hsla(0, 0%, 100%, 0.8); + border: none; + border-radius: 4px; + color: #3b4151; + font-family: monospace; + font-size: 12px; + font-weight: 600; + min-height: 280px; + outline: none; + padding: 10px; + width: 100%; +} +.swagger-ui textarea:focus { + border: 2px solid #61affe; +} +.swagger-ui textarea.curl { + background: #41444e; + border-radius: 4px; + color: #fff; + font-family: monospace; + font-size: 12px; + font-weight: 600; + margin: 0; + min-height: 100px; + padding: 10px; + resize: none; +} +.swagger-ui .checkbox { + color: #303030; + padding: 5px 0 10px; + transition: opacity 0.5s; +} +.swagger-ui .checkbox label { + display: flex; +} +.swagger-ui .checkbox p { + color: #3b4151; + font-family: monospace; + font-style: italic; + font-weight: 400 !important; + font-weight: 600; + margin: 0 !important; +} +.swagger-ui .checkbox input[type='checkbox'] { + display: none; +} +.swagger-ui .checkbox input[type='checkbox'] + label > .item { + background: #e8e8e8; + border-radius: 1px; + box-shadow: 0 0 0 2px #e8e8e8; + cursor: pointer; + display: inline-block; + flex: none; + height: 16px; + margin: 0 8px 0 0; + padding: 5px; + position: relative; + top: 3px; + width: 16px; +} +.swagger-ui .checkbox input[type='checkbox'] + label > .item:active { + transform: scale(0.9); +} +.swagger-ui .checkbox input[type='checkbox']:checked + label > .item { + background: #e8e8e8 + url('data:image/svg+xml;charset=utf-8,') + 50% no-repeat; +} +.swagger-ui .dialog-ux { + bottom: 0; + left: 0; + position: fixed; + right: 0; + top: 0; + z-index: 9999; +} +.swagger-ui .dialog-ux .backdrop-ux { + background: rgba(0, 0, 0, 0.8); + bottom: 0; + left: 0; + position: fixed; + right: 0; + top: 0; +} +.swagger-ui .dialog-ux .modal-ux { + background: #fff; + border: 1px solid #ebebeb; + border-radius: 4px; + box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.2); + left: 50%; + max-width: 650px; + min-width: 300px; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + width: 100%; + z-index: 9999; +} +.swagger-ui .dialog-ux .modal-ux-content { + max-height: 540px; + overflow-y: auto; + padding: 20px; +} +.swagger-ui .dialog-ux .modal-ux-content p { + color: #41444e; + color: #3b4151; + font-family: sans-serif; + font-size: 12px; + margin: 0 0 5px; +} +.swagger-ui .dialog-ux .modal-ux-content h4 { + color: #3b4151; + font-family: sans-serif; + font-size: 18px; + font-weight: 600; + margin: 15px 0 0; +} +.swagger-ui .dialog-ux .modal-ux-header { + align-items: center; + border-bottom: 1px solid #ebebeb; + display: flex; + padding: 12px 0; +} +.swagger-ui .dialog-ux .modal-ux-header .close-modal { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background: none; + border: none; + padding: 0 10px; +} +.swagger-ui .dialog-ux .modal-ux-header h3 { + color: #3b4151; + flex: 1; + font-family: sans-serif; + font-size: 20px; + font-weight: 600; + margin: 0; + padding: 0 20px; +} +.swagger-ui .model { + color: #3b4151; + font-family: monospace; + font-size: 12px; + font-weight: 300; + font-weight: 600; +} +.swagger-ui .model .deprecated span, +.swagger-ui .model .deprecated td { + color: #a0a0a0 !important; +} +.swagger-ui .model .deprecated > td:first-of-type { + -webkit-text-decoration: line-through; + text-decoration: line-through; +} +.swagger-ui .model-toggle { + cursor: pointer; + display: inline-block; + font-size: 10px; + margin: auto 0.3em; + position: relative; + top: 6px; + transform: rotate(90deg); + transform-origin: 50% 50%; + transition: transform 0.15s ease-in; +} +.swagger-ui .model-toggle.collapsed { + transform: rotate(0deg); +} +.swagger-ui .model-toggle:after { + background: url('data:image/svg+xml;charset=utf-8,') + 50% no-repeat; + background-size: 100%; + content: ''; + display: block; + height: 20px; + width: 20px; +} +.swagger-ui .model-jump-to-path { + cursor: pointer; + position: relative; +} +.swagger-ui .model-jump-to-path .view-line-link { + cursor: pointer; + position: absolute; + top: -0.4em; +} +.swagger-ui .model-title { + position: relative; +} +.swagger-ui .model-title:hover .model-hint { + visibility: visible; +} +.swagger-ui .model-hint { + background: rgba(0, 0, 0, 0.7); + border-radius: 4px; + color: #ebebeb; + padding: 0.1em 0.5em; + position: absolute; + top: -1.8em; + visibility: hidden; + white-space: nowrap; +} +.swagger-ui .model p { + margin: 0 0 1em; +} +.swagger-ui .model .property { + color: #999; + font-style: italic; +} +.swagger-ui .model .property.primitive { + color: #6b6b6b; +} +.swagger-ui .model .external-docs, +.swagger-ui table.model tr.description { + color: #666; + font-weight: 400; +} +.swagger-ui table.model tr.description td:first-child, +.swagger-ui table.model tr.property-row.required td:first-child { + font-weight: 700; +} +.swagger-ui table.model tr.property-row td { + vertical-align: top; +} +.swagger-ui table.model tr.property-row td:first-child { + padding-right: 0.2em; +} +.swagger-ui table.model tr.property-row .star { + color: red; +} +.swagger-ui table.model tr.extension { + color: #777; +} +.swagger-ui table.model tr.extension td:last-child { + vertical-align: top; +} +.swagger-ui table.model tr.external-docs td:first-child { + font-weight: 700; +} +.swagger-ui table.model tr .renderedMarkdown p:first-child { + margin-top: 0; +} +.swagger-ui section.models { + border: 1px solid rgba(59, 65, 81, 0.3); + border-radius: 4px; + margin: 30px 0; +} +.swagger-ui section.models .pointer { + cursor: pointer; +} +.swagger-ui section.models.is-open { + padding: 0 0 20px; +} +.swagger-ui section.models.is-open h4 { + border-bottom: 1px solid rgba(59, 65, 81, 0.3); + margin: 0 0 5px; +} +.swagger-ui section.models h4 { + align-items: center; + color: #606060; + cursor: pointer; + display: flex; + font-family: sans-serif; + font-size: 16px; + margin: 0; + padding: 10px 20px 10px 10px; + transition: all 0.2s; +} +.swagger-ui section.models h4 svg { + transition: all 0.4s; +} +.swagger-ui section.models h4 span { + flex: 1; +} +.swagger-ui section.models h4:hover { + background: rgba(0, 0, 0, 0.02); +} +.swagger-ui section.models h5 { + color: #707070; + font-family: sans-serif; + font-size: 16px; + margin: 0 0 10px; +} +.swagger-ui section.models .model-jump-to-path { + position: relative; + top: 5px; +} +.swagger-ui section.models .model-container { + background: rgba(0, 0, 0, 0.05); + border-radius: 4px; + margin: 0 20px 15px; + position: relative; + transition: all 0.5s; +} +.swagger-ui section.models .model-container:hover { + background: rgba(0, 0, 0, 0.07); +} +.swagger-ui section.models .model-container:first-of-type { + margin: 20px; +} +.swagger-ui section.models .model-container:last-of-type { + margin: 0 20px; +} +.swagger-ui section.models .model-container .models-jump-to-path { + opacity: 0.65; + position: absolute; + right: 5px; + top: 8px; +} +.swagger-ui section.models .model-box { + background: none; +} +.swagger-ui .model-box { + background: rgba(0, 0, 0, 0.1); + border-radius: 4px; + display: inline-block; + padding: 10px; +} +.swagger-ui .model-box .model-jump-to-path { + position: relative; + top: 4px; +} +.swagger-ui .model-box.deprecated { + opacity: 0.5; +} +.swagger-ui .model-title { + color: #505050; + font-family: sans-serif; + font-size: 16px; +} +.swagger-ui .model-title img { + bottom: 0; + margin-left: 1em; + position: relative; +} +.swagger-ui .model-deprecated-warning { + color: #f93e3e; + font-family: sans-serif; + font-size: 16px; + font-weight: 600; + margin-right: 1em; +} +.swagger-ui span > span.model .brace-close { + padding: 0 0 0 10px; +} +.swagger-ui .prop-name { + display: inline-block; + margin-right: 1em; +} +.swagger-ui .prop-type { + color: #55a; +} +.swagger-ui .prop-enum { + display: block; +} +.swagger-ui .prop-format { + color: #606060; +} +.swagger-ui .servers > label { + color: #3b4151; + font-family: sans-serif; + font-size: 12px; + margin: -20px 15px 0 0; +} +.swagger-ui .servers > label select { + max-width: 100%; + min-width: 130px; + width: 100%; +} +.swagger-ui .servers h4.message { + padding-bottom: 2em; +} +.swagger-ui .servers table tr { + width: 30em; +} +.swagger-ui .servers table td { + display: inline-block; + max-width: 15em; + padding-bottom: 10px; + padding-top: 10px; + vertical-align: middle; +} +.swagger-ui .servers table td:first-of-type { + padding-right: 1em; +} +.swagger-ui .servers table td input { + height: 100%; + width: 100%; +} +.swagger-ui .servers .computed-url { + margin: 2em 0; +} +.swagger-ui .servers .computed-url code { + display: inline-block; + font-size: 16px; + margin: 0 1em; + padding: 4px; +} +.swagger-ui .servers-title { + font-size: 12px; + font-weight: 700; +} +.swagger-ui .operation-servers h4.message { + margin-bottom: 2em; +} +.swagger-ui table { + border-collapse: collapse; + padding: 0 10px; + width: 100%; +} +.swagger-ui table.model tbody tr td { + padding: 0; + vertical-align: top; +} +.swagger-ui table.model tbody tr td:first-of-type { + padding: 0 0 0 2em; + width: 174px; +} +.swagger-ui table.headers td { + color: #3b4151; + font-family: monospace; + font-size: 12px; + font-weight: 300; + font-weight: 600; + vertical-align: middle; +} +.swagger-ui table.headers .header-example { + color: #999; + font-style: italic; +} +.swagger-ui table tbody tr td { + padding: 10px 0 0; + vertical-align: top; +} +.swagger-ui table tbody tr td:first-of-type { + min-width: 6em; + padding: 10px 0; +} +.swagger-ui table thead tr td, +.swagger-ui table thead tr th { + border-bottom: 1px solid rgba(59, 65, 81, 0.2); + color: #3b4151; + font-family: sans-serif; + font-size: 12px; + font-weight: 700; + padding: 12px 0; + text-align: left; +} +.swagger-ui .parameters-col_description { + margin-bottom: 2em; + width: 99%; +} +.swagger-ui .parameters-col_description input { + max-width: 340px; + width: 100%; +} +.swagger-ui .parameters-col_description select { + border-width: 1px; +} +.swagger-ui .parameters-col_description .markdown p, +.swagger-ui .parameters-col_description .renderedMarkdown p { + margin: 0; +} +.swagger-ui .parameter__name { + color: #3b4151; + font-family: sans-serif; + font-size: 16px; + font-weight: 400; + margin-right: 0.75em; +} +.swagger-ui .parameter__name.required { + font-weight: 700; +} +.swagger-ui .parameter__name.required span { + color: red; +} +.swagger-ui .parameter__name.required:after { + color: rgba(255, 0, 0, 0.6); + content: 'required'; + font-size: 10px; + padding: 5px; + position: relative; + top: -6px; +} +.swagger-ui .parameter__extension, +.swagger-ui .parameter__in { + color: grey; + font-family: monospace; + font-size: 12px; + font-style: italic; + font-weight: 600; +} +.swagger-ui .parameter__deprecated { + color: red; + font-family: monospace; + font-size: 12px; + font-style: italic; + font-weight: 600; +} +.swagger-ui .parameter__empty_value_toggle { + display: block; + font-size: 13px; + padding-bottom: 12px; + padding-top: 5px; +} +.swagger-ui .parameter__empty_value_toggle input { + margin-right: 7px; + width: auto; +} +.swagger-ui .parameter__empty_value_toggle.disabled { + opacity: 0.7; +} +.swagger-ui .table-container { + padding: 20px; +} +.swagger-ui .response-col_description { + width: 99%; +} +.swagger-ui .response-col_description .markdown p, +.swagger-ui .response-col_description .renderedMarkdown p { + margin: 0; +} +.swagger-ui .response-col_links { + min-width: 6em; +} +.swagger-ui .response__extension { + color: grey; + font-family: monospace; + font-size: 12px; + font-style: italic; + font-weight: 600; +} +.swagger-ui .topbar { + background-color: #1b1b1b; + padding: 10px 0; +} +.swagger-ui .topbar .topbar-wrapper { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 10px; +} +@media (max-width: 550px) { + .swagger-ui .topbar .topbar-wrapper { + align-items: start; + flex-direction: column; + } +} +.swagger-ui .topbar a { + align-items: center; + color: #fff; + display: flex; + flex: 1; + font-family: sans-serif; + font-size: 1.5em; + font-weight: 700; + max-width: 300px; + -webkit-text-decoration: none; + text-decoration: none; +} +.swagger-ui .topbar a span { + margin: 0; + padding: 0 10px; +} +.swagger-ui .topbar .download-url-wrapper { + display: flex; + flex: 3; + justify-content: flex-end; +} +.swagger-ui .topbar .download-url-wrapper input[type='text'] { + border: 2px solid #62a03f; + border-radius: 4px 0 0 4px; + margin: 0; + max-width: 100%; + outline: none; + width: 100%; +} +.swagger-ui .topbar .download-url-wrapper .select-label { + align-items: center; + color: #f0f0f0; + display: flex; + margin: 0; + max-width: 600px; + width: 100%; +} +.swagger-ui .topbar .download-url-wrapper .select-label span { + flex: 1; + font-size: 16px; + padding: 0 10px 0 0; + text-align: right; +} +.swagger-ui .topbar .download-url-wrapper .select-label select { + border: 2px solid #62a03f; + box-shadow: none; + flex: 2; + outline: none; + width: 100%; +} +.swagger-ui .topbar .download-url-wrapper .download-url-button { + background: #62a03f; + border: none; + border-radius: 0 4px 4px 0; + color: #fff; + font-family: sans-serif; + font-size: 16px; + font-weight: 700; + padding: 4px 30px; +} +@media (max-width: 550px) { + .swagger-ui .topbar .download-url-wrapper { + width: 100%; + } +} +.swagger-ui .info { + margin: 50px 0; +} +.swagger-ui .info.failed-config { + margin-left: auto; + margin-right: auto; + max-width: 880px; + text-align: center; +} +.swagger-ui .info hgroup.main { + margin: 0 0 20px; +} +.swagger-ui .info hgroup.main a { + font-size: 12px; +} +.swagger-ui .info pre { + font-size: 14px; +} +.swagger-ui .info li, +.swagger-ui .info p, +.swagger-ui .info table { + color: #3b4151; + font-family: sans-serif; + font-size: 14px; +} +.swagger-ui .info h1, +.swagger-ui .info h2, +.swagger-ui .info h3, +.swagger-ui .info h4, +.swagger-ui .info h5 { + color: #3b4151; + font-family: sans-serif; +} +.swagger-ui .info a { + color: #4990e2; + font-family: sans-serif; + font-size: 14px; + transition: all 0.4s; +} +.swagger-ui .info a:hover { + color: #1f69c0; +} +.swagger-ui .info > div { + margin: 0 0 5px; +} +.swagger-ui .info .base-url { + color: #3b4151; + font-family: monospace; + font-size: 12px; + font-weight: 300 !important; + font-weight: 600; + margin: 0; +} +.swagger-ui .info .title { + color: #3b4151; + font-family: sans-serif; + font-size: 36px; + margin: 0; +} +.swagger-ui .info .title small { + background: #7d8492; + border-radius: 57px; + display: inline-block; + font-size: 10px; + margin: 0 0 0 5px; + padding: 2px 4px; + position: relative; + top: -5px; + vertical-align: super; +} +.swagger-ui .info .title small.version-stamp { + background-color: #89bf04; +} +.swagger-ui .info .title small pre { + color: #fff; + font-family: sans-serif; + margin: 0; + padding: 0; +} +.swagger-ui .auth-btn-wrapper { + display: flex; + justify-content: center; + padding: 10px 0; +} +.swagger-ui .auth-btn-wrapper .btn-done { + margin-right: 1em; +} +.swagger-ui .auth-wrapper { + display: flex; + flex: 1; + justify-content: flex-end; +} +.swagger-ui .auth-wrapper .authorize { + margin-left: 10px; + margin-right: 10px; + padding-right: 20px; +} +.swagger-ui .auth-container { + border-bottom: 1px solid #ebebeb; + margin: 0 0 10px; + padding: 10px 20px; +} +.swagger-ui .auth-container:last-of-type { + border: 0; + margin: 0; + padding: 10px 20px; +} +.swagger-ui .auth-container h4 { + margin: 5px 0 15px !important; +} +.swagger-ui .auth-container .wrapper { + margin: 0; + padding: 0; +} +.swagger-ui .auth-container input[type='password'], +.swagger-ui .auth-container input[type='text'] { + min-width: 230px; +} +.swagger-ui .auth-container .errors { + background-color: #fee; + border-radius: 4px; + color: red; + color: #3b4151; + font-family: monospace; + font-size: 12px; + font-weight: 600; + margin: 1em; + padding: 10px; +} +.swagger-ui .auth-container .errors b { + margin-right: 1em; + text-transform: capitalize; +} +.swagger-ui .scopes h2 { + color: #3b4151; + font-family: sans-serif; + font-size: 14px; +} +.swagger-ui .scopes h2 a { + color: #4990e2; + cursor: pointer; + font-size: 12px; + padding-left: 10px; + -webkit-text-decoration: underline; + text-decoration: underline; +} +.swagger-ui .scope-def { + padding: 0 0 20px; +} +.swagger-ui .errors-wrapper { + animation: scaleUp 0.5s; + background: rgba(249, 62, 62, 0.1); + border: 2px solid #f93e3e; + border-radius: 4px; + margin: 20px; + padding: 10px 20px; +} +.swagger-ui .errors-wrapper .error-wrapper { + margin: 0 0 10px; +} +.swagger-ui .errors-wrapper .errors h4 { + color: #3b4151; + font-family: monospace; + font-size: 14px; + font-weight: 600; + margin: 0; +} +.swagger-ui .errors-wrapper .errors small { + color: #606060; +} +.swagger-ui .errors-wrapper .errors .message { + white-space: pre-line; +} +.swagger-ui .errors-wrapper .errors .message.thrown { + max-width: 100%; +} +.swagger-ui .errors-wrapper .errors .error-line { + cursor: pointer; + -webkit-text-decoration: underline; + text-decoration: underline; +} +.swagger-ui .errors-wrapper hgroup { + align-items: center; + display: flex; +} +.swagger-ui .errors-wrapper hgroup h4 { + color: #3b4151; + flex: 1; + font-family: sans-serif; + font-size: 20px; + margin: 0; +} +@keyframes scaleUp { + 0% { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: scale(1); + } +} +.swagger-ui .Resizer.vertical.disabled { + display: none; +} +.swagger-ui .markdown p, +.swagger-ui .markdown pre, +.swagger-ui .renderedMarkdown p, +.swagger-ui .renderedMarkdown pre { + margin: 1em auto; + word-break: break-all; + word-break: break-word; +} +.swagger-ui .markdown pre, +.swagger-ui .renderedMarkdown pre { + background: none; + color: #000; + font-weight: 400; + padding: 0; + white-space: pre-wrap; +} +.swagger-ui .markdown code, +.swagger-ui .renderedMarkdown code { + background: rgba(0, 0, 0, 0.05); + border-radius: 4px; + color: #9012fe; + font-family: monospace; + font-size: 14px; + font-weight: 600; + padding: 5px 7px; +} +.swagger-ui .markdown pre > code, +.swagger-ui .renderedMarkdown pre > code { + display: block; +} +.swagger-ui .json-schema-2020-12 { + background-color: rgba(0, 0, 0, 0.05); + border-radius: 4px; + margin: 0 20px 15px; + padding: 12px 0 12px 20px; +} +.swagger-ui .json-schema-2020-12:first-of-type { + margin: 20px; +} +.swagger-ui .json-schema-2020-12:last-of-type { + margin: 0 20px; +} +.swagger-ui .json-schema-2020-12--embedded { + background-color: inherit; + padding-bottom: 0; + padding-left: inherit; + padding-right: inherit; + padding-top: 0; +} +.swagger-ui .json-schema-2020-12-body { + border-left: 1px dashed rgba(0, 0, 0, 0.1); + margin: 2px 0; +} +.swagger-ui .json-schema-2020-12-body--collapsed { + display: none; +} +.swagger-ui .json-schema-2020-12-accordion { + border: none; + outline: none; + padding-left: 0; +} +.swagger-ui .json-schema-2020-12-accordion__children { + display: inline-block; +} +.swagger-ui .json-schema-2020-12-accordion__icon { + display: inline-block; + height: 18px; + vertical-align: bottom; + width: 18px; +} +.swagger-ui .json-schema-2020-12-accordion__icon--expanded { + transform: rotate(-90deg); + transform-origin: 50% 50%; + transition: transform 0.15s ease-in; +} +.swagger-ui .json-schema-2020-12-accordion__icon--collapsed { + transform: rotate(0deg); + transform-origin: 50% 50%; + transition: transform 0.15s ease-in; +} +.swagger-ui .json-schema-2020-12-accordion__icon svg { + height: 20px; + width: 20px; +} +.swagger-ui .json-schema-2020-12-expand-deep-button { + border: none; + color: #505050; + color: #afaeae; + font-family: sans-serif; + font-size: 12px; + padding-right: 0; +} +.swagger-ui .json-schema-2020-12-keyword { + margin: 5px 0; +} +.swagger-ui .json-schema-2020-12-keyword__children { + border-left: 1px dashed rgba(0, 0, 0, 0.1); + margin: 0 0 0 20px; + padding: 0; +} +.swagger-ui .json-schema-2020-12-keyword__children--collapsed { + display: none; +} +.swagger-ui .json-schema-2020-12-keyword__name { + font-size: 12px; + font-weight: 700; + margin-left: 20px; +} +.swagger-ui .json-schema-2020-12-keyword__name--primary { + color: #3b4151; + font-style: normal; +} +.swagger-ui .json-schema-2020-12-keyword__name--secondary { + color: #6b6b6b; + font-style: italic; +} +.swagger-ui .json-schema-2020-12-keyword__value { + color: #6b6b6b; + font-size: 12px; + font-style: italic; + font-weight: 400; +} +.swagger-ui .json-schema-2020-12-keyword__value--primary { + color: #3b4151; + font-style: normal; +} +.swagger-ui .json-schema-2020-12-keyword__value--secondary { + color: #6b6b6b; + font-style: italic; +} +.swagger-ui .json-schema-2020-12-keyword__value--const, +.swagger-ui .json-schema-2020-12-keyword__value--warning { + border: 1px dashed #6b6b6b; + border-radius: 4px; + color: #3b4151; + color: #6b6b6b; + display: inline-block; + font-family: monospace; + font-style: normal; + font-weight: 600; + line-height: 1.5; + margin-left: 10px; + padding: 1px 4px; +} +.swagger-ui .json-schema-2020-12-keyword__value--warning { + border: 1px dashed red; + color: red; +} +.swagger-ui + .json-schema-2020-12-keyword__name--secondary + + .json-schema-2020-12-keyword__value--secondary:before { + content: '='; +} +.swagger-ui .json-schema-2020-12__attribute { + color: #3b4151; + font-family: monospace; + font-size: 12px; + padding-left: 10px; + text-transform: lowercase; +} +.swagger-ui .json-schema-2020-12__attribute--primary { + color: #55a; +} +.swagger-ui .json-schema-2020-12__attribute--muted { + color: gray; +} +.swagger-ui .json-schema-2020-12__attribute--warning { + color: red; +} +.swagger-ui .json-schema-2020-12-keyword--\$vocabulary ul { + border-left: 1px dashed rgba(0, 0, 0, 0.1); + margin: 0 0 0 20px; +} +.swagger-ui .json-schema-2020-12-\$vocabulary-uri { + margin-left: 35px; +} +.swagger-ui .json-schema-2020-12-\$vocabulary-uri--disabled { + -webkit-text-decoration: line-through; + text-decoration: line-through; +} +.swagger-ui .json-schema-2020-12-keyword--description { + color: #6b6b6b; + font-size: 12px; + margin-left: 20px; +} +.swagger-ui .json-schema-2020-12-keyword--description p { + margin: 0; +} +.swagger-ui .json-schema-2020-12__title { + color: #505050; + display: inline-block; + font-family: sans-serif; + font-size: 12px; + font-weight: 700; + line-height: normal; +} +.swagger-ui .json-schema-2020-12__title .json-schema-2020-12-keyword__name { + margin: 0; +} +.swagger-ui .json-schema-2020-12-property { + margin: 7px 0; +} +.swagger-ui .json-schema-2020-12-property .json-schema-2020-12__title { + color: #3b4151; + font-family: monospace; + font-size: 12px; + font-weight: 600; + vertical-align: middle; +} +.swagger-ui .json-schema-2020-12-keyword--properties > ul { + border: none; + margin: 0; + padding: 0; +} +.swagger-ui .json-schema-2020-12-property { + list-style-type: none; +} +.swagger-ui + .json-schema-2020-12-property--required + > .json-schema-2020-12:first-of-type + > .json-schema-2020-12-head + .json-schema-2020-12__title:after { + color: red; + content: '*'; + font-weight: 700; +} +.swagger-ui .json-schema-2020-12-keyword--patternProperties ul { + border: none; + margin: 0; + padding: 0; +} +.swagger-ui + .json-schema-2020-12-keyword--patternProperties + .json-schema-2020-12__title:first-of-type:after, +.swagger-ui + .json-schema-2020-12-keyword--patternProperties + .json-schema-2020-12__title:first-of-type:before { + color: #55a; + content: '/'; +} +.swagger-ui .json-schema-2020-12-keyword--enum > ul { + display: inline-block; + margin: 0; + padding: 0; +} +.swagger-ui .json-schema-2020-12-keyword--enum > ul li { + display: inline; + list-style-type: none; +} +.swagger-ui .json-schema-2020-12__constraint { + background-color: #805ad5; + border-radius: 4px; + color: #3b4151; + color: #fff; + font-family: monospace; + font-weight: 600; + line-height: 1.5; + margin-left: 10px; + padding: 1px 3px; +} +.swagger-ui .json-schema-2020-12__constraint--string { + background-color: #d69e2e; + color: #fff; +} +.swagger-ui .json-schema-2020-12-keyword--dependentRequired > ul { + display: inline-block; + margin: 0; + padding: 0; +} +.swagger-ui .json-schema-2020-12-keyword--dependentRequired > ul li { + display: inline; + list-style-type: none; +} +.swagger-ui + .model-box + .json-schema-2020-12:not(.json-schema-2020-12--embedded) + > .json-schema-2020-12-head + .json-schema-2020-12__title:first-of-type { + font-size: 16px; +} +.swagger-ui .model-box > .json-schema-2020-12 { + margin: 0; +} +.swagger-ui .model-box .json-schema-2020-12 { + background-color: transparent; + padding: 0; +} +.swagger-ui .model-box .json-schema-2020-12-accordion, +.swagger-ui .model-box .json-schema-2020-12-expand-deep-button { + background-color: transparent; +} +.swagger-ui + .models + .json-schema-2020-12:not(.json-schema-2020-12--embedded) + > .json-schema-2020-12-head + .json-schema-2020-12__title:first-of-type { + font-size: 16px; +} diff --git a/backend/open_webui/static/user-import.csv b/backend/open_webui/static/user-import.csv new file mode 100644 index 0000000000000000000000000000000000000000..918a92aad71d708ae13fedb8b91f79c29a5b3e9d --- /dev/null +++ b/backend/open_webui/static/user-import.csv @@ -0,0 +1 @@ +Name,Email,Password,Role diff --git a/backend/open_webui/static/user.png b/backend/open_webui/static/user.png new file mode 100644 index 0000000000000000000000000000000000000000..7bdc70d159cfb0a032bec208e3775ab82755b299 Binary files /dev/null and b/backend/open_webui/static/user.png differ diff --git a/backend/open_webui/static/web-app-manifest-192x192.png b/backend/open_webui/static/web-app-manifest-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..fbd2eab6e2b8bcd7c510fca905f432c93a4f2ebc Binary files /dev/null and b/backend/open_webui/static/web-app-manifest-192x192.png differ diff --git a/backend/open_webui/static/web-app-manifest-512x512.png b/backend/open_webui/static/web-app-manifest-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..afebe2cd0820c3e18856a6a244d5b1526716340e Binary files /dev/null and b/backend/open_webui/static/web-app-manifest-512x512.png differ diff --git a/backend/open_webui/storage/provider.py b/backend/open_webui/storage/provider.py new file mode 100644 index 0000000000000000000000000000000000000000..f70f3e862b569d55432cab562c9bae3f8711968e --- /dev/null +++ b/backend/open_webui/storage/provider.py @@ -0,0 +1,348 @@ +import os +import shutil +import json +import logging +import re +from abc import ABC, abstractmethod +from typing import BinaryIO, Tuple, Dict + +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError +from open_webui.config import ( + S3_ACCESS_KEY_ID, + S3_BUCKET_NAME, + S3_ENDPOINT_URL, + S3_KEY_PREFIX, + S3_REGION_NAME, + S3_SECRET_ACCESS_KEY, + S3_USE_ACCELERATE_ENDPOINT, + S3_ADDRESSING_STYLE, + S3_ENABLE_TAGGING, + GCS_BUCKET_NAME, + GOOGLE_APPLICATION_CREDENTIALS_JSON, + AZURE_STORAGE_ENDPOINT, + AZURE_STORAGE_CONTAINER_NAME, + AZURE_STORAGE_KEY, + STORAGE_PROVIDER, + UPLOAD_DIR, +) +from google.cloud import storage +from google.cloud.exceptions import GoogleCloudError, NotFound +from open_webui.constants import ERROR_MESSAGES +from azure.identity import DefaultAzureCredential +from azure.storage.blob import BlobServiceClient +from azure.core.exceptions import ResourceNotFoundError + +log = logging.getLogger(__name__) + + +class StorageProvider(ABC): + @abstractmethod + def get_file(self, file_path: str) -> str: + pass + + @abstractmethod + def upload_file(self, file: BinaryIO, filename: str, tags: Dict[str, str]) -> Tuple[bytes, str]: + pass + + @abstractmethod + def delete_all_files(self) -> None: + pass + + @abstractmethod + def delete_file(self, file_path: str) -> None: + pass + + +class LocalStorageProvider(StorageProvider): + @staticmethod + def upload_file(file: BinaryIO, filename: str, tags: Dict[str, str]) -> Tuple[bytes, str]: + contents = file.read() + if not contents: + raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT) + file_path = os.path.join(UPLOAD_DIR, filename) + with open(file_path, 'wb') as f: + f.write(contents) + return contents, file_path + + @staticmethod + def get_file(file_path: str) -> str: + """Handles downloading of the file from local storage.""" + return file_path + + @staticmethod + def delete_file(file_path: str) -> None: + """Handles deletion of the file from local storage.""" + filename = os.path.basename(file_path) + file_path = os.path.join(UPLOAD_DIR, filename) + if os.path.isfile(file_path): + os.remove(file_path) + else: + log.warning(f'File {file_path} not found in local storage.') + + @staticmethod + def delete_all_files() -> None: + """Handles deletion of all files from local storage.""" + if os.path.exists(UPLOAD_DIR): + for filename in os.listdir(UPLOAD_DIR): + file_path = os.path.join(UPLOAD_DIR, filename) + try: + if os.path.isfile(file_path) or os.path.islink(file_path): + os.unlink(file_path) # Remove the file or link + elif os.path.isdir(file_path): + shutil.rmtree(file_path) # Remove the directory + except Exception as e: + log.exception(f'Failed to delete {file_path}. Reason: {e}') + else: + log.warning(f'Directory {UPLOAD_DIR} not found in local storage.') + + +class S3StorageProvider(StorageProvider): + def __init__(self): + config = Config( + s3={ + 'use_accelerate_endpoint': S3_USE_ACCELERATE_ENDPOINT, + 'addressing_style': S3_ADDRESSING_STYLE, + }, + # KIT change - see https://github.com/boto/boto3/issues/4400#issuecomment-2600742103∆ + request_checksum_calculation='when_required', + response_checksum_validation='when_required', + ) + + # If access key and secret are provided, use them for authentication + if S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY: + self.s3_client = boto3.client( + 's3', + region_name=S3_REGION_NAME, + endpoint_url=S3_ENDPOINT_URL, + aws_access_key_id=S3_ACCESS_KEY_ID, + aws_secret_access_key=S3_SECRET_ACCESS_KEY, + config=config, + ) + else: + # If no explicit credentials are provided, fall back to default AWS credentials + # This supports workload identity (IAM roles for EC2, EKS, etc.) + self.s3_client = boto3.client( + 's3', + region_name=S3_REGION_NAME, + endpoint_url=S3_ENDPOINT_URL, + config=config, + ) + + self.bucket_name = S3_BUCKET_NAME + self.key_prefix = S3_KEY_PREFIX if S3_KEY_PREFIX else '' + + @staticmethod + def sanitize_tag_value(s: str) -> str: + """Only include S3 allowed characters.""" + return re.sub(r'[^a-zA-Z0-9 äöüÄÖÜß\+\-=\._:/@]', '', s) + + def upload_file(self, file: BinaryIO, filename: str, tags: Dict[str, str]) -> Tuple[bytes, str]: + """Handles uploading of the file to S3 storage.""" + contents, file_path = LocalStorageProvider.upload_file(file, filename, tags) + s3_key = os.path.join(self.key_prefix, filename) + try: + self.s3_client.upload_file(file_path, self.bucket_name, s3_key) + if S3_ENABLE_TAGGING and tags: + sanitized_tags = {self.sanitize_tag_value(k): self.sanitize_tag_value(v) for k, v in tags.items()} + tagging = {'TagSet': [{'Key': k, 'Value': v} for k, v in sanitized_tags.items()]} + self.s3_client.put_object_tagging( + Bucket=self.bucket_name, + Key=s3_key, + Tagging=tagging, + ) + return ( + contents, + f's3://{self.bucket_name}/{s3_key}', + ) + except ClientError as e: + raise RuntimeError(f'Error uploading file to S3: {e}') + + def get_file(self, file_path: str) -> str: + """Handles downloading of the file from S3 storage.""" + try: + s3_key = self._extract_s3_key(file_path) + local_file_path = self._get_local_file_path(s3_key) + self.s3_client.download_file(self.bucket_name, s3_key, local_file_path) + return local_file_path + except ClientError as e: + raise RuntimeError(f'Error downloading file from S3: {e}') + + def delete_file(self, file_path: str) -> None: + """Handles deletion of the file from S3 storage.""" + try: + s3_key = self._extract_s3_key(file_path) + self.s3_client.delete_object(Bucket=self.bucket_name, Key=s3_key) + except ClientError as e: + raise RuntimeError(f'Error deleting file from S3: {e}') + + # Always delete from local storage + LocalStorageProvider.delete_file(file_path) + + def delete_all_files(self) -> None: + """Handles deletion of all files from S3 storage.""" + try: + response = self.s3_client.list_objects_v2(Bucket=self.bucket_name) + if 'Contents' in response: + for content in response['Contents']: + # Skip objects that were not uploaded from open-webui in the first place + if not content['Key'].startswith(self.key_prefix): + continue + + self.s3_client.delete_object(Bucket=self.bucket_name, Key=content['Key']) + except ClientError as e: + raise RuntimeError(f'Error deleting all files from S3: {e}') + + # Always delete from local storage + LocalStorageProvider.delete_all_files() + + # The s3 key is the name assigned to an object. It excludes the bucket name, but includes the internal path and the file name. + def _extract_s3_key(self, full_file_path: str) -> str: + return '/'.join(full_file_path.split('//')[1].split('/')[1:]) + + def _get_local_file_path(self, s3_key: str) -> str: + return os.path.join(UPLOAD_DIR, s3_key.split('/')[-1]) + + +class GCSStorageProvider(StorageProvider): + def __init__(self): + self.bucket_name = GCS_BUCKET_NAME + + if GOOGLE_APPLICATION_CREDENTIALS_JSON: + self.gcs_client = storage.Client.from_service_account_info( + info=json.loads(GOOGLE_APPLICATION_CREDENTIALS_JSON) + ) + else: + # if no credentials json is provided, credentials will be picked up from the environment + # if running on local environment, credentials would be user credentials + # if running on a Compute Engine instance, credentials would be from Google Metadata server + self.gcs_client = storage.Client() + self.bucket = self.gcs_client.bucket(GCS_BUCKET_NAME) + + def upload_file(self, file: BinaryIO, filename: str, tags: Dict[str, str]) -> Tuple[bytes, str]: + """Handles uploading of the file to GCS storage.""" + contents, file_path = LocalStorageProvider.upload_file(file, filename, tags) + try: + blob = self.bucket.blob(filename) + blob.upload_from_filename(file_path) + return contents, 'gs://' + self.bucket_name + '/' + filename + except GoogleCloudError as e: + raise RuntimeError(f'Error uploading file to GCS: {e}') + + def get_file(self, file_path: str) -> str: + """Handles downloading of the file from GCS storage.""" + try: + filename = file_path.removeprefix('gs://').split('/')[1] + local_file_path = os.path.join(UPLOAD_DIR, filename) + blob = self.bucket.get_blob(filename) + blob.download_to_filename(local_file_path) + + return local_file_path + except NotFound as e: + raise RuntimeError(f'Error downloading file from GCS: {e}') + + def delete_file(self, file_path: str) -> None: + """Handles deletion of the file from GCS storage.""" + try: + filename = file_path.removeprefix('gs://').split('/')[1] + blob = self.bucket.get_blob(filename) + blob.delete() + except NotFound as e: + raise RuntimeError(f'Error deleting file from GCS: {e}') + + # Always delete from local storage + LocalStorageProvider.delete_file(file_path) + + def delete_all_files(self) -> None: + """Handles deletion of all files from GCS storage.""" + try: + blobs = self.bucket.list_blobs() + + for blob in blobs: + blob.delete() + + except NotFound as e: + raise RuntimeError(f'Error deleting all files from GCS: {e}') + + # Always delete from local storage + LocalStorageProvider.delete_all_files() + + +class AzureStorageProvider(StorageProvider): + def __init__(self): + self.endpoint = AZURE_STORAGE_ENDPOINT + self.container_name = AZURE_STORAGE_CONTAINER_NAME + storage_key = AZURE_STORAGE_KEY + + if storage_key: + # Configure using the Azure Storage Account Endpoint and Key + self.blob_service_client = BlobServiceClient(account_url=self.endpoint, credential=storage_key) + else: + # Configure using the Azure Storage Account Endpoint and DefaultAzureCredential + # If the key is not configured, then the DefaultAzureCredential will be used to support Managed Identity authentication + self.blob_service_client = BlobServiceClient(account_url=self.endpoint, credential=DefaultAzureCredential()) + self.container_client = self.blob_service_client.get_container_client(self.container_name) + + def upload_file(self, file: BinaryIO, filename: str, tags: Dict[str, str]) -> Tuple[bytes, str]: + """Handles uploading of the file to Azure Blob Storage.""" + contents, file_path = LocalStorageProvider.upload_file(file, filename, tags) + try: + blob_client = self.container_client.get_blob_client(filename) + blob_client.upload_blob(contents, overwrite=True) + return contents, f'{self.endpoint}/{self.container_name}/{filename}' + except Exception as e: + raise RuntimeError(f'Error uploading file to Azure Blob Storage: {e}') + + def get_file(self, file_path: str) -> str: + """Handles downloading of the file from Azure Blob Storage.""" + try: + filename = file_path.split('/')[-1] + local_file_path = os.path.join(UPLOAD_DIR, filename) + blob_client = self.container_client.get_blob_client(filename) + with open(local_file_path, 'wb') as download_file: + download_file.write(blob_client.download_blob().readall()) + return local_file_path + except ResourceNotFoundError as e: + raise RuntimeError(f'Error downloading file from Azure Blob Storage: {e}') + + def delete_file(self, file_path: str) -> None: + """Handles deletion of the file from Azure Blob Storage.""" + try: + filename = file_path.split('/')[-1] + blob_client = self.container_client.get_blob_client(filename) + blob_client.delete_blob() + except ResourceNotFoundError as e: + raise RuntimeError(f'Error deleting file from Azure Blob Storage: {e}') + + # Always delete from local storage + LocalStorageProvider.delete_file(file_path) + + def delete_all_files(self) -> None: + """Handles deletion of all files from Azure Blob Storage.""" + try: + blobs = self.container_client.list_blobs() + for blob in blobs: + self.container_client.delete_blob(blob.name) + except Exception as e: + raise RuntimeError(f'Error deleting all files from Azure Blob Storage: {e}') + + # Always delete from local storage + LocalStorageProvider.delete_all_files() + + +def get_storage_provider(storage_provider: str): + if storage_provider == 'local': + Storage = LocalStorageProvider() + elif storage_provider == 's3': + Storage = S3StorageProvider() + elif storage_provider == 'gcs': + Storage = GCSStorageProvider() + elif storage_provider == 'azure': + Storage = AzureStorageProvider() + else: + raise RuntimeError(f'Unsupported storage provider: {storage_provider}') + return Storage + + +Storage = get_storage_provider(STORAGE_PROVIDER) diff --git a/backend/open_webui/tasks.py b/backend/open_webui/tasks.py new file mode 100644 index 0000000000000000000000000000000000000000..30754cfc482f977c290a39029b834c80e55189a8 --- /dev/null +++ b/backend/open_webui/tasks.py @@ -0,0 +1,210 @@ +# tasks.py +import asyncio +from typing import Dict +from uuid import uuid4 +import json +import logging +from redis.asyncio import Redis +from fastapi import Request +from typing import Dict, List, Optional + +from open_webui.env import REDIS_KEY_PREFIX + +log = logging.getLogger(__name__) + +# A dictionary to keep track of active tasks +tasks: Dict[str, asyncio.Task] = {} +item_tasks = {} + + +REDIS_TASKS_KEY = f'{REDIS_KEY_PREFIX}:tasks' +REDIS_ITEM_TASKS_KEY = f'{REDIS_KEY_PREFIX}:tasks:item' +REDIS_PUBSUB_CHANNEL = f'{REDIS_KEY_PREFIX}:tasks:commands' + + +async def redis_task_command_listener(app): + redis: Redis = app.state.redis + pubsub = redis.pubsub() + await pubsub.subscribe(REDIS_PUBSUB_CHANNEL) + + async for message in pubsub.listen(): + if message['type'] != 'message': + continue + try: + command = json.loads(message['data']) + if command.get('action') == 'stop': + task_id = command.get('task_id') + local_task = tasks.get(task_id) + if local_task: + local_task.cancel() + except Exception as e: + log.exception(f'Error handling distributed task command: {e}') + + +### ------------------------------ +### REDIS-ENABLED HANDLERS +### ------------------------------ + + +async def redis_save_task(redis: Redis, task_id: str, item_id: Optional[str]): + pipe = redis.pipeline() + pipe.hset(REDIS_TASKS_KEY, task_id, item_id or '') + if item_id: + pipe.sadd(f'{REDIS_ITEM_TASKS_KEY}:{item_id}', task_id) + await pipe.execute() + + +async def redis_cleanup_task(redis: Redis, task_id: str, item_id: Optional[str]): + pipe = redis.pipeline() + pipe.hdel(REDIS_TASKS_KEY, task_id) + if item_id: + pipe.srem(f'{REDIS_ITEM_TASKS_KEY}:{item_id}', task_id) + await pipe.execute() + # Remove the set key entirely if no tasks remain for this item + if await redis.scard(f'{REDIS_ITEM_TASKS_KEY}:{item_id}') == 0: + await redis.delete(f'{REDIS_ITEM_TASKS_KEY}:{item_id}') + else: + await pipe.execute() + + +async def redis_list_tasks(redis: Redis) -> List[str]: + return list(await redis.hkeys(REDIS_TASKS_KEY)) + + +async def redis_list_item_tasks(redis: Redis, item_id: str) -> List[str]: + return list(await redis.smembers(f'{REDIS_ITEM_TASKS_KEY}:{item_id}')) + + +async def redis_send_command(redis: Redis, command: dict): + command_json = json.dumps(command) + # RedisCluster doesn't expose publish() directly, but the + # PUBLISH command broadcasts across all cluster nodes server-side. + if hasattr(redis, 'nodes_manager'): + await redis.execute_command('PUBLISH', REDIS_PUBSUB_CHANNEL, command_json) + else: + await redis.publish(REDIS_PUBSUB_CHANNEL, command_json) + + +async def cleanup_task(redis, task_id: str, id=None): + """ + Remove a completed or canceled task from the global `tasks` dictionary. + """ + if redis: + await redis_cleanup_task(redis, task_id, id) + + tasks.pop(task_id, None) # Remove the task if it exists + + # If an ID is provided, remove the task from the item_tasks dictionary + if id and task_id in item_tasks.get(id, []): + item_tasks[id].remove(task_id) + if not item_tasks[id]: # If no tasks left for this ID, remove the entry + item_tasks.pop(id, None) + + +async def create_task(redis, coroutine, id=None): + """ + Create a new asyncio task and add it to the global task dictionary. + """ + task_id = str(uuid4()) # Generate a unique ID for the task + task = asyncio.create_task(coroutine) # Create the task + + # Add a done callback for cleanup + 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 + if item_tasks.get(id): + item_tasks[id].append(task_id) + else: + item_tasks[id] = [task_id] + + if redis: + await redis_save_task(redis, task_id, id) + + return task_id, task + + +async def list_tasks(redis): + """ + List all currently active task IDs. + """ + if redis: + return await redis_list_tasks(redis) + return list(tasks.keys()) + + +async def list_task_ids_by_item_id(redis, id): + """ + List all tasks associated with a specific ID. + """ + if redis: + return await redis_list_item_tasks(redis, id) + return item_tasks.get(id, []) + + +async def stop_task(redis, task_id: str): + """ + Cancel a running task and remove it from the global task list. + """ + if redis: + # Look up the item_id before cleanup so we can remove the set entry too + item_id = await redis.hget(REDIS_TASKS_KEY, task_id) + # PUBSUB: All instances check if they have this task, and stop if so. + await redis_send_command( + redis, + { + 'action': 'stop', + 'task_id': task_id, + }, + ) + # Always clean Redis directly — hdel/srem are idempotent, safe even + # if the done_callback on the owning process also fires cleanup. + await redis_cleanup_task(redis, task_id, item_id or None) + return {'status': True, 'message': f'Task {task_id} stopped.'} + + task = tasks.pop(task_id, None) + if not task: + return {'status': False, 'message': f'Task with ID {task_id} not found.'} + + task.cancel() # Request task cancellation + try: + await task # Wait for the task to handle the cancellation + except asyncio.CancelledError: + # Task successfully canceled + return {'status': True, 'message': f'Task {task_id} successfully stopped.'} + + if task.cancelled() or task.done(): + return {'status': True, 'message': f'Task {task_id} successfully cancelled.'} + + return {'status': True, 'message': f'Cancellation requested for {task_id}.'} + + +async def stop_item_tasks(redis: Redis, item_id: str): + """ + Stop all tasks associated with a specific item ID. + """ + task_ids = await list_task_ids_by_item_id(redis, item_id) + if not task_ids: + return {'status': True, 'message': f'No tasks found for item {item_id}.'} + + for task_id in task_ids: + result = await stop_task(redis, task_id) + if not result['status']: + return result # Return the first failure + + return {'status': True, 'message': f'All tasks for item {item_id} stopped.'} + + +async def has_active_tasks(redis, chat_id: str) -> bool: + """Check if a chat has any active tasks.""" + task_ids = await list_task_ids_by_item_id(redis, chat_id) + return len(task_ids) > 0 + + +async def get_active_chat_ids(redis, chat_ids: List[str]) -> List[str]: + """Filter a list of chat_ids to only those with active tasks.""" + active = [] + for chat_id in chat_ids: + if await has_active_tasks(redis, chat_id): + active.append(chat_id) + return active diff --git a/backend/open_webui/test/__init__.py b/backend/open_webui/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/open_webui/test/apps/webui/routers/test_auths.py b/backend/open_webui/test/apps/webui/routers/test_auths.py new file mode 100644 index 0000000000000000000000000000000000000000..9f9ae9bc5c1cd47b41379e1ffe049bd123b14a60 --- /dev/null +++ b/backend/open_webui/test/apps/webui/routers/test_auths.py @@ -0,0 +1,196 @@ +from test.util.abstract_integration_test import AbstractPostgresTest +from test.util.mock_user import mock_webui_user + + +class TestAuths(AbstractPostgresTest): + BASE_PATH = '/api/v1/auths' + + def setup_class(cls): + super().setup_class() + from open_webui.models.auths import Auths + from open_webui.models.users import Users + + cls.users = Users + cls.auths = Auths + + def test_get_session_user(self): + with mock_webui_user(): + response = self.fast_api_client.get(self.create_url('')) + assert response.status_code == 200 + assert response.json() == { + 'id': '1', + 'name': 'John Doe', + 'email': 'john.doe@openwebui.com', + 'role': 'user', + 'profile_image_url': '/user.png', + } + + def test_update_profile(self): + from open_webui.utils.auth import get_password_hash + + user = self.auths.insert_new_auth( + email='john.doe@openwebui.com', + password=get_password_hash('old_password'), + name='John Doe', + profile_image_url='/user.png', + role='user', + ) + + with mock_webui_user(id=user.id): + response = self.fast_api_client.post( + self.create_url('/update/profile'), + json={'name': 'John Doe 2', 'profile_image_url': '/user2.png'}, + ) + assert response.status_code == 200 + db_user = self.users.get_user_by_id(user.id) + assert db_user.name == 'John Doe 2' + assert db_user.profile_image_url == '/user2.png' + + def test_update_password(self): + from open_webui.utils.auth import get_password_hash + + user = self.auths.insert_new_auth( + email='john.doe@openwebui.com', + password=get_password_hash('old_password'), + name='John Doe', + profile_image_url='/user.png', + role='user', + ) + + with mock_webui_user(id=user.id): + response = self.fast_api_client.post( + self.create_url('/update/password'), + json={'password': 'old_password', 'new_password': 'new_password'}, + ) + assert response.status_code == 200 + + old_auth = self.auths.authenticate_user('john.doe@openwebui.com', 'old_password') + assert old_auth is None + new_auth = self.auths.authenticate_user('john.doe@openwebui.com', 'new_password') + assert new_auth is not None + + def test_signin(self): + from open_webui.utils.auth import get_password_hash + + user = self.auths.insert_new_auth( + email='john.doe@openwebui.com', + password=get_password_hash('password'), + name='John Doe', + profile_image_url='/user.png', + role='user', + ) + response = self.fast_api_client.post( + self.create_url('/signin'), + json={'email': 'john.doe@openwebui.com', 'password': 'password'}, + ) + assert response.status_code == 200 + data = response.json() + assert data['id'] == user.id + assert data['name'] == 'John Doe' + assert data['email'] == 'john.doe@openwebui.com' + assert data['role'] == 'user' + assert data['profile_image_url'] == '/user.png' + assert data['token'] is not None and len(data['token']) > 0 + assert data['token_type'] == 'Bearer' + + def test_signup(self): + response = self.fast_api_client.post( + self.create_url('/signup'), + json={ + 'name': 'John Doe', + 'email': 'john.doe@openwebui.com', + 'password': 'password', + }, + ) + assert response.status_code == 200 + data = response.json() + assert data['id'] is not None and len(data['id']) > 0 + assert data['name'] == 'John Doe' + assert data['email'] == 'john.doe@openwebui.com' + assert data['role'] in ['admin', 'user', 'pending'] + assert data['profile_image_url'] == '/user.png' + assert data['token'] is not None and len(data['token']) > 0 + assert data['token_type'] == 'Bearer' + + def test_add_user(self): + with mock_webui_user(): + response = self.fast_api_client.post( + self.create_url('/add'), + json={ + 'name': 'John Doe 2', + 'email': 'john.doe2@openwebui.com', + 'password': 'password2', + 'role': 'admin', + }, + ) + assert response.status_code == 200 + data = response.json() + assert data['id'] is not None and len(data['id']) > 0 + assert data['name'] == 'John Doe 2' + assert data['email'] == 'john.doe2@openwebui.com' + assert data['role'] == 'admin' + assert data['profile_image_url'] == '/user.png' + assert data['token'] is not None and len(data['token']) > 0 + assert data['token_type'] == 'Bearer' + + def test_get_admin_details(self): + self.auths.insert_new_auth( + email='john.doe@openwebui.com', + password='password', + name='John Doe', + profile_image_url='/user.png', + role='admin', + ) + with mock_webui_user(): + response = self.fast_api_client.get(self.create_url('/admin/details')) + + assert response.status_code == 200 + assert response.json() == { + 'name': 'John Doe', + 'email': 'john.doe@openwebui.com', + } + + def test_create_api_key_(self): + user = self.auths.insert_new_auth( + email='john.doe@openwebui.com', + password='password', + name='John Doe', + profile_image_url='/user.png', + role='admin', + ) + with mock_webui_user(id=user.id): + response = self.fast_api_client.post(self.create_url('/api_key')) + assert response.status_code == 200 + data = response.json() + assert data['api_key'] is not None + assert len(data['api_key']) > 0 + + def test_delete_api_key(self): + user = self.auths.insert_new_auth( + email='john.doe@openwebui.com', + password='password', + name='John Doe', + profile_image_url='/user.png', + role='admin', + ) + self.users.update_user_api_key_by_id(user.id, 'abc') + with mock_webui_user(id=user.id): + response = self.fast_api_client.delete(self.create_url('/api_key')) + assert response.status_code == 200 + assert response.json() == True + db_user = self.users.get_user_by_id(user.id) + assert db_user.api_key is None + + def test_get_api_key(self): + user = self.auths.insert_new_auth( + email='john.doe@openwebui.com', + password='password', + name='John Doe', + profile_image_url='/user.png', + role='admin', + ) + self.users.update_user_api_key_by_id(user.id, 'abc') + with mock_webui_user(id=user.id): + response = self.fast_api_client.get(self.create_url('/api_key')) + assert response.status_code == 200 + assert response.json() == {'api_key': 'abc'} diff --git a/backend/open_webui/test/apps/webui/routers/test_models.py b/backend/open_webui/test/apps/webui/routers/test_models.py new file mode 100644 index 0000000000000000000000000000000000000000..6a7e26a519c5c235adeded36023a098e19db856f --- /dev/null +++ b/backend/open_webui/test/apps/webui/routers/test_models.py @@ -0,0 +1,57 @@ +from test.util.abstract_integration_test import AbstractPostgresTest +from test.util.mock_user import mock_webui_user + + +class TestModels(AbstractPostgresTest): + BASE_PATH = '/api/v1/models' + + def setup_class(cls): + super().setup_class() + from open_webui.models.models import Model + + cls.models = Model + + def test_models(self): + with mock_webui_user(id='2'): + response = self.fast_api_client.get(self.create_url('/')) + assert response.status_code == 200 + assert len(response.json()) == 0 + + with mock_webui_user(id='2'): + response = self.fast_api_client.post( + self.create_url('/add'), + json={ + 'id': 'my-model', + 'base_model_id': 'base-model-id', + 'name': 'Hello World', + 'meta': { + 'profile_image_url': '/static/favicon.png', + 'description': 'description', + 'capabilities': None, + 'model_config': {}, + }, + 'params': {}, + }, + ) + assert response.status_code == 200 + + with mock_webui_user(id='2'): + response = self.fast_api_client.get(self.create_url('/')) + assert response.status_code == 200 + assert len(response.json()) == 1 + + with mock_webui_user(id='2'): + response = self.fast_api_client.get(self.create_url(query_params={'id': 'my-model'})) + assert response.status_code == 200 + data = response.json()[0] + assert data['id'] == 'my-model' + assert data['name'] == 'Hello World' + + with mock_webui_user(id='2'): + response = self.fast_api_client.delete(self.create_url('/delete?id=my-model')) + assert response.status_code == 200 + + with mock_webui_user(id='2'): + response = self.fast_api_client.get(self.create_url('/')) + assert response.status_code == 200 + assert len(response.json()) == 0 diff --git a/backend/open_webui/test/apps/webui/routers/test_users.py b/backend/open_webui/test/apps/webui/routers/test_users.py new file mode 100644 index 0000000000000000000000000000000000000000..ad64df3508efaca8261214374a4f0f192e5a7b18 --- /dev/null +++ b/backend/open_webui/test/apps/webui/routers/test_users.py @@ -0,0 +1,165 @@ +from test.util.abstract_integration_test import AbstractPostgresTest +from test.util.mock_user import mock_webui_user + + +def _get_user_by_id(data, param): + return next((item for item in data if item['id'] == param), None) + + +def _assert_user(data, id, **kwargs): + user = _get_user_by_id(data, id) + assert user is not None + comparison_data = { + 'name': f'user {id}', + 'email': f'user{id}@openwebui.com', + 'profile_image_url': f'/api/v1/users/{id}/profile/image', + 'role': 'user', + **kwargs, + } + for key, value in comparison_data.items(): + assert user[key] == value + + +class TestUsers(AbstractPostgresTest): + BASE_PATH = '/api/v1/users' + + def setup_class(cls): + super().setup_class() + from open_webui.models.users import Users + + cls.users = Users + + def setup_method(self): + super().setup_method() + self.users.insert_new_user( + id='1', + name='user 1', + email='user1@openwebui.com', + profile_image_url='/user1.png', + role='user', + ) + self.users.insert_new_user( + id='2', + name='user 2', + email='user2@openwebui.com', + profile_image_url='/user2.png', + role='user', + ) + + def test_users(self): + # Get all users + with mock_webui_user(id='3'): + response = self.fast_api_client.get(self.create_url('')) + assert response.status_code == 200 + assert len(response.json()) == 2 + data = response.json() + _assert_user(data, '1') + _assert_user(data, '2') + + # update role + with mock_webui_user(id='3'): + response = self.fast_api_client.post(self.create_url('/update/role'), json={'id': '2', 'role': 'admin'}) + assert response.status_code == 200 + _assert_user([response.json()], '2', role='admin') + + # Get all users + with mock_webui_user(id='3'): + response = self.fast_api_client.get(self.create_url('')) + assert response.status_code == 200 + assert len(response.json()) == 2 + data = response.json() + _assert_user(data, '1') + _assert_user(data, '2', role='admin') + + # Get (empty) user settings + with mock_webui_user(id='2'): + response = self.fast_api_client.get(self.create_url('/user/settings')) + assert response.status_code == 200 + assert response.json() is None + + # Update user settings + with mock_webui_user(id='2'): + response = self.fast_api_client.post( + self.create_url('/user/settings/update'), + json={ + 'ui': {'attr1': 'value1', 'attr2': 'value2'}, + 'model_config': {'attr3': 'value3', 'attr4': 'value4'}, + }, + ) + assert response.status_code == 200 + + # Get user settings + with mock_webui_user(id='2'): + response = self.fast_api_client.get(self.create_url('/user/settings')) + assert response.status_code == 200 + assert response.json() == { + 'ui': {'attr1': 'value1', 'attr2': 'value2'}, + 'model_config': {'attr3': 'value3', 'attr4': 'value4'}, + } + + # Get (empty) user info + with mock_webui_user(id='1'): + response = self.fast_api_client.get(self.create_url('/user/info')) + assert response.status_code == 200 + assert response.json() is None + + # Update user info + with mock_webui_user(id='1'): + response = self.fast_api_client.post( + self.create_url('/user/info/update'), + json={'attr1': 'value1', 'attr2': 'value2'}, + ) + assert response.status_code == 200 + + # Get user info + with mock_webui_user(id='1'): + response = self.fast_api_client.get(self.create_url('/user/info')) + assert response.status_code == 200 + assert response.json() == {'attr1': 'value1', 'attr2': 'value2'} + + # Get user by id + with mock_webui_user(id='1'): + response = self.fast_api_client.get(self.create_url('/2')) + assert response.status_code == 200 + assert response.json() == {'name': 'user 2', 'profile_image_url': '/user2.png'} + + # Update user by id + with mock_webui_user(id='1'): + response = self.fast_api_client.post( + self.create_url('/2/update'), + json={ + 'name': 'user 2 updated', + 'email': 'user2-updated@openwebui.com', + 'profile_image_url': '/user2-updated.png', + }, + ) + assert response.status_code == 200 + + # Get all users + with mock_webui_user(id='3'): + response = self.fast_api_client.get(self.create_url('')) + assert response.status_code == 200 + assert len(response.json()) == 2 + data = response.json() + _assert_user(data, '1') + _assert_user( + data, + '2', + role='admin', + name='user 2 updated', + email='user2-updated@openwebui.com', + profile_image_url=f'/api/v1/users/2/profile/image', + ) + + # Delete user by id + with mock_webui_user(id='1'): + response = self.fast_api_client.delete(self.create_url('/2')) + assert response.status_code == 200 + + # Get all users + with mock_webui_user(id='3'): + response = self.fast_api_client.get(self.create_url('')) + assert response.status_code == 200 + assert len(response.json()) == 1 + data = response.json() + _assert_user(data, '1') diff --git a/backend/open_webui/test/apps/webui/storage/test_provider.py b/backend/open_webui/test/apps/webui/storage/test_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..806b072d87907efb0a97e9935e9f94e63e8d4a70 --- /dev/null +++ b/backend/open_webui/test/apps/webui/storage/test_provider.py @@ -0,0 +1,406 @@ +import io +import os +import boto3 +import pytest +from botocore.exceptions import ClientError +from moto import mock_aws +from open_webui.storage import provider +from gcp_storage_emulator.server import create_server +from google.cloud import storage +from azure.storage.blob import BlobServiceClient, ContainerClient, BlobClient +from unittest.mock import MagicMock + + +def mock_upload_dir(monkeypatch, tmp_path): + """Fixture to monkey-patch the UPLOAD_DIR and create a temporary directory.""" + directory = tmp_path / 'uploads' + directory.mkdir() + monkeypatch.setattr(provider, 'UPLOAD_DIR', str(directory)) + return directory + + +def test_imports(): + provider.StorageProvider + provider.LocalStorageProvider + provider.S3StorageProvider + provider.GCSStorageProvider + provider.AzureStorageProvider + provider.Storage + + +def test_get_storage_provider(): + Storage = provider.get_storage_provider('local') + assert isinstance(Storage, provider.LocalStorageProvider) + Storage = provider.get_storage_provider('s3') + assert isinstance(Storage, provider.S3StorageProvider) + Storage = provider.get_storage_provider('gcs') + assert isinstance(Storage, provider.GCSStorageProvider) + Storage = provider.get_storage_provider('azure') + assert isinstance(Storage, provider.AzureStorageProvider) + with pytest.raises(RuntimeError): + provider.get_storage_provider('invalid') + + +def test_class_instantiation(): + with pytest.raises(TypeError): + provider.StorageProvider() + with pytest.raises(TypeError): + + class Test(provider.StorageProvider): + pass + + Test() + provider.LocalStorageProvider() + provider.S3StorageProvider() + provider.GCSStorageProvider() + provider.AzureStorageProvider() + + +class TestLocalStorageProvider: + Storage = provider.LocalStorageProvider() + file_content = b'test content' + file_bytesio = io.BytesIO(file_content) + filename = 'test.txt' + filename_extra = 'test_exyta.txt' + file_bytesio_empty = io.BytesIO() + + def test_upload_file(self, monkeypatch, tmp_path): + upload_dir = mock_upload_dir(monkeypatch, tmp_path) + contents, file_path = self.Storage.upload_file(self.file_bytesio, self.filename) + assert (upload_dir / self.filename).exists() + assert (upload_dir / self.filename).read_bytes() == self.file_content + assert contents == self.file_content + assert file_path == str(upload_dir / self.filename) + with pytest.raises(ValueError): + self.Storage.upload_file(self.file_bytesio_empty, self.filename) + + def test_get_file(self, monkeypatch, tmp_path): + upload_dir = mock_upload_dir(monkeypatch, tmp_path) + file_path = str(upload_dir / self.filename) + file_path_return = self.Storage.get_file(file_path) + assert file_path == file_path_return + + def test_delete_file(self, monkeypatch, tmp_path): + upload_dir = mock_upload_dir(monkeypatch, tmp_path) + (upload_dir / self.filename).write_bytes(self.file_content) + assert (upload_dir / self.filename).exists() + file_path = str(upload_dir / self.filename) + self.Storage.delete_file(file_path) + assert not (upload_dir / self.filename).exists() + + def test_delete_all_files(self, monkeypatch, tmp_path): + upload_dir = mock_upload_dir(monkeypatch, tmp_path) + (upload_dir / self.filename).write_bytes(self.file_content) + (upload_dir / self.filename_extra).write_bytes(self.file_content) + self.Storage.delete_all_files() + assert not (upload_dir / self.filename).exists() + assert not (upload_dir / self.filename_extra).exists() + + +@mock_aws +class TestS3StorageProvider: + def __init__(self): + self.Storage = provider.S3StorageProvider() + self.Storage.bucket_name = 'my-bucket' + self.s3_client = boto3.resource('s3', region_name='us-east-1') + self.file_content = b'test content' + self.filename = 'test.txt' + self.filename_extra = 'test_exyta.txt' + self.file_bytesio_empty = io.BytesIO() + super().__init__() + + def test_upload_file(self, monkeypatch, tmp_path): + upload_dir = mock_upload_dir(monkeypatch, tmp_path) + # S3 checks + with pytest.raises(Exception): + self.Storage.upload_file(io.BytesIO(self.file_content), self.filename) + self.s3_client.create_bucket(Bucket=self.Storage.bucket_name) + contents, s3_file_path = self.Storage.upload_file(io.BytesIO(self.file_content), self.filename) + object = self.s3_client.Object(self.Storage.bucket_name, self.filename) + assert self.file_content == object.get()['Body'].read() + # local checks + assert (upload_dir / self.filename).exists() + assert (upload_dir / self.filename).read_bytes() == self.file_content + assert contents == self.file_content + assert s3_file_path == 's3://' + self.Storage.bucket_name + '/' + self.filename + with pytest.raises(ValueError): + self.Storage.upload_file(self.file_bytesio_empty, self.filename) + + def test_get_file(self, monkeypatch, tmp_path): + upload_dir = mock_upload_dir(monkeypatch, tmp_path) + self.s3_client.create_bucket(Bucket=self.Storage.bucket_name) + contents, s3_file_path = self.Storage.upload_file(io.BytesIO(self.file_content), self.filename) + file_path = self.Storage.get_file(s3_file_path) + assert file_path == str(upload_dir / self.filename) + assert (upload_dir / self.filename).exists() + + def test_delete_file(self, monkeypatch, tmp_path): + upload_dir = mock_upload_dir(monkeypatch, tmp_path) + self.s3_client.create_bucket(Bucket=self.Storage.bucket_name) + contents, s3_file_path = self.Storage.upload_file(io.BytesIO(self.file_content), self.filename) + assert (upload_dir / self.filename).exists() + self.Storage.delete_file(s3_file_path) + assert not (upload_dir / self.filename).exists() + with pytest.raises(ClientError) as exc: + self.s3_client.Object(self.Storage.bucket_name, self.filename).load() + error = exc.value.response['Error'] + assert error['Code'] == '404' + assert error['Message'] == 'Not Found' + + def test_delete_all_files(self, monkeypatch, tmp_path): + upload_dir = mock_upload_dir(monkeypatch, tmp_path) + # create 2 files + self.s3_client.create_bucket(Bucket=self.Storage.bucket_name) + self.Storage.upload_file(io.BytesIO(self.file_content), self.filename) + object = self.s3_client.Object(self.Storage.bucket_name, self.filename) + assert self.file_content == object.get()['Body'].read() + assert (upload_dir / self.filename).exists() + assert (upload_dir / self.filename).read_bytes() == self.file_content + self.Storage.upload_file(io.BytesIO(self.file_content), self.filename_extra) + object = self.s3_client.Object(self.Storage.bucket_name, self.filename_extra) + assert self.file_content == object.get()['Body'].read() + assert (upload_dir / self.filename).exists() + assert (upload_dir / self.filename).read_bytes() == self.file_content + + self.Storage.delete_all_files() + assert not (upload_dir / self.filename).exists() + with pytest.raises(ClientError) as exc: + self.s3_client.Object(self.Storage.bucket_name, self.filename).load() + error = exc.value.response['Error'] + assert error['Code'] == '404' + assert error['Message'] == 'Not Found' + assert not (upload_dir / self.filename_extra).exists() + with pytest.raises(ClientError) as exc: + self.s3_client.Object(self.Storage.bucket_name, self.filename_extra).load() + error = exc.value.response['Error'] + assert error['Code'] == '404' + assert error['Message'] == 'Not Found' + + self.Storage.delete_all_files() + assert not (upload_dir / self.filename).exists() + assert not (upload_dir / self.filename_extra).exists() + + def test_init_without_credentials(self, monkeypatch): + """Test that S3StorageProvider can initialize without explicit credentials.""" + # Temporarily unset the environment variables + monkeypatch.setattr(provider, 'S3_ACCESS_KEY_ID', None) + monkeypatch.setattr(provider, 'S3_SECRET_ACCESS_KEY', None) + + # Should not raise an exception + storage = provider.S3StorageProvider() + assert storage.s3_client is not None + assert storage.bucket_name == provider.S3_BUCKET_NAME + + +class TestGCSStorageProvider: + Storage = provider.GCSStorageProvider() + Storage.bucket_name = 'my-bucket' + file_content = b'test content' + filename = 'test.txt' + filename_extra = 'test_exyta.txt' + file_bytesio_empty = io.BytesIO() + + @pytest.fixture(scope='class') + def setup(self): + host, port = 'localhost', 9023 + + server = create_server(host, port, in_memory=True) + server.start() + os.environ['STORAGE_EMULATOR_HOST'] = f'http://{host}:{port}' + + gcs_client = storage.Client() + bucket = gcs_client.bucket(self.Storage.bucket_name) + bucket.create() + self.Storage.gcs_client, self.Storage.bucket = gcs_client, bucket + yield + bucket.delete(force=True) + server.stop() + + def test_upload_file(self, monkeypatch, tmp_path, setup): + upload_dir = mock_upload_dir(monkeypatch, tmp_path) + # catch error if bucket does not exist + with pytest.raises(Exception): + self.Storage.bucket = monkeypatch(self.Storage, 'bucket', None) + self.Storage.upload_file(io.BytesIO(self.file_content), self.filename) + contents, gcs_file_path = self.Storage.upload_file(io.BytesIO(self.file_content), self.filename) + object = self.Storage.bucket.get_blob(self.filename) + assert self.file_content == object.download_as_bytes() + # local checks + assert (upload_dir / self.filename).exists() + assert (upload_dir / self.filename).read_bytes() == self.file_content + assert contents == self.file_content + assert gcs_file_path == 'gs://' + self.Storage.bucket_name + '/' + self.filename + # test error if file is empty + with pytest.raises(ValueError): + self.Storage.upload_file(self.file_bytesio_empty, self.filename) + + def test_get_file(self, monkeypatch, tmp_path, setup): + upload_dir = mock_upload_dir(monkeypatch, tmp_path) + contents, gcs_file_path = self.Storage.upload_file(io.BytesIO(self.file_content), self.filename) + file_path = self.Storage.get_file(gcs_file_path) + assert file_path == str(upload_dir / self.filename) + assert (upload_dir / self.filename).exists() + + def test_delete_file(self, monkeypatch, tmp_path, setup): + upload_dir = mock_upload_dir(monkeypatch, tmp_path) + contents, gcs_file_path = self.Storage.upload_file(io.BytesIO(self.file_content), self.filename) + # ensure that local directory has the uploaded file as well + assert (upload_dir / self.filename).exists() + assert self.Storage.bucket.get_blob(self.filename).name == self.filename + self.Storage.delete_file(gcs_file_path) + # check that deleting file from gcs will delete the local file as well + assert not (upload_dir / self.filename).exists() + assert self.Storage.bucket.get_blob(self.filename) == None + + def test_delete_all_files(self, monkeypatch, tmp_path, setup): + upload_dir = mock_upload_dir(monkeypatch, tmp_path) + # create 2 files + self.Storage.upload_file(io.BytesIO(self.file_content), self.filename) + object = self.Storage.bucket.get_blob(self.filename) + assert (upload_dir / self.filename).exists() + assert (upload_dir / self.filename).read_bytes() == self.file_content + assert self.Storage.bucket.get_blob(self.filename).name == self.filename + assert self.file_content == object.download_as_bytes() + self.Storage.upload_file(io.BytesIO(self.file_content), self.filename_extra) + object = self.Storage.bucket.get_blob(self.filename_extra) + assert (upload_dir / self.filename_extra).exists() + assert (upload_dir / self.filename_extra).read_bytes() == self.file_content + assert self.Storage.bucket.get_blob(self.filename_extra).name == self.filename_extra + assert self.file_content == object.download_as_bytes() + + self.Storage.delete_all_files() + assert not (upload_dir / self.filename).exists() + assert not (upload_dir / self.filename_extra).exists() + assert self.Storage.bucket.get_blob(self.filename) == None + assert self.Storage.bucket.get_blob(self.filename_extra) == None + + +class TestAzureStorageProvider: + def __init__(self): + super().__init__() + + @pytest.fixture(scope='class') + def setup_storage(self, monkeypatch): + # Create mock Blob Service Client and related clients + mock_blob_service_client = MagicMock() + mock_container_client = MagicMock() + mock_blob_client = MagicMock() + + # Set up return values for the mock + mock_blob_service_client.get_container_client.return_value = mock_container_client + mock_container_client.get_blob_client.return_value = mock_blob_client + + # Monkeypatch the Azure classes to return our mocks + monkeypatch.setattr( + azure.storage.blob, + 'BlobServiceClient', + lambda *args, **kwargs: mock_blob_service_client, + ) + monkeypatch.setattr( + azure.storage.blob, + 'ContainerClient', + lambda *args, **kwargs: mock_container_client, + ) + monkeypatch.setattr(azure.storage.blob, 'BlobClient', lambda *args, **kwargs: mock_blob_client) + + self.Storage = provider.AzureStorageProvider() + self.Storage.endpoint = 'https://myaccount.blob.core.windows.net' + self.Storage.container_name = 'my-container' + self.file_content = b'test content' + self.filename = 'test.txt' + self.filename_extra = 'test_extra.txt' + self.file_bytesio_empty = io.BytesIO() + + # Apply mocks to the Storage instance + self.Storage.blob_service_client = mock_blob_service_client + self.Storage.container_client = mock_container_client + + def test_upload_file(self, monkeypatch, tmp_path): + upload_dir = mock_upload_dir(monkeypatch, tmp_path) + + # Simulate an error when container does not exist + self.Storage.container_client.get_blob_client.side_effect = Exception('Container does not exist') + with pytest.raises(Exception): + self.Storage.upload_file(io.BytesIO(self.file_content), self.filename) + + # Reset side effect and create container + self.Storage.container_client.get_blob_client.side_effect = None + self.Storage.create_container() + contents, azure_file_path = self.Storage.upload_file(io.BytesIO(self.file_content), self.filename) + + # Assertions + self.Storage.container_client.get_blob_client.assert_called_with(self.filename) + self.Storage.container_client.get_blob_client().upload_blob.assert_called_once_with( + self.file_content, overwrite=True + ) + assert contents == self.file_content + assert ( + azure_file_path == f'https://myaccount.blob.core.windows.net/{self.Storage.container_name}/{self.filename}' + ) + assert (upload_dir / self.filename).exists() + assert (upload_dir / self.filename).read_bytes() == self.file_content + + with pytest.raises(ValueError): + self.Storage.upload_file(self.file_bytesio_empty, self.filename) + + def test_get_file(self, monkeypatch, tmp_path): + upload_dir = mock_upload_dir(monkeypatch, tmp_path) + self.Storage.create_container() + + # Mock upload behavior + self.Storage.upload_file(io.BytesIO(self.file_content), self.filename) + # Mock blob download behavior + self.Storage.container_client.get_blob_client().download_blob().readall.return_value = self.file_content + + file_url = f'https://myaccount.blob.core.windows.net/{self.Storage.container_name}/{self.filename}' + file_path = self.Storage.get_file(file_url) + + assert file_path == str(upload_dir / self.filename) + assert (upload_dir / self.filename).exists() + assert (upload_dir / self.filename).read_bytes() == self.file_content + + def test_delete_file(self, monkeypatch, tmp_path): + upload_dir = mock_upload_dir(monkeypatch, tmp_path) + self.Storage.create_container() + + # Mock file upload + self.Storage.upload_file(io.BytesIO(self.file_content), self.filename) + # Mock deletion + self.Storage.container_client.get_blob_client().delete_blob.return_value = None + + file_url = f'https://myaccount.blob.core.windows.net/{self.Storage.container_name}/{self.filename}' + self.Storage.delete_file(file_url) + + self.Storage.container_client.get_blob_client().delete_blob.assert_called_once() + assert not (upload_dir / self.filename).exists() + + def test_delete_all_files(self, monkeypatch, tmp_path): + upload_dir = mock_upload_dir(monkeypatch, tmp_path) + self.Storage.create_container() + + # Mock file uploads + self.Storage.upload_file(io.BytesIO(self.file_content), self.filename) + self.Storage.upload_file(io.BytesIO(self.file_content), self.filename_extra) + + # Mock listing and deletion behavior + self.Storage.container_client.list_blobs.return_value = [ + {'name': self.filename}, + {'name': self.filename_extra}, + ] + self.Storage.container_client.get_blob_client().delete_blob.return_value = None + + self.Storage.delete_all_files() + + self.Storage.container_client.list_blobs.assert_called_once() + self.Storage.container_client.get_blob_client().delete_blob.assert_any_call() + assert not (upload_dir / self.filename).exists() + assert not (upload_dir / self.filename_extra).exists() + + def test_get_file_not_found(self, monkeypatch): + self.Storage.create_container() + + file_url = f'https://myaccount.blob.core.windows.net/{self.Storage.container_name}/{self.filename}' + # Mock behavior to raise an error for missing blobs + self.Storage.container_client.get_blob_client().download_blob.side_effect = Exception('Blob not found') + with pytest.raises(Exception, match='Blob not found'): + self.Storage.get_file(file_url) diff --git a/backend/open_webui/test/util/test_redis.py b/backend/open_webui/test/util/test_redis.py new file mode 100644 index 0000000000000000000000000000000000000000..036fb36362f097c245a7253adc8f4c47f9ede7fa --- /dev/null +++ b/backend/open_webui/test/util/test_redis.py @@ -0,0 +1,781 @@ +import pytest +from unittest.mock import Mock, patch, AsyncMock +import redis +from open_webui.utils.redis import ( + SentinelRedisProxy, + parse_redis_service_url, + get_redis_connection, + get_sentinels_from_env, + MAX_RETRY_COUNT, +) +import inspect + + +class TestSentinelRedisProxy: + """Test Redis Sentinel failover functionality""" + + def test_parse_redis_service_url_valid(self): + """Test parsing valid Redis service URL""" + url = 'redis://user:pass@mymaster:6379/0' + result = parse_redis_service_url(url) + + assert result['username'] == 'user' + assert result['password'] == 'pass' + assert result['service'] == 'mymaster' + assert result['port'] == 6379 + assert result['db'] == 0 + + def test_parse_redis_service_url_defaults(self): + """Test parsing Redis service URL with defaults""" + url = 'redis://mymaster' + result = parse_redis_service_url(url) + + assert result['username'] is None + assert result['password'] is None + assert result['service'] == 'mymaster' + assert result['port'] == 6379 + assert result['db'] == 0 + + def test_parse_redis_service_url_invalid_scheme(self): + """Test parsing invalid URL scheme""" + with pytest.raises(ValueError, match='Invalid Redis URL scheme'): + parse_redis_service_url('http://invalid') + + def test_get_sentinels_from_env(self): + """Test parsing sentinel hosts from environment""" + hosts = 'sentinel1,sentinel2,sentinel3' + port = '26379' + + result = get_sentinels_from_env(hosts, port) + expected = [('sentinel1', 26379), ('sentinel2', 26379), ('sentinel3', 26379)] + + assert result == expected + + def test_get_sentinels_from_env_empty(self): + """Test empty sentinel hosts""" + result = get_sentinels_from_env(None, '26379') + assert result == [] + + @patch('redis.sentinel.Sentinel') + def test_sentinel_redis_proxy_sync_success(self, mock_sentinel_class): + """Test successful sync operation with SentinelRedisProxy""" + mock_sentinel = Mock() + mock_master = Mock() + mock_master.get.return_value = 'test_value' + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=False) + + # Test attribute access + get_method = proxy.__getattr__('get') + result = get_method('test_key') + + assert result == 'test_value' + mock_sentinel.master_for.assert_called_with('mymaster') + mock_master.get.assert_called_with('test_key') + + @patch('redis.sentinel.Sentinel') + @pytest.mark.asyncio + async def test_sentinel_redis_proxy_async_success(self, mock_sentinel_class): + """Test successful async operation with SentinelRedisProxy""" + mock_sentinel = Mock() + mock_master = Mock() + mock_master.get = AsyncMock(return_value='test_value') + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=True) + + # Test async attribute access + get_method = proxy.__getattr__('get') + result = await get_method('test_key') + + assert result == 'test_value' + mock_sentinel.master_for.assert_called_with('mymaster') + mock_master.get.assert_called_with('test_key') + + @patch('redis.sentinel.Sentinel') + def test_sentinel_redis_proxy_failover_retry(self, mock_sentinel_class): + """Test retry mechanism during failover""" + mock_sentinel = Mock() + mock_master = Mock() + + # First call fails, second succeeds + mock_master.get.side_effect = [ + redis.exceptions.ConnectionError('Master down'), + 'test_value', + ] + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=False) + + get_method = proxy.__getattr__('get') + result = get_method('test_key') + + assert result == 'test_value' + assert mock_master.get.call_count == 2 + + @patch('redis.sentinel.Sentinel') + def test_sentinel_redis_proxy_max_retries_exceeded(self, mock_sentinel_class): + """Test failure after max retries exceeded""" + mock_sentinel = Mock() + mock_master = Mock() + + # All calls fail + mock_master.get.side_effect = redis.exceptions.ConnectionError('Master down') + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=False) + + get_method = proxy.__getattr__('get') + + with pytest.raises(redis.exceptions.ConnectionError): + get_method('test_key') + + assert mock_master.get.call_count == MAX_RETRY_COUNT + + @patch('redis.sentinel.Sentinel') + def test_sentinel_redis_proxy_readonly_error_retry(self, mock_sentinel_class): + """Test retry on ReadOnlyError""" + mock_sentinel = Mock() + mock_master = Mock() + + # First call gets ReadOnlyError (old master), second succeeds (new master) + mock_master.get.side_effect = [ + redis.exceptions.ReadOnlyError('Read only'), + 'test_value', + ] + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=False) + + get_method = proxy.__getattr__('get') + result = get_method('test_key') + + assert result == 'test_value' + assert mock_master.get.call_count == 2 + + @patch('redis.sentinel.Sentinel') + def test_sentinel_redis_proxy_factory_methods(self, mock_sentinel_class): + """Test factory methods are passed through directly""" + mock_sentinel = Mock() + mock_master = Mock() + mock_pipeline = Mock() + mock_master.pipeline.return_value = mock_pipeline + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=False) + + # Factory methods should be passed through without wrapping + pipeline_method = proxy.__getattr__('pipeline') + result = pipeline_method() + + assert result == mock_pipeline + mock_master.pipeline.assert_called_once() + + @patch('redis.sentinel.Sentinel') + @patch('redis.from_url') + def test_get_redis_connection_with_sentinel(self, mock_from_url, mock_sentinel_class): + """Test getting Redis connection with Sentinel""" + mock_sentinel = Mock() + mock_sentinel_class.return_value = mock_sentinel + + sentinels = [('sentinel1', 26379), ('sentinel2', 26379)] + redis_url = 'redis://user:pass@mymaster:6379/0' + + result = get_redis_connection(redis_url=redis_url, redis_sentinels=sentinels, async_mode=False) + + assert isinstance(result, SentinelRedisProxy) + mock_sentinel_class.assert_called_once() + mock_from_url.assert_not_called() + + @patch('redis.Redis.from_url') + def test_get_redis_connection_without_sentinel(self, mock_from_url): + """Test getting Redis connection without Sentinel""" + mock_redis = Mock() + mock_from_url.return_value = mock_redis + + redis_url = 'redis://localhost:6379/0' + + result = get_redis_connection(redis_url=redis_url, redis_sentinels=None, async_mode=False) + + assert result == mock_redis + mock_from_url.assert_called_once_with(redis_url, decode_responses=True) + + @patch('redis.asyncio.from_url') + def test_get_redis_connection_without_sentinel_async(self, mock_from_url): + """Test getting async Redis connection without Sentinel""" + mock_redis = Mock() + mock_from_url.return_value = mock_redis + + redis_url = 'redis://localhost:6379/0' + + result = get_redis_connection(redis_url=redis_url, redis_sentinels=None, async_mode=True) + + assert result == mock_redis + mock_from_url.assert_called_once_with(redis_url, decode_responses=True) + + +class TestSentinelRedisProxyCommands: + """Test Redis commands through SentinelRedisProxy""" + + @patch('redis.sentinel.Sentinel') + def test_hash_commands_sync(self, mock_sentinel_class): + """Test Redis hash commands in sync mode""" + mock_sentinel = Mock() + mock_master = Mock() + + # Mock hash command responses + mock_master.hset.return_value = 1 + mock_master.hget.return_value = 'test_value' + mock_master.hgetall.return_value = {'key1': 'value1', 'key2': 'value2'} + mock_master.hdel.return_value = 1 + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=False) + + # Test hset + hset_method = proxy.__getattr__('hset') + result = hset_method('test_hash', 'field1', 'value1') + assert result == 1 + mock_master.hset.assert_called_with('test_hash', 'field1', 'value1') + + # Test hget + hget_method = proxy.__getattr__('hget') + result = hget_method('test_hash', 'field1') + assert result == 'test_value' + mock_master.hget.assert_called_with('test_hash', 'field1') + + # Test hgetall + hgetall_method = proxy.__getattr__('hgetall') + result = hgetall_method('test_hash') + assert result == {'key1': 'value1', 'key2': 'value2'} + mock_master.hgetall.assert_called_with('test_hash') + + # Test hdel + hdel_method = proxy.__getattr__('hdel') + result = hdel_method('test_hash', 'field1') + assert result == 1 + mock_master.hdel.assert_called_with('test_hash', 'field1') + + @patch('redis.sentinel.Sentinel') + @pytest.mark.asyncio + async def test_hash_commands_async(self, mock_sentinel_class): + """Test Redis hash commands in async mode""" + mock_sentinel = Mock() + mock_master = Mock() + + # Mock async hash command responses + mock_master.hset = AsyncMock(return_value=1) + mock_master.hget = AsyncMock(return_value='test_value') + mock_master.hgetall = AsyncMock(return_value={'key1': 'value1', 'key2': 'value2'}) + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=True) + + # Test hset + hset_method = proxy.__getattr__('hset') + result = await hset_method('test_hash', 'field1', 'value1') + assert result == 1 + mock_master.hset.assert_called_with('test_hash', 'field1', 'value1') + + # Test hget + hget_method = proxy.__getattr__('hget') + result = await hget_method('test_hash', 'field1') + assert result == 'test_value' + mock_master.hget.assert_called_with('test_hash', 'field1') + + # Test hgetall + hgetall_method = proxy.__getattr__('hgetall') + result = await hgetall_method('test_hash') + assert result == {'key1': 'value1', 'key2': 'value2'} + mock_master.hgetall.assert_called_with('test_hash') + + @patch('redis.sentinel.Sentinel') + def test_string_commands_sync(self, mock_sentinel_class): + """Test Redis string commands in sync mode""" + mock_sentinel = Mock() + mock_master = Mock() + + # Mock string command responses + mock_master.set.return_value = True + mock_master.get.return_value = 'test_value' + mock_master.delete.return_value = 1 + mock_master.exists.return_value = True + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=False) + + # Test set + set_method = proxy.__getattr__('set') + result = set_method('test_key', 'test_value') + assert result is True + mock_master.set.assert_called_with('test_key', 'test_value') + + # Test get + get_method = proxy.__getattr__('get') + result = get_method('test_key') + assert result == 'test_value' + mock_master.get.assert_called_with('test_key') + + # Test delete + delete_method = proxy.__getattr__('delete') + result = delete_method('test_key') + assert result == 1 + mock_master.delete.assert_called_with('test_key') + + # Test exists + exists_method = proxy.__getattr__('exists') + result = exists_method('test_key') + assert result is True + mock_master.exists.assert_called_with('test_key') + + @patch('redis.sentinel.Sentinel') + def test_list_commands_sync(self, mock_sentinel_class): + """Test Redis list commands in sync mode""" + mock_sentinel = Mock() + mock_master = Mock() + + # Mock list command responses + mock_master.lpush.return_value = 1 + mock_master.rpop.return_value = 'test_value' + mock_master.llen.return_value = 5 + mock_master.lrange.return_value = ['item1', 'item2', 'item3'] + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=False) + + # Test lpush + lpush_method = proxy.__getattr__('lpush') + result = lpush_method('test_list', 'item1') + assert result == 1 + mock_master.lpush.assert_called_with('test_list', 'item1') + + # Test rpop + rpop_method = proxy.__getattr__('rpop') + result = rpop_method('test_list') + assert result == 'test_value' + mock_master.rpop.assert_called_with('test_list') + + # Test llen + llen_method = proxy.__getattr__('llen') + result = llen_method('test_list') + assert result == 5 + mock_master.llen.assert_called_with('test_list') + + # Test lrange + lrange_method = proxy.__getattr__('lrange') + result = lrange_method('test_list', 0, -1) + assert result == ['item1', 'item2', 'item3'] + mock_master.lrange.assert_called_with('test_list', 0, -1) + + @patch('redis.sentinel.Sentinel') + def test_pubsub_commands_sync(self, mock_sentinel_class): + """Test Redis pubsub commands in sync mode""" + mock_sentinel = Mock() + mock_master = Mock() + mock_pubsub = Mock() + + # Mock pubsub responses + mock_master.pubsub.return_value = mock_pubsub + mock_master.publish.return_value = 1 + mock_pubsub.subscribe.return_value = None + mock_pubsub.get_message.return_value = {'type': 'message', 'data': 'test_data'} + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=False) + + # Test pubsub (factory method - should pass through) + pubsub_method = proxy.__getattr__('pubsub') + result = pubsub_method() + assert result == mock_pubsub + mock_master.pubsub.assert_called_once() + + # Test publish + publish_method = proxy.__getattr__('publish') + result = publish_method('test_channel', 'test_message') + assert result == 1 + mock_master.publish.assert_called_with('test_channel', 'test_message') + + @patch('redis.sentinel.Sentinel') + def test_pipeline_commands_sync(self, mock_sentinel_class): + """Test Redis pipeline commands in sync mode""" + mock_sentinel = Mock() + mock_master = Mock() + mock_pipeline = Mock() + + # Mock pipeline responses + mock_master.pipeline.return_value = mock_pipeline + mock_pipeline.set.return_value = mock_pipeline + mock_pipeline.get.return_value = mock_pipeline + mock_pipeline.execute.return_value = [True, 'test_value'] + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=False) + + # Test pipeline (factory method - should pass through) + pipeline_method = proxy.__getattr__('pipeline') + result = pipeline_method() + assert result == mock_pipeline + mock_master.pipeline.assert_called_once() + + @patch('redis.sentinel.Sentinel') + def test_commands_with_failover_retry(self, mock_sentinel_class): + """Test Redis commands with failover retry mechanism""" + mock_sentinel = Mock() + mock_master = Mock() + + # First call fails with connection error, second succeeds + mock_master.hget.side_effect = [ + redis.exceptions.ConnectionError('Connection failed'), + 'recovered_value', + ] + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=False) + + # Test hget with retry + hget_method = proxy.__getattr__('hget') + result = hget_method('test_hash', 'field1') + + assert result == 'recovered_value' + assert mock_master.hget.call_count == 2 + + # Verify both calls were made with same parameters + expected_calls = [(('test_hash', 'field1'),), (('test_hash', 'field1'),)] + actual_calls = [call.args for call in mock_master.hget.call_args_list] + assert actual_calls == expected_calls + + @patch('redis.sentinel.Sentinel') + def test_commands_with_readonly_error_retry(self, mock_sentinel_class): + """Test Redis commands with ReadOnlyError retry mechanism""" + mock_sentinel = Mock() + mock_master = Mock() + + # First call fails with ReadOnlyError, second succeeds + mock_master.hset.side_effect = [ + redis.exceptions.ReadOnlyError("READONLY You can't write against a read only replica"), + 1, + ] + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=False) + + # Test hset with retry + hset_method = proxy.__getattr__('hset') + result = hset_method('test_hash', 'field1', 'value1') + + assert result == 1 + assert mock_master.hset.call_count == 2 + + # Verify both calls were made with same parameters + expected_calls = [ + (('test_hash', 'field1', 'value1'),), + (('test_hash', 'field1', 'value1'),), + ] + actual_calls = [call.args for call in mock_master.hset.call_args_list] + assert actual_calls == expected_calls + + @patch('redis.sentinel.Sentinel') + @pytest.mark.asyncio + async def test_async_commands_with_failover_retry(self, mock_sentinel_class): + """Test async Redis commands with failover retry mechanism""" + mock_sentinel = Mock() + mock_master = Mock() + + # First call fails with connection error, second succeeds + mock_master.hget = AsyncMock( + side_effect=[ + redis.exceptions.ConnectionError('Connection failed'), + 'recovered_value', + ] + ) + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=True) + + # Test async hget with retry + hget_method = proxy.__getattr__('hget') + result = await hget_method('test_hash', 'field1') + + assert result == 'recovered_value' + assert mock_master.hget.call_count == 2 + + # Verify both calls were made with same parameters + expected_calls = [(('test_hash', 'field1'),), (('test_hash', 'field1'),)] + actual_calls = [call.args for call in mock_master.hget.call_args_list] + assert actual_calls == expected_calls + + +class TestSentinelRedisProxyFactoryMethods: + """Test Redis factory methods in async mode - these are special cases that remain sync""" + + @patch('redis.sentinel.Sentinel') + @pytest.mark.asyncio + async def test_pubsub_factory_method_async(self, mock_sentinel_class): + """Test pubsub factory method in async mode - should pass through without wrapping""" + mock_sentinel = Mock() + mock_master = Mock() + mock_pubsub = Mock() + + # Mock pubsub factory method + mock_master.pubsub.return_value = mock_pubsub + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=True) + + # Test pubsub factory method - should NOT be wrapped as async + pubsub_method = proxy.__getattr__('pubsub') + result = pubsub_method() + + assert result == mock_pubsub + mock_master.pubsub.assert_called_once() + + # Verify it's not wrapped as async (no await needed) + assert not inspect.iscoroutine(result) + + @patch('redis.sentinel.Sentinel') + @pytest.mark.asyncio + async def test_pipeline_factory_method_async(self, mock_sentinel_class): + """Test pipeline factory method in async mode - should pass through without wrapping""" + mock_sentinel = Mock() + mock_master = Mock() + mock_pipeline = Mock() + + # Mock pipeline factory method + mock_master.pipeline.return_value = mock_pipeline + mock_pipeline.set.return_value = mock_pipeline + mock_pipeline.get.return_value = mock_pipeline + mock_pipeline.execute.return_value = [True, 'test_value'] + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=True) + + # Test pipeline factory method - should NOT be wrapped as async + pipeline_method = proxy.__getattr__('pipeline') + result = pipeline_method() + + assert result == mock_pipeline + mock_master.pipeline.assert_called_once() + + # Verify it's not wrapped as async (no await needed) + assert not inspect.iscoroutine(result) + + # Test pipeline usage (these should also be sync) + pipeline_result = result.set('key', 'value').get('key').execute() + assert pipeline_result == [True, 'test_value'] + + @patch('redis.sentinel.Sentinel') + @pytest.mark.asyncio + async def test_factory_methods_vs_regular_commands_async(self, mock_sentinel_class): + """Test that factory methods behave differently from regular commands in async mode""" + mock_sentinel = Mock() + mock_master = Mock() + + # Mock both factory method and regular command + mock_pubsub = Mock() + mock_master.pubsub.return_value = mock_pubsub + mock_master.get = AsyncMock(return_value='test_value') + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=True) + + # Test factory method - should NOT be wrapped + pubsub_method = proxy.__getattr__('pubsub') + pubsub_result = pubsub_method() + + # Test regular command - should be wrapped as async + get_method = proxy.__getattr__('get') + get_result = get_method('test_key') + + # Factory method returns directly + assert pubsub_result == mock_pubsub + assert not inspect.iscoroutine(pubsub_result) + + # Regular command returns coroutine + assert inspect.iscoroutine(get_result) + + # Regular command needs await + actual_value = await get_result + assert actual_value == 'test_value' + + @patch('redis.sentinel.Sentinel') + @pytest.mark.asyncio + async def test_factory_methods_with_failover_async(self, mock_sentinel_class): + """Test factory methods with failover in async mode""" + mock_sentinel = Mock() + mock_master = Mock() + + # First call fails, second succeeds + mock_pubsub = Mock() + mock_master.pubsub.side_effect = [ + redis.exceptions.ConnectionError('Connection failed'), + mock_pubsub, + ] + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=True) + + # Test pubsub factory method with failover + pubsub_method = proxy.__getattr__('pubsub') + result = pubsub_method() + + assert result == mock_pubsub + assert mock_master.pubsub.call_count == 2 # Retry happened + + # Verify it's still not wrapped as async after retry + assert not inspect.iscoroutine(result) + + @patch('redis.sentinel.Sentinel') + @pytest.mark.asyncio + async def test_monitor_factory_method_async(self, mock_sentinel_class): + """Test monitor factory method in async mode - should pass through without wrapping""" + mock_sentinel = Mock() + mock_master = Mock() + mock_monitor = Mock() + + # Mock monitor factory method + mock_master.monitor.return_value = mock_monitor + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=True) + + # Test monitor factory method - should NOT be wrapped as async + monitor_method = proxy.__getattr__('monitor') + result = monitor_method() + + assert result == mock_monitor + mock_master.monitor.assert_called_once() + + # Verify it's not wrapped as async (no await needed) + assert not inspect.iscoroutine(result) + + @patch('redis.sentinel.Sentinel') + @pytest.mark.asyncio + async def test_client_factory_method_async(self, mock_sentinel_class): + """Test client factory method in async mode - should pass through without wrapping""" + mock_sentinel = Mock() + mock_master = Mock() + mock_client = Mock() + + # Mock client factory method + mock_master.client.return_value = mock_client + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=True) + + # Test client factory method - should NOT be wrapped as async + client_method = proxy.__getattr__('client') + result = client_method() + + assert result == mock_client + mock_master.client.assert_called_once() + + # Verify it's not wrapped as async (no await needed) + assert not inspect.iscoroutine(result) + + @patch('redis.sentinel.Sentinel') + @pytest.mark.asyncio + async def test_transaction_factory_method_async(self, mock_sentinel_class): + """Test transaction factory method in async mode - should pass through without wrapping""" + mock_sentinel = Mock() + mock_master = Mock() + mock_transaction = Mock() + + # Mock transaction factory method + mock_master.transaction.return_value = mock_transaction + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=True) + + # Test transaction factory method - should NOT be wrapped as async + transaction_method = proxy.__getattr__('transaction') + result = transaction_method() + + assert result == mock_transaction + mock_master.transaction.assert_called_once() + + # Verify it's not wrapped as async (no await needed) + assert not inspect.iscoroutine(result) + + @patch('redis.sentinel.Sentinel') + @pytest.mark.asyncio + async def test_all_factory_methods_async(self, mock_sentinel_class): + """Test all factory methods in async mode - comprehensive test""" + mock_sentinel = Mock() + mock_master = Mock() + + # Mock all factory methods + mock_objects = { + 'pipeline': Mock(), + 'pubsub': Mock(), + 'monitor': Mock(), + 'client': Mock(), + 'transaction': Mock(), + } + + for method_name, mock_obj in mock_objects.items(): + setattr(mock_master, method_name, Mock(return_value=mock_obj)) + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=True) + + # Test all factory methods + for method_name, expected_obj in mock_objects.items(): + method = proxy.__getattr__(method_name) + result = method() + + assert result == expected_obj + assert not inspect.iscoroutine(result) + getattr(mock_master, method_name).assert_called_once() + + # Reset mock for next iteration + getattr(mock_master, method_name).reset_mock() + + @patch('redis.sentinel.Sentinel') + @pytest.mark.asyncio + async def test_mixed_factory_and_regular_commands_async(self, mock_sentinel_class): + """Test using both factory methods and regular commands in async mode""" + mock_sentinel = Mock() + mock_master = Mock() + + # Mock pipeline factory and regular commands + mock_pipeline = Mock() + mock_master.pipeline.return_value = mock_pipeline + mock_pipeline.set.return_value = mock_pipeline + mock_pipeline.get.return_value = mock_pipeline + mock_pipeline.execute.return_value = [True, 'pipeline_value'] + + mock_master.get = AsyncMock(return_value='regular_value') + + mock_sentinel.master_for.return_value = mock_master + + proxy = SentinelRedisProxy(mock_sentinel, 'mymaster', async_mode=True) + + # Use factory method (sync) + pipeline = proxy.__getattr__('pipeline')() + pipeline_result = pipeline.set('key1', 'value1').get('key1').execute() + + # Use regular command (async) + get_method = proxy.__getattr__('get') + regular_result = await get_method('key2') + + # Verify both work correctly + assert pipeline_result == [True, 'pipeline_value'] + assert regular_result == 'regular_value' + + # Verify calls + mock_master.pipeline.assert_called_once() + mock_master.get.assert_called_with('key2') diff --git a/backend/open_webui/tools/__init__.py b/backend/open_webui/tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..112324b569037e4c93a416dc890774c3382f9bec --- /dev/null +++ b/backend/open_webui/tools/__init__.py @@ -0,0 +1,6 @@ +""" +Open WebUI Tools Package. + +This package contains built-in tools that are automatically available +when native function calling is enabled. +""" diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py new file mode 100644 index 0000000000000000000000000000000000000000..18b888bbd70d5a0bb3ed738c40fbbf194af9d64a --- /dev/null +++ b/backend/open_webui/tools/builtin.py @@ -0,0 +1,3307 @@ +""" +Built-in tools for Open WebUI. + +These tools are automatically available when native function calling is enabled. + +IMPORTANT: DO NOT IMPORT THIS MODULE DIRECTLY IN OTHER PARTS OF THE CODEBASE. +""" + +import json +import logging +import time +import asyncio +from typing import Optional + +from fastapi import Request + +from open_webui.models.users import UserModel +from open_webui.routers.retrieval import search_web as _search_web +from open_webui.retrieval.utils import get_content_from_url +from open_webui.routers.images import ( + image_generations, + image_edits, + CreateImageForm, + EditImageForm, +) +from open_webui.routers.memories import ( + query_memory, + add_memory as _add_memory, + update_memory_by_id, + QueryMemoryForm, + AddMemoryForm, + MemoryUpdateModel, +) +from open_webui.models.notes import Notes +from open_webui.models.chats import Chats +from open_webui.models.channels import Channels, ChannelMember, Channel +from open_webui.models.messages import Messages, Message +from open_webui.models.groups import Groups +from open_webui.models.memories import Memories +from open_webui.retrieval.vector.async_client import ASYNC_VECTOR_DB_CLIENT +from open_webui.utils.sanitize import sanitize_code + +log = logging.getLogger(__name__) + +MAX_KNOWLEDGE_BASE_SEARCH_ITEMS = 10_000 + +# ============================================================================= +# TIME UTILITIES +# ============================================================================= + + +async def get_current_timestamp( + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Get the current Unix timestamp in seconds. + + :return: JSON with current_timestamp (seconds), current_iso (UTC ISO format), and user_local_iso (user's local time) + """ + try: + import datetime + from zoneinfo import ZoneInfo + + now = datetime.datetime.now(datetime.timezone.utc) + result = { + 'current_timestamp': int(now.timestamp()), + 'current_iso': now.isoformat(), + } + + # Include the user's local time if timezone is available + tz_name = __user__.get('timezone') if __user__ else None + if tz_name: + try: + user_tz = ZoneInfo(tz_name) + user_now = now.astimezone(user_tz) + result['user_local_iso'] = user_now.isoformat() + result['user_timezone'] = tz_name + except Exception: + pass + + return json.dumps(result, ensure_ascii=False) + except Exception as e: + log.exception(f'get_current_timestamp error: {e}') + return json.dumps({'error': str(e)}) + + +async def calculate_timestamp( + days_ago: int = 0, + weeks_ago: int = 0, + months_ago: int = 0, + years_ago: int = 0, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Get the current Unix timestamp, optionally adjusted by days, weeks, months, or years. + Use this to calculate timestamps for date filtering in search functions. + Examples: "last week" = weeks_ago=1, "3 days ago" = days_ago=3, "a year ago" = years_ago=1 + + :param days_ago: Number of days to subtract from current time (default: 0) + :param weeks_ago: Number of weeks to subtract from current time (default: 0) + :param months_ago: Number of months to subtract from current time (default: 0) + :param years_ago: Number of years to subtract from current time (default: 0) + :return: JSON with current_timestamp and calculated_timestamp (both in seconds) + """ + try: + import datetime + from dateutil.relativedelta import relativedelta + + now = datetime.datetime.now(datetime.timezone.utc) + current_ts = int(now.timestamp()) + + # Calculate the adjusted time + total_days = days_ago + (weeks_ago * 7) + adjusted = now - datetime.timedelta(days=total_days) + + # Handle months and years separately (variable length) + if months_ago > 0 or years_ago > 0: + adjusted = adjusted - relativedelta(months=months_ago, years=years_ago) + + adjusted_ts = int(adjusted.timestamp()) + + result = { + 'current_timestamp': current_ts, + 'current_iso': now.isoformat(), + 'calculated_timestamp': adjusted_ts, + 'calculated_iso': adjusted.isoformat(), + } + + # Include the user's local time if timezone is available + tz_name = __user__.get('timezone') if __user__ else None + if tz_name: + try: + from zoneinfo import ZoneInfo + + user_tz = ZoneInfo(tz_name) + result['user_local_iso'] = now.astimezone(user_tz).isoformat() + result['calculated_local_iso'] = adjusted.astimezone(user_tz).isoformat() + result['user_timezone'] = tz_name + except Exception: + pass + + return json.dumps(result, ensure_ascii=False) + except ImportError: + # Fallback without dateutil + import datetime + + now = datetime.datetime.now(datetime.timezone.utc) + current_ts = int(now.timestamp()) + total_days = days_ago + (weeks_ago * 7) + (months_ago * 30) + (years_ago * 365) + adjusted = now - datetime.timedelta(days=total_days) + adjusted_ts = int(adjusted.timestamp()) + result = { + 'current_timestamp': current_ts, + 'current_iso': now.isoformat(), + 'calculated_timestamp': adjusted_ts, + 'calculated_iso': adjusted.isoformat(), + } + + tz_name = __user__.get('timezone') if __user__ else None + if tz_name: + try: + from zoneinfo import ZoneInfo + + user_tz = ZoneInfo(tz_name) + result['user_local_iso'] = now.astimezone(user_tz).isoformat() + result['calculated_local_iso'] = adjusted.astimezone(user_tz).isoformat() + result['user_timezone'] = tz_name + except Exception: + pass + + return json.dumps(result, ensure_ascii=False) + except Exception as e: + log.exception(f'calculate_timestamp error: {e}') + return json.dumps({'error': str(e)}) + + +# ============================================================================= +# WEB SEARCH TOOLS +# ============================================================================= + + +async def search_web( + query: str, + count: Optional[int] = None, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Search the public web for information. Best for current events, external references, + or topics not covered in internal documents. + + :param query: The search query to look up + :param count: Number of results to return (default: admin-configured value) + :return: JSON with search results containing title, link, and snippet for each result + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + try: + engine = __request__.app.state.config.WEB_SEARCH_ENGINE + user = UserModel(**__user__) if __user__ else None + + configured = __request__.app.state.config.WEB_SEARCH_RESULT_COUNT + max_count = 5 if configured is None else configured + count = max(1, min(count, max_count)) if count is not None else max_count + + results = await asyncio.to_thread(_search_web, __request__, engine, query, user) + + # Limit results + results = results[:count] if results else [] + + return json.dumps( + [{'title': r.title, 'link': r.link, 'snippet': r.snippet} for r in results], + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'search_web error: {e}') + return json.dumps({'error': str(e)}) + + +async def fetch_url( + url: str, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Fetch and extract the main text content from a web page URL. + + :param url: The URL to fetch content from + :return: The extracted text content from the page + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + try: + content, _ = await asyncio.to_thread(get_content_from_url, __request__, url) + + # Truncate if configured (WEB_FETCH_MAX_CONTENT_LENGTH) + # Guard: content may be None if the web loader silently failed + if content is not None: + max_length = getattr(__request__.app.state.config, 'WEB_FETCH_MAX_CONTENT_LENGTH', None) + if max_length and max_length > 0 and len(content) > max_length: + content = content[:max_length] + '\n\n[Content truncated...]' + else: + content = '' + + return content + except Exception as e: + log.exception(f'fetch_url error: {e}') + return json.dumps({'error': str(e)}) + + +# ============================================================================= +# IMAGE GENERATION TOOLS +# ============================================================================= + + +async def generate_image( + prompt: str, + __request__: Request = None, + __user__: dict = None, + __event_emitter__: callable = None, + __chat_id__: str = None, + __message_id__: str = None, +) -> str: + """ + Generate an image based on a text prompt. + + :param prompt: A detailed description of the image to generate + :return: Confirmation that the image was generated, or an error message + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + try: + user = UserModel(**__user__) if __user__ else None + + images = await image_generations( + request=__request__, + form_data=CreateImageForm(prompt=prompt), + user=user, + ) + + # Prepare file entries for the images + image_files = [{'type': 'image', 'url': img['url']} for img in images] + + # Persist files to DB if chat context is available + if __chat_id__ and __message_id__ and images: + db_files = await Chats.add_message_files_by_id_and_message_id( + __chat_id__, + __message_id__, + image_files, + ) + if db_files is not None: + image_files = db_files + + # Emit the images to the UI if event emitter is available + if __event_emitter__ and image_files: + await __event_emitter__( + { + 'type': 'chat:message:files', + 'data': { + 'files': image_files, + }, + } + ) + # Return a message indicating the image is already displayed + return json.dumps( + { + 'status': 'success', + 'message': 'The image has been successfully generated and is already visible to the user in the chat. You do not need to display or embed the image again - just acknowledge that it has been created.', + 'images': images, + }, + ensure_ascii=False, + ) + + return json.dumps({'status': 'success', 'images': images}, ensure_ascii=False) + except Exception as e: + log.exception(f'generate_image error: {e}') + return json.dumps({'error': str(e)}) + + +async def edit_image( + prompt: str, + image_urls: list[str], + __request__: Request = None, + __user__: dict = None, + __event_emitter__: callable = None, + __chat_id__: str = None, + __message_id__: str = None, +) -> str: + """ + Edit existing images based on a text prompt. + + :param prompt: A description of the changes to make to the images + :param image_urls: A list of URLs of the images to edit + :return: Confirmation that the images were edited, or an error message + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + try: + user = UserModel(**__user__) if __user__ else None + + images = await image_edits( + request=__request__, + form_data=EditImageForm(prompt=prompt, image=image_urls), + user=user, + ) + + # Prepare file entries for the images + image_files = [{'type': 'image', 'url': img['url']} for img in images] + + # Persist files to DB if chat context is available + if __chat_id__ and __message_id__ and images: + db_files = await Chats.add_message_files_by_id_and_message_id( + __chat_id__, + __message_id__, + image_files, + ) + if db_files is not None: + image_files = db_files + + # Emit the images to the UI if event emitter is available + if __event_emitter__ and image_files: + await __event_emitter__( + { + 'type': 'chat:message:files', + 'data': { + 'files': image_files, + }, + } + ) + # Return a message indicating the image is already displayed + return json.dumps( + { + 'status': 'success', + 'message': 'The edited image has been successfully generated and is already visible to the user in the chat. You do not need to display or embed the image again - just acknowledge that it has been created.', + 'images': images, + }, + ensure_ascii=False, + ) + + return json.dumps({'status': 'success', 'images': images}, ensure_ascii=False) + except Exception as e: + log.exception(f'edit_image error: {e}') + return json.dumps({'error': str(e)}) + + +# ============================================================================= +# CODE INTERPRETER TOOLS +# ============================================================================= + + +async def execute_code( + code: str, + __request__: Request = None, + __user__: dict = None, + __event_emitter__: callable = None, + __event_call__: callable = None, + __chat_id__: str = None, + __message_id__: str = None, + __metadata__: dict = None, +) -> str: + """ + Execute Python code in a sandboxed environment and return the output. + Use this to perform calculations, data analysis, generate visualizations, + or run any Python code that would help answer the user's question. + + :param code: The Python code to execute + :return: JSON with stdout, stderr, and result from execution + """ + from uuid import uuid4 + + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + try: + # Sanitize code (strips ANSI codes and markdown fences) + code = sanitize_code(code) + + # Import blocked modules from config (same as middleware) + from open_webui.config import CODE_INTERPRETER_BLOCKED_MODULES + + # Add import blocking code if there are blocked modules + if CODE_INTERPRETER_BLOCKED_MODULES: + import textwrap + + blocking_code = textwrap.dedent( + f""" + import builtins + + BLOCKED_MODULES = {CODE_INTERPRETER_BLOCKED_MODULES} + + _real_import = builtins.__import__ + def restricted_import(name, globals=None, locals=None, fromlist=(), level=0): + if name.split('.')[0] in BLOCKED_MODULES: + importer_name = globals.get('__name__') if globals else None + if importer_name == '__main__': + raise ImportError( + f"Direct import of module {{name}} is restricted." + ) + return _real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = restricted_import + """ + ) + code = blocking_code + '\n' + code + + engine = getattr(__request__.app.state.config, 'CODE_INTERPRETER_ENGINE', 'pyodide') + if engine == 'pyodide': + # Execute via frontend pyodide using bidirectional event call + if __event_call__ is None: + return json.dumps( + {'error': 'Event call not available. WebSocket connection required for pyodide execution.'} + ) + + output = await __event_call__( + { + 'type': 'execute:python', + 'data': { + 'id': str(uuid4()), + 'code': code, + 'session_id': (__metadata__.get('session_id') if __metadata__ else None), + 'files': (__metadata__.get('files', []) if __metadata__ else []), + }, + } + ) + + # Parse the output - pyodide returns dict with stdout, stderr, result + if isinstance(output, dict): + stdout = output.get('stdout', '') + stderr = output.get('stderr', '') + result = output.get('result', '') + else: + stdout = '' + stderr = '' + result = str(output) if output else '' + + elif engine == 'jupyter': + from open_webui.utils.code_interpreter import execute_code_jupyter + + output = await execute_code_jupyter( + __request__.app.state.config.CODE_INTERPRETER_JUPYTER_URL, + code, + ( + __request__.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN + if __request__.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH == 'token' + else None + ), + ( + __request__.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD + if __request__.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH == 'password' + else None + ), + __request__.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT, + ) + + stdout = output.get('stdout', '') + stderr = output.get('stderr', '') + result = output.get('result', '') + + else: + return json.dumps({'error': f'Unknown code interpreter engine: {engine}'}) + + # Handle image outputs (base64 encoded) - replace with uploaded URLs + # Get actual user object for image upload (upload_image requires user.id attribute) + if __user__ and __user__.get('id'): + from open_webui.models.users import Users + from open_webui.utils.files import get_image_url_from_base64 + + user = await Users.get_user_by_id(__user__['id']) + + # Extract and upload images from stdout + if stdout and isinstance(stdout, str): + stdout_lines = stdout.split('\n') + for idx, line in enumerate(stdout_lines): + if 'data:image/png;base64' in line: + image_url = await get_image_url_from_base64( + __request__, + line, + __metadata__ or {}, + user, + ) + if image_url: + stdout_lines[idx] = f'![Output Image]({image_url})' + stdout = '\n'.join(stdout_lines) + + # Extract and upload images from result + if result and isinstance(result, str): + result_lines = result.split('\n') + for idx, line in enumerate(result_lines): + if 'data:image/png;base64' in line: + image_url = await get_image_url_from_base64( + __request__, + line, + __metadata__ or {}, + user, + ) + if image_url: + result_lines[idx] = f'![Output Image]({image_url})' + result = '\n'.join(result_lines) + + response = { + 'status': 'success', + 'stdout': stdout, + 'stderr': stderr, + 'result': result, + } + + return json.dumps(response, ensure_ascii=False) + except Exception as e: + log.exception(f'execute_code error: {e}') + return json.dumps({'error': str(e)}) + + +# ============================================================================= +# MEMORY TOOLS +# ============================================================================= + + +async def search_memories( + query: str, + count: int = 5, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Search the user's stored memories for relevant information. + + :param query: The search query to find relevant memories + :param count: Number of memories to return (default 5) + :return: JSON with matching memories and their dates + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + try: + user = UserModel(**__user__) if __user__ else None + + results = await query_memory( + __request__, + QueryMemoryForm(content=query, k=count), + user, + ) + + if results and hasattr(results, 'documents') and results.documents: + memories = [] + for doc_idx, doc in enumerate(results.documents[0]): + memory_id = None + if results.ids and results.ids[0]: + memory_id = results.ids[0][doc_idx] + created_at = 'Unknown' + if results.metadatas and results.metadatas[0][doc_idx].get('created_at'): + created_at = time.strftime( + '%Y-%m-%d', + time.localtime(results.metadatas[0][doc_idx]['created_at']), + ) + memories.append({'id': memory_id, 'date': created_at, 'content': doc}) + return json.dumps(memories, ensure_ascii=False) + else: + return json.dumps([]) + except Exception as e: + log.exception(f'search_memories error: {e}') + return json.dumps({'error': str(e)}) + + +async def add_memory( + content: str, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Store a new memory for the user. + + :param content: The memory content to store + :return: Confirmation that the memory was stored + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + try: + user = UserModel(**__user__) if __user__ else None + + memory = await _add_memory( + __request__, + AddMemoryForm(content=content), + user, + ) + + return json.dumps({'status': 'success', 'id': memory.id}, ensure_ascii=False) + except Exception as e: + log.exception(f'add_memory error: {e}') + return json.dumps({'error': str(e)}) + + +async def replace_memory_content( + memory_id: str, + content: str, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Update the content of an existing memory by its ID. + + :param memory_id: The ID of the memory to update + :param content: The new content for the memory + :return: Confirmation that the memory was updated + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + try: + user = UserModel(**__user__) if __user__ else None + + memory = await update_memory_by_id( + memory_id=memory_id, + request=__request__, + form_data=MemoryUpdateModel(content=content), + user=user, + ) + + return json.dumps( + {'status': 'success', 'id': memory.id, 'content': memory.content}, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'replace_memory_content error: {e}') + return json.dumps({'error': str(e)}) + + +async def delete_memory( + memory_id: str, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Delete a memory by its ID. + + :param memory_id: The ID of the memory to delete + :return: Confirmation that the memory was deleted + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + try: + user = UserModel(**__user__) if __user__ else None + + result = await Memories.delete_memory_by_id_and_user_id(memory_id, user.id) + + if result: + await ASYNC_VECTOR_DB_CLIENT.delete(collection_name=f'user-memory-{user.id}', ids=[memory_id]) + return json.dumps( + {'status': 'success', 'message': f'Memory {memory_id} deleted'}, + ensure_ascii=False, + ) + else: + return json.dumps({'error': 'Memory not found or access denied'}) + except Exception as e: + log.exception(f'delete_memory error: {e}') + return json.dumps({'error': str(e)}) + + +async def list_memories( + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + List all stored memories for the user. + + :return: JSON list of all memories with id, content, and dates + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + try: + user = UserModel(**__user__) if __user__ else None + + memories = await Memories.get_memories_by_user_id(user.id) + + if memories: + result = [ + { + 'id': m.id, + 'content': m.content, + 'created_at': time.strftime('%Y-%m-%d %H:%M', time.localtime(m.created_at)), + 'updated_at': time.strftime('%Y-%m-%d %H:%M', time.localtime(m.updated_at)), + } + for m in memories + ] + return json.dumps(result, ensure_ascii=False) + else: + return json.dumps([]) + except Exception as e: + log.exception(f'list_memories error: {e}') + return json.dumps({'error': str(e)}) + + +# ============================================================================= +# NOTES TOOLS +# ============================================================================= + + +async def search_notes( + query: str, + count: int = 5, + start_timestamp: Optional[int] = None, + end_timestamp: Optional[int] = None, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Search the user's notes by title and content. + + :param query: The search query to find matching notes + :param count: Maximum number of results to return (default: 5) + :param start_timestamp: Only include notes updated after this Unix timestamp (seconds) + :param end_timestamp: Only include notes updated before this Unix timestamp (seconds) + :return: JSON with matching notes containing id, title, and content snippet + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + user_id = __user__.get('id') + user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id)] + + result = await Notes.search_notes( + user_id=user_id, + filter={ + 'query': query, + 'user_id': user_id, + 'group_ids': user_group_ids, + 'permission': 'read', + }, + skip=0, + limit=count * 3, # Fetch more for filtering + ) + + # Convert timestamps to nanoseconds for comparison + start_ts = start_timestamp * 1_000_000_000 if start_timestamp else None + end_ts = end_timestamp * 1_000_000_000 if end_timestamp else None + + notes = [] + for note in result.items: + # Apply date filters (updated_at is in nanoseconds) + if start_ts and note.updated_at < start_ts: + continue + if end_ts and note.updated_at > end_ts: + continue + + # Extract a snippet from the markdown content + content_snippet = '' + if note.data and note.data.get('content', {}).get('md'): + md_content = note.data['content']['md'] + content_lower = md_content.lower() + + # Find the first matching word to center the snippet around. + search_words = query.lower().split() + match_pos = -1 + match_len = len(query) + for word in search_words: + found_pos = content_lower.find(word) + if found_pos != -1: + match_pos = found_pos + match_len = len(word) + break + + if match_pos != -1: + snippet_start = max(0, match_pos - 50) + snippet_end = min(len(md_content), match_pos + match_len + 100) + content_snippet = ( + ('...' if snippet_start > 0 else '') + + md_content[snippet_start:snippet_end] + + ('...' if snippet_end < len(md_content) else '') + ) + else: + content_snippet = md_content[:150] + ('...' if len(md_content) > 150 else '') + + notes.append( + { + 'id': note.id, + 'title': note.title, + 'snippet': content_snippet, + 'updated_at': note.updated_at, + } + ) + + if len(notes) >= count: + break + + return json.dumps(notes, ensure_ascii=False) + except Exception as e: + log.exception(f'search_notes error: {e}') + return json.dumps({'error': str(e)}) + + +async def view_note( + note_id: str, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Get the full content of a note by its ID. + + :param note_id: The ID of the note to retrieve + :return: JSON with the note's id, title, and full markdown content + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + note = await Notes.get_note_by_id(note_id) + + if not note: + return json.dumps({'error': 'Note not found'}) + + # Check access permission + user_id = __user__.get('id') + user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id)] + + from open_webui.models.access_grants import AccessGrants + + if note.user_id != user_id and not await AccessGrants.has_access( + user_id=user_id, + resource_type='note', + resource_id=note.id, + permission='read', + user_group_ids=set(user_group_ids), + ): + return json.dumps({'error': 'Access denied'}) + + # Extract markdown content + content = '' + if note.data and note.data.get('content', {}).get('md'): + content = note.data['content']['md'] + + return json.dumps( + { + 'id': note.id, + 'title': note.title, + 'content': content, + 'updated_at': note.updated_at, + 'created_at': note.created_at, + }, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'view_note error: {e}') + return json.dumps({'error': str(e)}) + + +async def write_note( + title: str, + content: str, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Create a new note with the given title and content. + + :param title: The title of the new note + :param content: The markdown content for the note + :return: JSON with success status and new note id + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + from open_webui.models.notes import NoteForm + + user_id = __user__.get('id') + + form = NoteForm( + title=title, + data={'content': {'md': content}}, + access_grants=[], # Private by default - only owner can access + ) + + new_note = await Notes.insert_new_note(user_id, form) + + if not new_note: + return json.dumps({'error': 'Failed to create note'}) + + return json.dumps( + { + 'status': 'success', + 'id': new_note.id, + 'title': new_note.title, + 'created_at': new_note.created_at, + }, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'write_note error: {e}') + return json.dumps({'error': str(e)}) + + +async def replace_note_content( + note_id: str, + content: str, + title: Optional[str] = None, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Update the content of a note. Use this to modify task lists, add notes, or update content. + + :param note_id: The ID of the note to update + :param content: The new markdown content for the note + :param title: Optional new title for the note + :return: JSON with success status and updated note info + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + from open_webui.models.notes import NoteUpdateForm + + note = await Notes.get_note_by_id(note_id) + + if not note: + return json.dumps({'error': 'Note not found'}) + + # Check write permission + user_id = __user__.get('id') + user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id)] + + from open_webui.models.access_grants import AccessGrants + + if note.user_id != user_id and not await AccessGrants.has_access( + user_id=user_id, + resource_type='note', + resource_id=note.id, + permission='write', + user_group_ids=set(user_group_ids), + ): + return json.dumps({'error': 'Write access denied'}) + + # Build update form + update_data = {'data': {'content': {'md': content}}} + if title: + update_data['title'] = title + + form = NoteUpdateForm(**update_data) + updated_note = await Notes.update_note_by_id(note_id, form) + + if not updated_note: + return json.dumps({'error': 'Failed to update note'}) + + return json.dumps( + { + 'status': 'success', + 'id': updated_note.id, + 'title': updated_note.title, + 'updated_at': updated_note.updated_at, + }, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'replace_note_content error: {e}') + return json.dumps({'error': str(e)}) + + +# ============================================================================= +# CHATS TOOLS +# ============================================================================= + + +async def search_chats( + query: str, + count: int = 5, + start_timestamp: Optional[int] = None, + end_timestamp: Optional[int] = None, + __request__: Request = None, + __user__: dict = None, + __chat_id__: str = None, +) -> str: + """ + Search the user's previous chat conversations by title and message content. + + :param query: The search query to find matching chats + :param count: Maximum number of results to return (default: 5) + :param start_timestamp: Only include chats updated after this Unix timestamp (seconds) + :param end_timestamp: Only include chats updated before this Unix timestamp (seconds) + :return: JSON with matching chats containing id, title, updated_at, and content snippet + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + user_id = __user__.get('id') + + chats = await Chats.get_chats_by_user_id_and_search_text( + user_id=user_id, + search_text=query, + include_archived=False, + skip=0, + limit=count * 3, # Fetch more for filtering + ) + + results = [] + for chat in chats: + # Skip the current chat to avoid showing it in search results + if __chat_id__ and chat.id == __chat_id__: + continue + + # Apply date filters (updated_at is in seconds) + if start_timestamp and chat.updated_at < start_timestamp: + continue + if end_timestamp and chat.updated_at > end_timestamp: + continue + + # Find a matching message snippet + snippet = '' + messages = chat.chat.get('history', {}).get('messages', {}) + lower_query = query.lower() + + for msg_id, msg in messages.items(): + content = msg.get('content', '') + if isinstance(content, str) and lower_query in content.lower(): + idx = content.lower().find(lower_query) + start = max(0, idx - 50) + end = min(len(content), idx + len(query) + 100) + snippet = ('...' if start > 0 else '') + content[start:end] + ('...' if end < len(content) else '') + break + + if not snippet and lower_query in chat.title.lower(): + snippet = f'Title match: {chat.title}' + + results.append( + { + 'id': chat.id, + 'title': chat.title, + 'snippet': snippet, + 'updated_at': chat.updated_at, + } + ) + + if len(results) >= count: + break + + return json.dumps(results, ensure_ascii=False) + except Exception as e: + log.exception(f'search_chats error: {e}') + return json.dumps({'error': str(e)}) + + +async def view_chat( + chat_id: str, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Get the full conversation history of a chat by its ID. + + :param chat_id: The ID of the chat to retrieve + :return: JSON with the chat's id, title, and messages + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + user_id = __user__.get('id') + + chat = await Chats.get_chat_by_id_and_user_id(chat_id, user_id) + + if not chat: + return json.dumps({'error': 'Chat not found or access denied'}) + + # Extract messages from history + messages = [] + history = chat.chat.get('history', {}) + msg_dict = history.get('messages', {}) + + # Build message chain from currentId + current_id = history.get('currentId') + visited = set() + + while current_id and current_id not in visited: + visited.add(current_id) + msg = msg_dict.get(current_id) + if msg: + messages.append( + { + 'role': msg.get('role', ''), + 'content': msg.get('content', ''), + } + ) + current_id = msg.get('parentId') if msg else None + + # Reverse to get chronological order + messages.reverse() + + return json.dumps( + { + 'id': chat.id, + 'title': chat.title, + 'messages': messages, + 'updated_at': chat.updated_at, + 'created_at': chat.created_at, + }, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'view_chat error: {e}') + return json.dumps({'error': str(e)}) + + +# ============================================================================= +# CHANNELS TOOLS +# ============================================================================= + + +async def search_channels( + query: str, + count: int = 5, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Search for channels by name and description that the user has access to. + + :param query: The search query to find matching channels + :param count: Maximum number of results to return (default: 5) + :return: JSON with matching channels containing id, name, description, and type + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + user_id = __user__.get('id') + + # Get all channels the user has access to + all_channels = await Channels.get_channels_by_user_id(user_id) + + # Filter by query + lower_query = query.lower() + matching_channels = [] + + for channel in all_channels: + name_match = lower_query in channel.name.lower() if channel.name else False + desc_match = lower_query in (channel.description or '').lower() + + if name_match or desc_match: + matching_channels.append( + { + 'id': channel.id, + 'name': channel.name, + 'description': channel.description or '', + 'type': channel.type or 'public', + } + ) + + if len(matching_channels) >= count: + break + + return json.dumps(matching_channels, ensure_ascii=False) + except Exception as e: + log.exception(f'search_channels error: {e}') + return json.dumps({'error': str(e)}) + + +async def search_channel_messages( + query: str, + count: int = 10, + start_timestamp: Optional[int] = None, + end_timestamp: Optional[int] = None, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Search for messages in channels the user is a member of, including thread replies. + + :param query: The search query to find matching messages + :param count: Maximum number of results to return (default: 10) + :param start_timestamp: Only include messages created after this Unix timestamp (seconds) + :param end_timestamp: Only include messages created before this Unix timestamp (seconds) + :return: JSON with matching messages containing channel info, message content, and thread context + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + user_id = __user__.get('id') + + # Get all channels the user has access to + user_channels = await Channels.get_channels_by_user_id(user_id) + channel_ids = [c.id for c in user_channels] + channel_map = {c.id: c for c in user_channels} + + if not channel_ids: + return json.dumps([]) + + # Convert timestamps to nanoseconds (Message.created_at is in nanoseconds) + start_ts = start_timestamp * 1_000_000_000 if start_timestamp else None + end_ts = end_timestamp * 1_000_000_000 if end_timestamp else None + + # Search messages using the model method + matching_messages = await Messages.search_messages_by_channel_ids( + channel_ids=channel_ids, + query=query, + start_timestamp=start_ts, + end_timestamp=end_ts, + limit=count, + ) + + results = [] + for msg in matching_messages: + channel = channel_map.get(msg.channel_id) + + # Extract snippet around the match + content = msg.content or '' + lower_query = query.lower() + idx = content.lower().find(lower_query) + if idx != -1: + start = max(0, idx - 50) + end = min(len(content), idx + len(query) + 100) + snippet = ('...' if start > 0 else '') + content[start:end] + ('...' if end < len(content) else '') + else: + snippet = content[:150] + ('...' if len(content) > 150 else '') + + results.append( + { + 'channel_id': msg.channel_id, + 'channel_name': channel.name if channel else 'Unknown', + 'message_id': msg.id, + 'content_snippet': snippet, + 'is_thread_reply': msg.parent_id is not None, + 'parent_id': msg.parent_id, + 'created_at': msg.created_at, + } + ) + + return json.dumps(results, ensure_ascii=False) + except Exception as e: + log.exception(f'search_channel_messages error: {e}') + return json.dumps({'error': str(e)}) + + +async def view_channel_message( + message_id: str, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Get the full content of a channel message by its ID, including thread replies. + + :param message_id: The ID of the message to retrieve + :return: JSON with the message content, channel info, and thread replies if any + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + user_id = __user__.get('id') + + message = await Messages.get_message_by_id(message_id) + + if not message: + return json.dumps({'error': 'Message not found'}) + + # Verify user has access to the channel + channel = await Channels.get_channel_by_id(message.channel_id) + if not channel: + return json.dumps({'error': 'Channel not found'}) + + # Check if user has access to the channel + user_channels = await Channels.get_channels_by_user_id(user_id) + channel_ids = [c.id for c in user_channels] + + if message.channel_id not in channel_ids: + return json.dumps({'error': 'Access denied'}) + + # Build response with thread information + result = { + 'id': message.id, + 'channel_id': message.channel_id, + 'channel_name': channel.name, + 'content': message.content, + 'user_id': message.user_id, + 'is_thread_reply': message.parent_id is not None, + 'parent_id': message.parent_id, + 'reply_count': message.reply_count, + 'created_at': message.created_at, + 'updated_at': message.updated_at, + } + + # Include user info if available + if message.user: + result['user_name'] = message.user.name + + return json.dumps(result, ensure_ascii=False) + except Exception as e: + log.exception(f'view_channel_message error: {e}') + return json.dumps({'error': str(e)}) + + +async def view_channel_thread( + parent_message_id: str, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Get all messages in a channel thread, including the parent message and all replies. + + :param parent_message_id: The ID of the parent message that started the thread + :return: JSON with the parent message and all thread replies in chronological order + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + user_id = __user__.get('id') + + # Get the parent message + parent_message = await Messages.get_message_by_id(parent_message_id) + + if not parent_message: + return json.dumps({'error': 'Message not found'}) + + # Verify user has access to the channel + channel = await Channels.get_channel_by_id(parent_message.channel_id) + if not channel: + return json.dumps({'error': 'Channel not found'}) + + user_channels = await Channels.get_channels_by_user_id(user_id) + channel_ids = [c.id for c in user_channels] + + if parent_message.channel_id not in channel_ids: + return json.dumps({'error': 'Access denied'}) + + # Get all thread replies + thread_replies = await Messages.get_thread_replies_by_message_id(parent_message_id) + + # Build the response + messages = [] + + # Add parent message first + messages.append( + { + 'id': parent_message.id, + 'content': parent_message.content, + 'user_id': parent_message.user_id, + 'user_name': parent_message.user.name if parent_message.user else None, + 'is_parent': True, + 'created_at': parent_message.created_at, + } + ) + + # Add thread replies (reverse to get chronological order) + for reply in reversed(thread_replies): + messages.append( + { + 'id': reply.id, + 'content': reply.content, + 'user_id': reply.user_id, + 'user_name': reply.user.name if reply.user else None, + 'is_parent': False, + 'reply_to_id': reply.reply_to_id, + 'created_at': reply.created_at, + } + ) + + return json.dumps( + { + 'channel_id': parent_message.channel_id, + 'channel_name': channel.name, + 'thread_id': parent_message_id, + 'message_count': len(messages), + 'messages': messages, + }, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'view_channel_thread error: {e}') + return json.dumps({'error': str(e)}) + + +# ============================================================================= +# KNOWLEDGE BASE TOOLS +# ============================================================================= + + +async def list_knowledge_bases( + count: int = 10, + skip: int = 0, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + List the user's accessible knowledge bases. + + :param count: Maximum number of KBs to return (default: 10) + :param skip: Number of results to skip for pagination (default: 0) + :return: JSON with KBs containing id, name, description, and file_count + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + from open_webui.models.knowledge import Knowledges + + user_id = __user__.get('id') + user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id)] + + result = await Knowledges.search_knowledge_bases( + user_id, + filter={ + 'query': '', + 'user_id': user_id, + 'group_ids': user_group_ids, + }, + skip=skip, + limit=count, + ) + + knowledge_bases = [] + for knowledge_base in result.items: + files = await Knowledges.get_files_by_id(knowledge_base.id) + file_count = len(files) if files else 0 + + knowledge_bases.append( + { + 'id': knowledge_base.id, + 'name': knowledge_base.name, + 'description': knowledge_base.description or '', + 'file_count': file_count, + 'updated_at': knowledge_base.updated_at, + } + ) + + return json.dumps(knowledge_bases, ensure_ascii=False) + except Exception as e: + log.exception(f'list_knowledge_bases error: {e}') + return json.dumps({'error': str(e)}) + + +async def search_knowledge_bases( + query: str, + count: int = 5, + skip: int = 0, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Search the user's accessible knowledge bases by name and description. + + :param query: The search query to find matching knowledge bases + :param count: Maximum number of results to return (default: 5) + :param skip: Number of results to skip for pagination (default: 0) + :return: JSON with matching KBs containing id, name, description, and file_count + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + from open_webui.models.knowledge import Knowledges + + user_id = __user__.get('id') + user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id)] + + result = await Knowledges.search_knowledge_bases( + user_id, + filter={ + 'query': query, + 'user_id': user_id, + 'group_ids': user_group_ids, + }, + skip=skip, + limit=count, + ) + + knowledge_bases = [] + for knowledge_base in result.items: + files = await Knowledges.get_files_by_id(knowledge_base.id) + file_count = len(files) if files else 0 + + knowledge_bases.append( + { + 'id': knowledge_base.id, + 'name': knowledge_base.name, + 'description': knowledge_base.description or '', + 'file_count': file_count, + 'updated_at': knowledge_base.updated_at, + } + ) + + return json.dumps(knowledge_bases, ensure_ascii=False) + except Exception as e: + log.exception(f'search_knowledge_bases error: {e}') + return json.dumps({'error': str(e)}) + + +async def search_knowledge_files( + query: str, + knowledge_id: Optional[str] = None, + count: int = 5, + skip: int = 0, + __request__: Request = None, + __user__: dict = None, + __model_knowledge__: Optional[list[dict]] = None, +) -> str: + """ + Search files by filename across knowledge bases the user has access to. + When the model has attached knowledge, searches only within attached KBs and files. + + :param query: The search query to find matching files by filename + :param knowledge_id: Optional KB id to limit search to a specific knowledge base + :param count: Maximum number of results to return (default: 5) + :param skip: Number of results to skip for pagination (default: 0) + :return: JSON with matching files containing id, filename, and updated_at + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + from open_webui.models.knowledge import Knowledges + from open_webui.models.files import Files + from open_webui.models.access_grants import AccessGrants + + user_id = __user__.get('id') + user_role = __user__.get('role', 'user') + user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id)] + + # When model has attached knowledge, scope to attached KBs/files only + if __model_knowledge__: + attached_kb_ids = set() + attached_file_ids = set() + + for item in __model_knowledge__: + item_type = item.get('type') + item_id = item.get('id') + if item_type == 'collection': + attached_kb_ids.add(item_id) + elif item_type == 'file': + attached_file_ids.add(item_id) + + # If knowledge_id specified, verify it's in the attached set + if knowledge_id: + if knowledge_id not in attached_kb_ids: + return json.dumps({'error': f'Knowledge base {knowledge_id} is not attached to this model'}) + attached_kb_ids = {knowledge_id} + + all_files = [] + + # Search within attached KBs + for kb_id in attached_kb_ids: + knowledge = await Knowledges.get_knowledge_by_id(kb_id) + if not knowledge: + continue + + if not ( + user_role == 'admin' + or knowledge.user_id == user_id + or await AccessGrants.has_access( + user_id=user_id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='read', + user_group_ids=set(user_group_ids), + ) + ): + continue + + result = await Knowledges.search_files_by_id( + knowledge_id=kb_id, + user_id=user_id, + filter={'query': query}, + skip=0, + limit=count + skip, + ) + + for file in result.items: + all_files.append( + { + 'id': file.id, + 'filename': file.filename, + 'knowledge_id': knowledge.id, + 'knowledge_name': knowledge.name, + 'updated_at': file.updated_at, + } + ) + + # Search within directly attached files (filename match) + if not knowledge_id and attached_file_ids: + query_lower = query.lower() if query else '' + for file_id in attached_file_ids: + file = await Files.get_file_by_id(file_id) + if file and (not query_lower or query_lower in file.filename.lower()): + all_files.append( + { + 'id': file.id, + 'filename': file.filename, + 'updated_at': file.updated_at, + } + ) + + # Apply pagination across combined results + all_files = all_files[skip : skip + count] + return json.dumps(all_files, ensure_ascii=False) + + # No attached knowledge - search all accessible KBs + if knowledge_id: + result = await Knowledges.search_files_by_id( + knowledge_id=knowledge_id, + user_id=user_id, + filter={'query': query}, + skip=skip, + limit=count, + ) + else: + result = await Knowledges.search_knowledge_files( + filter={ + 'query': query, + 'user_id': user_id, + 'group_ids': user_group_ids, + }, + skip=skip, + limit=count, + ) + + files = [] + for file in result.items: + file_info = { + 'id': file.id, + 'filename': file.filename, + 'updated_at': file.updated_at, + } + if hasattr(file, 'collection') and file.collection: + file_info['knowledge_id'] = file.collection.get('id', '') + file_info['knowledge_name'] = file.collection.get('name', '') + files.append(file_info) + + return json.dumps(files, ensure_ascii=False) + except Exception as e: + log.exception(f'search_knowledge_files error: {e}') + return json.dumps({'error': str(e)}) + + +# Hard cap for view_file / view_knowledge_file output +MAX_VIEW_FILE_CHARS = 100_000 +DEFAULT_VIEW_FILE_MAX_CHARS = 10_000 + + +async def view_file( + file_id: str, + offset: int = 0, + max_chars: int = DEFAULT_VIEW_FILE_MAX_CHARS, + __request__: Request = None, + __user__: dict = None, + __model_knowledge__: Optional[list[dict]] = None, +) -> str: + """ + Get the content of a file by its ID. Supports pagination for large files. + + :param file_id: The ID of the file to retrieve + :param offset: Character offset to start reading from (default: 0) + :param max_chars: Maximum characters to return (default: 10000, hard cap: 100000) + :return: JSON with the file's id, filename, content, and pagination metadata if truncated + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + # Coerce parameters from LLM tool calls (may come as strings) + if isinstance(offset, str): + try: + offset = int(offset) + except ValueError: + offset = 0 + if isinstance(max_chars, str): + try: + max_chars = int(max_chars) + except ValueError: + max_chars = DEFAULT_VIEW_FILE_MAX_CHARS + + # Enforce hard cap + max_chars = min(max(max_chars, 1), MAX_VIEW_FILE_CHARS) + offset = max(offset, 0) + + try: + from open_webui.models.files import Files + from open_webui.utils.access_control.files import has_access_to_file + + user_id = __user__.get('id') + user_role = __user__.get('role', 'user') + + file = await Files.get_file_by_id(file_id) + if not file: + return json.dumps({'error': 'File not found'}) + + if ( + file.user_id != user_id + and user_role != 'admin' + and not any( + item.get('type') == 'file' and item.get('id') == file_id for item in (__model_knowledge__ or []) + ) + and not await has_access_to_file( + file_id=file_id, + access_type='read', + user=UserModel(**__user__), + ) + ): + return json.dumps({'error': 'File not found'}) + + content = '' + if file.data: + content = file.data.get('content', '') + + total_chars = len(content) + sliced = content[offset : offset + max_chars] + is_truncated = (offset + len(sliced)) < total_chars + + result = { + 'id': file.id, + 'filename': file.filename, + 'content': sliced, + 'updated_at': file.updated_at, + 'created_at': file.created_at, + } + + if is_truncated or offset > 0: + result['truncated'] = is_truncated + result['total_chars'] = total_chars + result['returned_chars'] = len(sliced) + result['offset'] = offset + if is_truncated: + result['next_offset'] = offset + len(sliced) + + return json.dumps(result, ensure_ascii=False) + except Exception as e: + log.exception(f'view_file error: {e}') + return json.dumps({'error': str(e)}) + + +async def view_knowledge_file( + file_id: str, + offset: int = 0, + max_chars: int = DEFAULT_VIEW_FILE_MAX_CHARS, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Get the content of a file from a knowledge base. Supports pagination for large files. + + :param file_id: The ID of the file to retrieve + :param offset: Character offset to start reading from (default: 0) + :param max_chars: Maximum characters to return (default: 10000, hard cap: 100000) + :return: JSON with the file's id, filename, content, and pagination metadata if truncated + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + # Coerce parameters from LLM tool calls (may come as strings) + if isinstance(offset, str): + try: + offset = int(offset) + except ValueError: + offset = 0 + if isinstance(max_chars, str): + try: + max_chars = int(max_chars) + except ValueError: + max_chars = DEFAULT_VIEW_FILE_MAX_CHARS + + # Enforce hard cap + max_chars = min(max(max_chars, 1), MAX_VIEW_FILE_CHARS) + offset = max(offset, 0) + + try: + from open_webui.models.files import Files + from open_webui.models.knowledge import Knowledges + from open_webui.models.access_grants import AccessGrants + + user_id = __user__.get('id') + user_role = __user__.get('role', 'user') + user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id)] + + file = await Files.get_file_by_id(file_id) + if not file: + return json.dumps({'error': 'File not found'}) + + # Check access via any KB containing this file + knowledges = await Knowledges.get_knowledges_by_file_id(file_id) + has_knowledge_access = False + knowledge_info = None + + for knowledge_base in knowledges: + if ( + user_role == 'admin' + or knowledge_base.user_id == user_id + or await AccessGrants.has_access( + user_id=user_id, + resource_type='knowledge', + resource_id=knowledge_base.id, + permission='read', + user_group_ids=set(user_group_ids), + ) + ): + has_knowledge_access = True + knowledge_info = {'id': knowledge_base.id, 'name': knowledge_base.name} + break + + if not has_knowledge_access: + if file.user_id != user_id and user_role != 'admin': + return json.dumps({'error': 'Access denied'}) + + content = '' + if file.data: + content = file.data.get('content', '') + + total_chars = len(content) + sliced = content[offset : offset + max_chars] + is_truncated = (offset + len(sliced)) < total_chars + + result = { + 'id': file.id, + 'filename': file.filename, + 'content': sliced, + 'updated_at': file.updated_at, + 'created_at': file.created_at, + } + if knowledge_info: + result['knowledge_id'] = knowledge_info['id'] + result['knowledge_name'] = knowledge_info['name'] + + if is_truncated or offset > 0: + result['truncated'] = is_truncated + result['total_chars'] = total_chars + result['returned_chars'] = len(sliced) + result['offset'] = offset + if is_truncated: + result['next_offset'] = offset + len(sliced) + + return json.dumps(result, ensure_ascii=False) + except Exception as e: + log.exception(f'view_knowledge_file error: {e}') + return json.dumps({'error': str(e)}) + + +async def list_knowledge( + __request__: Request = None, + __user__: dict = None, + __model_knowledge__: Optional[list[dict]] = None, +) -> str: + """ + List all knowledge bases, files, and notes attached to the current model. + Use this first to discover what knowledge is available before querying or reading files. + + :return: JSON with knowledge_bases, files, and notes attached to this model + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + if not __model_knowledge__: + return json.dumps({'knowledge_bases': [], 'files': [], 'notes': []}) + + try: + from open_webui.models.knowledge import Knowledges + from open_webui.models.files import Files + from open_webui.models.notes import Notes + from open_webui.models.access_grants import AccessGrants + + user_id = __user__.get('id') + user_role = __user__.get('role', 'user') + user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id)] + + knowledge_bases = [] + files = [] + notes = [] + + for item in __model_knowledge__: + item_type = item.get('type') + item_id = item.get('id') + + if item_type == 'collection': + knowledge = await Knowledges.get_knowledge_by_id(item_id) + if knowledge and ( + user_role == 'admin' + or knowledge.user_id == user_id + or await AccessGrants.has_access( + user_id=user_id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='read', + user_group_ids=set(user_group_ids), + ) + ): + kb_files = await Knowledges.get_files_by_id(knowledge.id) + file_count = len(kb_files) if kb_files else 0 + + kb_entry = { + 'id': knowledge.id, + 'name': knowledge.name, + 'description': knowledge.description or '', + 'file_count': file_count, + } + + # Include file listing for each KB + if kb_files: + kb_entry['files'] = [{'id': f.id, 'filename': f.filename} for f in kb_files] + + knowledge_bases.append(kb_entry) + + elif item_type == 'file': + file = await Files.get_file_by_id(item_id) + if file: + files.append( + { + 'id': file.id, + 'filename': file.filename, + 'updated_at': file.updated_at, + } + ) + + elif item_type == 'note': + note = await Notes.get_note_by_id(item_id) + if note and ( + user_role == 'admin' + or note.user_id == user_id + or await AccessGrants.has_access( + user_id=user_id, + resource_type='note', + resource_id=note.id, + permission='read', + ) + ): + notes.append( + { + 'id': note.id, + 'title': note.title, + } + ) + + return json.dumps( + { + 'knowledge_bases': knowledge_bases, + 'files': files, + 'notes': notes, + }, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'list_knowledge error: {e}') + return json.dumps({'error': str(e)}) + + +async def query_knowledge_files( + query: str, + knowledge_ids: Optional[list[str]] = None, + count: int = 5, + __request__: Request = None, + __user__: dict = None, + __model_knowledge__: list[dict] = None, +) -> str: + """ + Search knowledge base files using semantic/vector search. Searches across collections (KBs), + individual files, and notes that the user has access to. + + :param query: The search query to find semantically relevant content + :param knowledge_ids: Optional list of KB ids to limit search to specific knowledge bases + :param count: Maximum number of results to return (default: 5) + :return: JSON with relevant chunks containing content, source filename, and relevance score + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + # Coerce parameters from LLM tool calls (may come as strings) + if isinstance(count, str): + try: + count = int(count) + except ValueError: + count = 5 # Default fallback + + # Handle knowledge_ids being string "None", "null", or empty + if isinstance(knowledge_ids, str): + if knowledge_ids.lower() in ('none', 'null', ''): + knowledge_ids = None + else: + # Try to parse as JSON array if it looks like one + try: + knowledge_ids = json.loads(knowledge_ids) + except json.JSONDecodeError: + # Treat as single ID + knowledge_ids = [knowledge_ids] + + try: + from open_webui.models.knowledge import Knowledges + from open_webui.models.files import Files + from open_webui.models.notes import Notes + from open_webui.retrieval.utils import query_collection + from open_webui.models.access_grants import AccessGrants + + user_id = __user__.get('id') + user_role = __user__.get('role', 'user') + user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id)] + + embedding_function = __request__.app.state.EMBEDDING_FUNCTION + if not embedding_function: + return json.dumps({'error': 'Embedding function not configured'}) + + collection_names = [] + note_results = [] # Notes aren't vectorized, handle separately + + # If model has attached knowledge, use those + if __model_knowledge__: + for item in __model_knowledge__: + item_type = item.get('type') + item_id = item.get('id') + + if item_type == 'collection': + # Knowledge base - use KB ID as collection name + knowledge = await Knowledges.get_knowledge_by_id(item_id) + if knowledge and ( + user_role == 'admin' + or knowledge.user_id == user_id + or await AccessGrants.has_access( + user_id=user_id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='read', + user_group_ids=set(user_group_ids), + ) + ): + collection_names.append(item_id) + + elif item_type == 'file': + # Individual file - use file-{id} as collection name + file = await Files.get_file_by_id(item_id) + if file: + collection_names.append(f'file-{item_id}') + + elif item_type == 'note': + # Note - always return full content as context + note = await Notes.get_note_by_id(item_id) + if note and ( + user_role == 'admin' + or note.user_id == user_id + or await AccessGrants.has_access( + user_id=user_id, + resource_type='note', + resource_id=note.id, + permission='read', + ) + ): + content = note.data.get('content', {}).get('md', '') + note_results.append( + { + 'content': content, + 'source': note.title, + 'note_id': note.id, + 'type': 'note', + } + ) + + elif knowledge_ids: + # User specified specific KBs + for knowledge_id in knowledge_ids: + knowledge = await Knowledges.get_knowledge_by_id(knowledge_id) + if knowledge and ( + user_role == 'admin' + or knowledge.user_id == user_id + or await AccessGrants.has_access( + user_id=user_id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='read', + user_group_ids=set(user_group_ids), + ) + ): + collection_names.append(knowledge_id) + else: + # No model knowledge and no specific IDs - search all accessible KBs + result = await Knowledges.search_knowledge_bases( + user_id, + filter={ + 'query': '', + 'user_id': user_id, + 'group_ids': user_group_ids, + }, + skip=0, + limit=50, + ) + collection_names = [knowledge_base.id for knowledge_base in result.items] + + chunks = [] + + # Add note results first + chunks.extend(note_results) + + # Query vector collections if any + if collection_names: + query_results = await query_collection( + __request__, + collection_names=collection_names, + queries=[query], + embedding_function=embedding_function, + k=count, + ) + + if query_results and 'documents' in query_results: + documents = query_results.get('documents', [[]])[0] + metadatas = query_results.get('metadatas', [[]])[0] + distances = query_results.get('distances', [[]])[0] + + for idx, doc in enumerate(documents): + chunk_info = { + 'content': doc, + 'source': metadatas[idx].get('source', metadatas[idx].get('name', 'Unknown')), + 'file_id': metadatas[idx].get('file_id', ''), + } + if idx < len(distances): + chunk_info['distance'] = distances[idx] + chunks.append(chunk_info) + + # Limit to requested count + chunks = chunks[:count] + + return json.dumps(chunks, ensure_ascii=False) + except Exception as e: + log.exception(f'query_knowledge_files error: {e}') + return json.dumps({'error': str(e)}) + + +async def query_knowledge_bases( + query: str, + count: int = 5, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Search knowledge bases by semantic similarity to query. + Finds KBs whose name/description match the meaning of your query. + Use this to discover relevant knowledge bases before querying their files. + + :param query: Natural language query describing what you're looking for + :param count: Maximum results (default: 5) + :return: JSON with matching KBs (id, name, description, similarity) + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + import heapq + from open_webui.models.knowledge import Knowledges + from open_webui.routers.knowledge import KNOWLEDGE_BASES_COLLECTION + from open_webui.retrieval.vector.async_client import ASYNC_VECTOR_DB_CLIENT + + user_id = __user__.get('id') + user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id)] + query_embedding = await __request__.app.state.EMBEDDING_FUNCTION(query) + + # Min-heap of (distance, knowledge_base_id) - only holds top `count` results + top_results_heap = [] + seen_ids = set() + page_offset = 0 + page_size = 100 + + while True: + accessible_knowledge_bases = await Knowledges.search_knowledge_bases( + user_id, + filter={'user_id': user_id, 'group_ids': user_group_ids}, + skip=page_offset, + limit=page_size, + ) + + if not accessible_knowledge_bases.items: + break + + accessible_ids = [kb.id for kb in accessible_knowledge_bases.items] + + search_results = await ASYNC_VECTOR_DB_CLIENT.search( + collection_name=KNOWLEDGE_BASES_COLLECTION, + vectors=[query_embedding], + filter={'knowledge_base_id': {'$in': accessible_ids}}, + limit=count, + ) + + if search_results and search_results.ids and search_results.ids[0]: + result_ids = search_results.ids[0] + result_distances = search_results.distances[0] if search_results.distances else [0] * len(result_ids) + + for knowledge_base_id, distance in zip(result_ids, result_distances): + if knowledge_base_id in seen_ids: + continue + seen_ids.add(knowledge_base_id) + + if len(top_results_heap) < count: + heapq.heappush(top_results_heap, (distance, knowledge_base_id)) + elif distance > top_results_heap[0][0]: + heapq.heapreplace(top_results_heap, (distance, knowledge_base_id)) + + page_offset += page_size + if len(accessible_knowledge_bases.items) < page_size: + break + if page_offset >= MAX_KNOWLEDGE_BASE_SEARCH_ITEMS: + break + + # Sort by distance descending (best first) and fetch KB details + sorted_results = sorted(top_results_heap, key=lambda x: x[0], reverse=True) + + matching_knowledge_bases = [] + for distance, knowledge_base_id in sorted_results: + knowledge_base = await Knowledges.get_knowledge_by_id(knowledge_base_id) + if knowledge_base: + matching_knowledge_bases.append( + { + 'id': knowledge_base.id, + 'name': knowledge_base.name, + 'description': knowledge_base.description or '', + 'similarity': round(distance, 4), + } + ) + + return json.dumps(matching_knowledge_bases, ensure_ascii=False) + + except Exception as e: + log.exception(f'query_knowledge_bases error: {e}') + return json.dumps({'error': str(e)}) + + +# ============================================================================= +# SKILLS TOOLS +# ============================================================================= + + +async def view_skill( + id: str, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Load the full instructions of a skill by its id from the available skills manifest. + Use this when you need detailed instructions for a skill listed in . + + :param id: The id of the skill to load (as shown in the manifest) + :return: The full skill instructions as markdown content + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + from open_webui.models.skills import Skills + from open_webui.models.access_grants import AccessGrants + + user_id = __user__.get('id') + + # Direct DB lookup by id (case-insensitive since IDs are stored lowercase) + skill = await Skills.get_skill_by_id(id.lower()) + + if not skill or not skill.is_active: + return json.dumps({'error': f"Skill '{id}' not found"}) + + # Check user access + user_role = __user__.get('role', 'user') + if user_role != 'admin' and skill.user_id != user_id: + user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id)] + if not await AccessGrants.has_access( + user_id=user_id, + resource_type='skill', + resource_id=skill.id, + permission='read', + user_group_ids=set(user_group_ids), + ): + return json.dumps({'error': 'Access denied'}) + + return json.dumps( + { + 'name': skill.name, + 'content': skill.content, + }, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'view_skill error: {e}') + return json.dumps({'error': str(e)}) + + +# ============================================================================= +# TASK MANAGEMENT TOOLS +# ============================================================================= + +from pydantic import BaseModel, Field +from typing import Literal + +VALID_TASK_STATUSES = {'pending', 'in_progress', 'completed', 'cancelled'} + + +class TaskItem(BaseModel): + id: Optional[str] = Field(None, description='Unique identifier for the task. Auto-generated if omitted.') + content: str = Field(..., description='Task description.') + status: Literal['pending', 'in_progress', 'completed', 'cancelled'] = Field('pending', description='Task status.') + + +def _task_summary(all_tasks: list[dict]) -> dict: + """Build summary counts for a task list.""" + pending = sum(1 for t in all_tasks if t['status'] == 'pending') + in_progress = sum(1 for t in all_tasks if t['status'] == 'in_progress') + completed = sum(1 for t in all_tasks if t['status'] == 'completed') + cancelled = sum(1 for t in all_tasks if t['status'] == 'cancelled') + return { + 'total': len(all_tasks), + 'pending': pending, + 'in_progress': in_progress, + 'completed': completed, + 'cancelled': cancelled, + } + + +async def _emit_tasks(event_emitter, all_tasks: list[dict]): + """Persist task state to the UI.""" + if event_emitter: + await event_emitter( + { + 'type': 'chat:message:tasks', + 'data': { + 'tasks': all_tasks, + }, + } + ) + + +async def create_tasks( + tasks: list[TaskItem], + __chat_id__: str = None, + __message_id__: str = None, + __event_emitter__: callable = None, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Create a task checklist to track progress on multi-step work. + Call this once at the start to define all steps, then use + update_task to mark each task as you complete it. + + :param tasks: List of task items. Each item: content (string, required), status (pending|in_progress|completed|cancelled, default pending), id (optional, auto-generated). + :return: JSON with the full task list and summary counts + """ + if __chat_id__ is None: + return json.dumps({'error': 'Chat context not available'}) + + try: + all_tasks = [] + for idx, task in enumerate(tasks): + if hasattr(task, 'model_dump'): + d = task.model_dump(exclude_none=True) + elif isinstance(task, dict): + d = task + else: + d = dict(task) + + content = str(d.get('content', '')).strip() + if not content: + continue + + item_id = str(d.get('id', '') or '').strip() or str(idx + 1) + status = str(d.get('status', 'pending')).strip().lower() + if status not in VALID_TASK_STATUSES: + status = 'pending' + + all_tasks.append({'id': item_id, 'content': content, 'status': status}) + + await Chats.update_chat_tasks_by_id(__chat_id__, all_tasks) + await _emit_tasks(__event_emitter__, all_tasks) + + return json.dumps( + {'tasks': all_tasks, 'summary': _task_summary(all_tasks)}, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'tasks error: {e}') + return json.dumps({'error': str(e)}) + + +async def update_task( + id: str, + status: str = 'completed', + __chat_id__: str = None, + __message_id__: str = None, + __event_emitter__: callable = None, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Mark a single task as completed, in_progress, pending, or cancelled. + Call this after finishing each step. You MUST call this for every + task, including the very last one. + + :param id: The task ID to update + :param status: New status: completed, in_progress, pending, or cancelled (default: completed) + :return: JSON with the updated task list and summary counts + """ + if __chat_id__ is None: + return json.dumps({'error': 'Chat context not available'}) + + try: + status = status.strip().lower() + if status not in VALID_TASK_STATUSES: + return json.dumps( + {'error': f'Invalid status: {status}. Must be one of: {", ".join(sorted(VALID_TASK_STATUSES))}'} + ) + + all_tasks = await Chats.get_chat_tasks_by_id(__chat_id__) + + found = False + for task in all_tasks: + if task['id'] == id: + task['status'] = status + found = True + break + + if not found: + return json.dumps({'error': f'Task with id "{id}" not found'}) + + await Chats.update_chat_tasks_by_id(__chat_id__, all_tasks) + await _emit_tasks(__event_emitter__, all_tasks) + + return json.dumps( + {'tasks': all_tasks, 'summary': _task_summary(all_tasks)}, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'update_task_status error: {e}') + return json.dumps({'error': str(e)}) + + +# ============================================================================= +# AUTOMATION TOOLS +# ============================================================================= + + +async def create_automation( + name: str, + prompt: str, + rrule: str, + __request__: Request = None, + __user__: dict = None, + __metadata__: dict = None, +) -> str: + """ + Create a scheduled automation that runs a prompt on a recurring or one-time schedule. + Use this when the user wants to schedule a task to run automatically. + The automation will use the current chat model. + + The rrule parameter must be a valid iCalendar RRULE string. Common examples: + - Every day at 9am: "DTSTART:20250101T090000\\nRRULE:FREQ=DAILY" + - Every Monday at 8am: "DTSTART:20250106T080000\\nRRULE:FREQ=WEEKLY;BYDAY=MO" + - Every hour: "RRULE:FREQ=HOURLY;INTERVAL=1" + - Every 30 minutes: "RRULE:FREQ=MINUTELY;INTERVAL=30" + - Once at a specific time: "DTSTART:20250415T140000\\nRRULE:FREQ=DAILY;COUNT=1" + - First day of every month: "DTSTART:20250101T090000\\nRRULE:FREQ=MONTHLY;BYMONTHDAY=1" + + The DTSTART time should reflect the desired execution time. Use COUNT=1 for one-time automations. + + :param name: A short descriptive name for the automation + :param prompt: The prompt/instructions to execute on each run + :param rrule: An iCalendar RRULE string defining the schedule + :return: JSON with the created automation details including id, next scheduled runs + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + from open_webui.models.automations import Automations, AutomationForm, AutomationData + from open_webui.models.users import Users + from open_webui.utils.automations import validate_rrule, next_run_ns, next_n_runs_ns + + user_id = __user__.get('id') + user = await Users.get_user_by_id(user_id) + if not user: + return json.dumps({'error': 'User not found'}) + + # Fall back to model dict ID since __metadata__ may predate model_id assignment + metadata = __metadata__ or {} + model_id = metadata.get('model_id') or ( + metadata.get('model', {}).get('id') if isinstance(metadata.get('model'), dict) else None + ) + if not model_id: + return json.dumps({'error': 'Could not detect current model'}) + + # Validate the RRULE + try: + validate_rrule(rrule, tz=user.timezone) + except ValueError as e: + return json.dumps({'error': f'Invalid schedule: {e}'}) + + tz = user.timezone + form = AutomationForm( + name=name, + data=AutomationData( + prompt=prompt, + model_id=model_id, + rrule=rrule, + ), + is_active=True, + ) + + automation = await Automations.insert(user_id, form, next_run_ns(rrule, tz=tz)) + + return json.dumps( + { + 'status': 'success', + 'id': automation.id, + 'name': automation.name, + 'model_id': model_id, + 'is_active': automation.is_active, + 'next_runs': next_n_runs_ns(rrule, tz=tz), + }, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'create_automation error: {e}') + return json.dumps({'error': str(e)}) + + +async def update_automation( + automation_id: str, + name: Optional[str] = None, + prompt: Optional[str] = None, + rrule: Optional[str] = None, + model_id: Optional[str] = None, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Update an existing automation. Only the provided fields are changed; omitted fields stay the same. + + :param automation_id: The ID of the automation to update + :param name: New name for the automation (optional) + :param prompt: New prompt/instructions (optional) + :param rrule: New iCalendar RRULE schedule string (optional). See create_automation for format examples. + :param model_id: New model ID to use (optional) + :return: JSON with the updated automation details + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + from open_webui.models.automations import Automations, AutomationForm, AutomationData + from open_webui.models.users import Users + from open_webui.utils.automations import validate_rrule, next_run_ns, next_n_runs_ns + + user_id = __user__.get('id') + user = await Users.get_user_by_id(user_id) + + automation = await Automations.get_by_id(automation_id) + if not automation: + return json.dumps({'error': 'Automation not found'}) + if automation.user_id != user_id: + return json.dumps({'error': 'Access denied'}) + + # Merge provided fields with existing values + new_name = name if name is not None else automation.name + new_prompt = prompt if prompt is not None else automation.data.get('prompt', '') + new_model_id = model_id if model_id is not None else automation.data.get('model_id', '') + new_rrule = rrule if rrule is not None else automation.data.get('rrule', '') + + # Validate RRULE if changed + if rrule is not None: + try: + validate_rrule(new_rrule, tz=user.timezone if user else None) + except ValueError as e: + return json.dumps({'error': f'Invalid schedule: {e}'}) + + tz = user.timezone if user else None + form = AutomationForm( + name=new_name, + data=AutomationData( + prompt=new_prompt, + model_id=new_model_id, + rrule=new_rrule, + ), + is_active=automation.is_active, + ) + + updated = await Automations.update(automation_id, form, next_run_ns(new_rrule, tz=tz)) + + return json.dumps( + { + 'status': 'success', + 'id': updated.id, + 'name': updated.name, + 'model_id': new_model_id, + 'is_active': updated.is_active, + 'next_runs': next_n_runs_ns(new_rrule, tz=tz), + }, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'update_automation error: {e}') + return json.dumps({'error': str(e)}) + + +async def list_automations( + status: Optional[str] = None, + count: int = 10, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + List the user's scheduled automations. + + :param status: Filter by status: "active", "paused", or omit for all + :param count: Maximum number of automations to return (default: 10) + :return: JSON list of automations with id, name, prompt snippet, schedule, status, and next runs + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + from open_webui.models.automations import Automations + from open_webui.models.users import Users + from open_webui.utils.automations import next_n_runs_ns + + user_id = __user__.get('id') + user = await Users.get_user_by_id(user_id) + + result = await Automations.search_automations( + user_id=user_id, + status=status, + skip=0, + limit=count, + ) + + automations = [] + for item in result.items: + rrule = item.data.get('rrule', '') + prompt_text = item.data.get('prompt', '') + snippet = prompt_text[:100] + ('...' if len(prompt_text) > 100 else '') + + automations.append( + { + 'id': item.id, + 'name': item.name, + 'prompt_snippet': snippet, + 'model_id': item.data.get('model_id', ''), + 'rrule': rrule, + 'is_active': item.is_active, + 'last_run_at': item.last_run_at, + 'next_runs': next_n_runs_ns(rrule, tz=user.timezone if user else None), + } + ) + + return json.dumps( + {'automations': automations, 'total': result.total}, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'list_automations error: {e}') + return json.dumps({'error': str(e)}) + + +async def toggle_automation( + automation_id: str, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Pause or resume a scheduled automation. If active, it will be paused. If paused, it will be resumed. + + :param automation_id: The ID of the automation to toggle + :return: JSON with the updated automation status + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + from open_webui.models.automations import Automations + from open_webui.models.users import Users + from open_webui.utils.automations import next_run_ns + + user_id = __user__.get('id') + user = await Users.get_user_by_id(user_id) + + automation = await Automations.get_by_id(automation_id) + if not automation: + return json.dumps({'error': 'Automation not found'}) + if automation.user_id != user_id: + return json.dumps({'error': 'Access denied'}) + + rrule = automation.data.get('rrule', '') + toggled = await Automations.toggle( + automation_id, + next_run_ns(rrule, tz=user.timezone if user else None), + ) + + return json.dumps( + { + 'status': 'success', + 'id': toggled.id, + 'name': toggled.name, + 'is_active': toggled.is_active, + }, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'toggle_automation error: {e}') + return json.dumps({'error': str(e)}) + + +async def delete_automation( + automation_id: str, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Delete a scheduled automation and all its run history. + + :param automation_id: The ID of the automation to delete + :return: JSON confirming the automation was deleted + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + from open_webui.models.automations import Automations, AutomationRuns + + user_id = __user__.get('id') + + automation = await Automations.get_by_id(automation_id) + if not automation: + return json.dumps({'error': 'Automation not found'}) + if automation.user_id != user_id: + return json.dumps({'error': 'Access denied'}) + + name = automation.name + await AutomationRuns.delete_by_automation(automation_id) + await Automations.delete(automation_id) + + return json.dumps( + { + 'status': 'success', + 'message': f'Automation "{name}" deleted', + }, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'delete_automation error: {e}') + return json.dumps({'error': str(e)}) + + +# ============================================================================= +# CALENDAR TOOLS +# ============================================================================= + + +def _get_user_tz(user_dict: dict): + """Get the user's timezone as a ZoneInfo, falling back to UTC.""" + from zoneinfo import ZoneInfo + + tz_name = None + if user_dict: + tz_name = user_dict.get('timezone') + if tz_name: + try: + return ZoneInfo(tz_name) + except Exception: + pass + return ZoneInfo('UTC') + + +def _dt_to_ns(dt_str: str, tz) -> int: + """Convert a datetime string to nanoseconds since epoch, interpreting in the given timezone.""" + from datetime import datetime + + dt = datetime.fromisoformat(dt_str) + # If naive (no timezone info), localize to user's timezone + if dt.tzinfo is None: + dt = dt.replace(tzinfo=tz) + return int(dt.timestamp() * 1_000) * 1_000_000 + + +def _ns_to_dt(ns: int, tz) -> str: + """Convert nanoseconds since epoch to a datetime string in the given timezone.""" + from datetime import datetime + + seconds = ns / 1_000_000_000 + dt = datetime.fromtimestamp(seconds, tz=tz) + return dt.strftime('%Y-%m-%d %H:%M') + + +def _event_to_dict(event, tz) -> dict: + """Convert a calendar event model to a human-friendly dict with local timestamps.""" + alert_minutes = None + if event.meta and 'alert_minutes' in event.meta: + alert_minutes = event.meta['alert_minutes'] + return { + 'id': event.id, + 'calendar_id': event.calendar_id, + 'title': event.title, + 'description': event.description or '', + 'start': _ns_to_dt(event.start_at, tz), + 'end': _ns_to_dt(event.end_at, tz) if event.end_at else None, + 'all_day': event.all_day, + 'location': event.location or '', + 'reminder_minutes': alert_minutes if alert_minutes is not None else 10, + 'color': event.color, + 'is_cancelled': event.is_cancelled, + } + + +async def search_calendar_events( + query: Optional[str] = None, + start: Optional[str] = None, + end: Optional[str] = None, + count: int = 10, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Search calendar events by text and/or date range. + Returns matching events across all accessible calendars. + + :param query: Search text to match against event title, description, or location (optional) + :param start: Only return events starting at or after this datetime, e.g. "2026-04-20 00:00" (optional) + :param end: Only return events starting before this datetime, e.g. "2026-04-27 00:00" (optional) + :param count: Maximum number of events to return (default: 10) + :return: JSON list of matching events with id, title, description, start, end, calendar_id, location + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + from open_webui.models.calendar import CalendarEvents + + user_id = __user__.get('id') + tz = _get_user_tz(__user__) + + if isinstance(count, str): + try: + count = int(count) + except ValueError: + count = 10 + + if start or end: + # Date range query — use get_events_by_range + try: + start_ns = _dt_to_ns(start, tz) if start else 0 + except (ValueError, TypeError) as e: + return json.dumps({'error': f'Invalid start datetime: {e}'}) + + try: + end_ns = ( + _dt_to_ns(end, tz) + if end + else int(time.time() * 1_000) * 1_000_000 + 365 * 86400 * 1_000_000_000_000 + ) + except (ValueError, TypeError) as e: + return json.dumps({'error': f'Invalid end datetime: {e}'}) + + items = await CalendarEvents.get_events_by_range( + user_id=user_id, + start=start_ns, + end=end_ns, + ) + + # Apply text filter if query is also provided + if query: + q = query.lower() + items = [ + e + for e in items + if q in (e.title or '').lower() + or q in (e.description or '').lower() + or q in (e.location or '').lower() + ] + + events = [_event_to_dict(item, tz) for item in items[:count]] + return json.dumps( + {'events': events, 'total': len(items)}, + ensure_ascii=False, + ) + else: + # Text-only search + result = await CalendarEvents.search_events( + user_id=user_id, + query=query, + skip=0, + limit=count, + ) + + events = [_event_to_dict(item, tz) for item in result.items] + return json.dumps( + {'events': events, 'total': result.total}, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'search_calendar_events error: {e}') + return json.dumps({'error': str(e)}) + + +async def create_calendar_event( + title: str, + start: str, + end: Optional[str] = None, + description: Optional[str] = None, + calendar_id: Optional[str] = None, + all_day: bool = False, + location: Optional[str] = None, + reminder_minutes: Optional[int] = None, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Create a new calendar event. If no calendar_id is provided, the event is + added to the user's default calendar. + + :param title: Event title + :param start: Start datetime string in your local time (e.g. "2026-04-20 09:00" or "2026-04-20T09:00:00") + :param end: End datetime string in your local time (optional, omit for point-in-time events) + :param description: Event description (optional) + :param calendar_id: Target calendar ID (optional, uses default calendar if omitted) + :param all_day: Whether this is an all-day event (default: false) + :param location: Event location (optional) + :param reminder_minutes: Minutes before the event to send a reminder notification (optional, default: 10). Use 0 for "at time of event", -1 for no reminder. Accepts any positive integer for custom timing (e.g. 120 for 2 hours before). + :return: JSON with the created event details including id + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + from open_webui.models.calendar import Calendars, CalendarEvents, CalendarEventForm + + user_id = __user__.get('id') + + # Resolve calendar_id: use provided, or fall back to default + if not calendar_id: + calendars = await Calendars.get_calendars_by_user(user_id) + default_cal = next((c for c in calendars if c.is_default), None) + if not default_cal and calendars: + default_cal = calendars[0] + if not default_cal: + return json.dumps({'error': 'No calendars found. Cannot create event.'}) + calendar_id = default_cal.id + + # Verify access + cal = await Calendars.get_calendar_by_id(calendar_id) + if not cal: + return json.dumps({'error': 'Calendar not found'}) + if cal.user_id != user_id and __user__.get('role') != 'admin': + from open_webui.models.access_grants import AccessGrants + from open_webui.models.groups import Groups + + user_group_ids = [g.id for g in await Groups.get_groups_by_member_id(user_id)] + if not await AccessGrants.has_access( + user_id=user_id, + resource_type='calendar', + resource_id=cal.id, + permission='write', + user_group_ids=set(user_group_ids), + ): + return json.dumps({'error': 'Access denied to this calendar'}) + + # Coerce boolean from LLM + if isinstance(all_day, str): + all_day = all_day.lower() in ('true', '1', 'yes') + + # Convert datetime strings to nanoseconds using user's timezone + tz = _get_user_tz(__user__) + try: + start_ns = _dt_to_ns(start, tz) + except (ValueError, TypeError) as e: + return json.dumps({'error': f'Invalid start datetime: {e}. Use format like "2026-04-20 09:00"'}) + + end_ns = None + if end: + try: + end_ns = _dt_to_ns(end, tz) + except (ValueError, TypeError) as e: + return json.dumps({'error': f'Invalid end datetime: {e}. Use format like "2026-04-20 10:00"'}) + elif not all_day: + # Default to 1 hour duration + end_ns = start_ns + 3_600_000_000_000 + + # Build meta with reminder setting + meta = {} + if reminder_minutes is not None: + if isinstance(reminder_minutes, str): + try: + reminder_minutes = int(reminder_minutes) + except ValueError: + reminder_minutes = 10 + meta['alert_minutes'] = reminder_minutes + else: + meta['alert_minutes'] = 10 + + form = CalendarEventForm( + calendar_id=calendar_id, + title=title, + description=description, + start_at=start_ns, + end_at=end_ns, + all_day=all_day, + location=location, + meta=meta, + ) + + event = await CalendarEvents.insert_new_event(user_id, form) + if not event: + return json.dumps({'error': 'Failed to create event'}) + + return json.dumps( + { + 'status': 'success', + **_event_to_dict(event, tz), + }, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'create_calendar_event error: {e}') + return json.dumps({'error': str(e)}) + + +async def update_calendar_event( + event_id: str, + title: Optional[str] = None, + description: Optional[str] = None, + start: Optional[str] = None, + end: Optional[str] = None, + all_day: Optional[bool] = None, + location: Optional[str] = None, + is_cancelled: Optional[bool] = None, + reminder_minutes: Optional[int] = None, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Update an existing calendar event. Only provided fields are changed; + omitted fields stay the same. + + :param event_id: The ID of the event to update + :param title: New event title (optional) + :param description: New event description (optional) + :param start: New start datetime string in your local time, e.g. "2026-04-20 09:00" (optional) + :param end: New end datetime string in your local time (optional) + :param all_day: Whether this is an all-day event (optional) + :param location: New event location (optional) + :param is_cancelled: Set to true to cancel the event (optional) + :param reminder_minutes: Minutes before the event to send a reminder notification (optional). Use 0 for "at time of event", -1 for no reminder. Accepts any positive integer for custom timing (e.g. 120 for 2 hours before). + :return: JSON with the updated event details + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + from open_webui.models.calendar import Calendars, CalendarEvents, CalendarEventUpdateForm + from open_webui.models.access_grants import AccessGrants + from open_webui.models.groups import Groups + + user_id = __user__.get('id') + + event = await CalendarEvents.get_event_by_id(event_id) + if not event: + return json.dumps({'error': 'Event not found'}) + + # Check write access to the event's calendar + cal = await Calendars.get_calendar_by_id(event.calendar_id) + if cal and cal.user_id != user_id and __user__.get('role') != 'admin': + user_group_ids = [g.id for g in await Groups.get_groups_by_member_id(user_id)] + if not await AccessGrants.has_access( + user_id=user_id, + resource_type='calendar', + resource_id=cal.id, + permission='write', + user_group_ids=set(user_group_ids), + ): + return json.dumps({'error': 'Access denied'}) + + # Coerce boolean strings from LLM + if isinstance(all_day, str): + all_day = all_day.lower() in ('true', '1', 'yes') + if isinstance(is_cancelled, str): + is_cancelled = is_cancelled.lower() in ('true', '1', 'yes') + + # Convert datetime strings to nanoseconds using user's timezone + tz = _get_user_tz(__user__) + start_ns = None + if start is not None: + try: + start_ns = _dt_to_ns(start, tz) + except (ValueError, TypeError) as e: + return json.dumps({'error': f'Invalid start datetime: {e}'}) + + end_ns = None + if end is not None: + try: + end_ns = _dt_to_ns(end, tz) + except (ValueError, TypeError) as e: + return json.dumps({'error': f'Invalid end datetime: {e}'}) + + # Build meta update with reminder setting if provided + meta = None + if reminder_minutes is not None: + if isinstance(reminder_minutes, str): + try: + reminder_minutes = int(reminder_minutes) + except ValueError: + reminder_minutes = None + if reminder_minutes is not None: + meta = {'alert_minutes': reminder_minutes} + + form = CalendarEventUpdateForm( + title=title, + description=description, + start_at=start_ns, + end_at=end_ns, + all_day=all_day, + location=location, + is_cancelled=is_cancelled, + meta=meta, + ) + + updated = await CalendarEvents.update_event_by_id(event_id, form) + if not updated: + return json.dumps({'error': 'Failed to update event'}) + + return json.dumps( + { + 'status': 'success', + **_event_to_dict(updated, tz), + }, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'update_calendar_event error: {e}') + return json.dumps({'error': str(e)}) + + +async def delete_calendar_event( + event_id: str, + __request__: Request = None, + __user__: dict = None, +) -> str: + """ + Delete a calendar event permanently. + + :param event_id: The ID of the event to delete + :return: JSON confirming the event was deleted + """ + if __request__ is None: + return json.dumps({'error': 'Request context not available'}) + + if not __user__: + return json.dumps({'error': 'User context not available'}) + + try: + from open_webui.models.calendar import Calendars, CalendarEvents + from open_webui.models.access_grants import AccessGrants + from open_webui.models.groups import Groups + + user_id = __user__.get('id') + + event = await CalendarEvents.get_event_by_id(event_id) + if not event: + return json.dumps({'error': 'Event not found'}) + + # Check write access + cal = await Calendars.get_calendar_by_id(event.calendar_id) + if cal and cal.user_id != user_id and __user__.get('role') != 'admin': + user_group_ids = [g.id for g in await Groups.get_groups_by_member_id(user_id)] + if not await AccessGrants.has_access( + user_id=user_id, + resource_type='calendar', + resource_id=cal.id, + permission='write', + user_group_ids=set(user_group_ids), + ): + return json.dumps({'error': 'Access denied'}) + + title = event.title + result = await CalendarEvents.delete_event_by_id(event_id) + if not result: + return json.dumps({'error': 'Failed to delete event'}) + + return json.dumps( + { + 'status': 'success', + 'message': f'Event "{title}" deleted', + }, + ensure_ascii=False, + ) + except Exception as e: + log.exception(f'delete_calendar_event error: {e}') + return json.dumps({'error': str(e)}) diff --git a/backend/open_webui/utils/access_control/__init__.py b/backend/open_webui/utils/access_control/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..06e8d0aba04d41704e26df40e981ffccaab52985 --- /dev/null +++ b/backend/open_webui/utils/access_control/__init__.py @@ -0,0 +1,346 @@ +import json +from typing import Any + +from open_webui.models.users import UserModel +from open_webui.models.groups import Groups +from open_webui.models.access_grants import ( + has_public_read_access_grant, + has_public_write_access_grant, + has_user_access_grant, + strip_user_access_grants, +) +from open_webui.config import DEFAULT_USER_PERMISSIONS + +from sqlalchemy.ext.asyncio import AsyncSession + + +def fill_missing_permissions(permissions: dict[str, Any], default_permissions: dict[str, Any]) -> dict[str, Any]: + """ + Recursively fills in missing properties in the permissions dictionary + using the default permissions as a template. + """ + for key, value in default_permissions.items(): + if key not in permissions: + permissions[key] = value + elif isinstance(value, dict) and isinstance(permissions[key], dict): # Both are nested dictionaries + permissions[key] = fill_missing_permissions(permissions[key], value) + + return permissions + + +async def get_permissions( + user_id: str, + default_permissions: dict[str, Any], + db: AsyncSession | None = None, +) -> dict[str, Any]: + """ + Get all permissions for a user by combining the permissions of all groups the user is a member of. + If a permission is defined in multiple groups, the most permissive value is used (True > False). + Permissions are nested in a dict with the permission key as the key and a boolean as the value. + """ + + def combine_permissions(permissions: dict[str, Any], group_permissions: dict[str, Any]) -> dict[str, Any]: + """Combine permissions from multiple groups by taking the most permissive value.""" + for key, value in group_permissions.items(): + if isinstance(value, dict): + if key not in permissions: + permissions[key] = {} + permissions[key] = combine_permissions(permissions[key], value) + else: + if key not in permissions: + permissions[key] = value + else: + permissions[key] = permissions[key] or value # Use the most permissive value (True > False) + return permissions + + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + + # Deep copy default permissions to avoid modifying the original dict + permissions = json.loads(json.dumps(default_permissions)) + + # Combine permissions from all user groups + for group in user_groups: + permissions = combine_permissions(permissions, group.permissions or {}) + + # Ensure all fields from default_permissions are present and filled in + permissions = fill_missing_permissions(permissions, default_permissions) + + return permissions + + +async def has_permission( + user_id: str, + permission_key: str, + default_permissions: dict[str, Any] = {}, + db: AsyncSession | None = None, +) -> bool: + """ + Check if a user has a specific permission by checking the group permissions + and fall back to default permissions if not found in any group. + + Permission keys can be hierarchical and separated by dots ('.'). + """ + + def get_permission(permissions: dict[str, Any], keys: list[str]) -> bool: + """Traverse permissions dict using a list of keys (from dot-split permission_key).""" + for key in keys: + if key not in permissions: + return False # If any part of the hierarchy is missing, deny access + permissions = permissions[key] # Traverse one level deeper + + return bool(permissions) # Return the boolean at the final level + + permission_hierarchy = permission_key.split('.') + + # Retrieve user group permissions + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + + for group in user_groups: + if get_permission(group.permissions or {}, permission_hierarchy): + return True + + # Check default permissions afterward if the group permissions don't allow it + default_permissions = fill_missing_permissions(default_permissions, DEFAULT_USER_PERMISSIONS) + return get_permission(default_permissions, permission_hierarchy) + + +async def has_access( + user_id: str, + permission: str = 'read', + access_grants: list | None = None, + user_group_ids: set[str] | None = None, + db: AsyncSession | None = None, +) -> bool: + """ + Check if a user has the specified permission using an in-memory access_grants list. + + Used for config-driven resources (arena models, tool servers) that store + access control as JSON in PersistentConfig rather than in the access_grant DB table. + + Semantics: + - None or [] → private (owner-only, deny all) + - [{"principal_type": "user", "principal_id": "*", "permission": "read"}] → public read + - Specific grants → check user/group membership + """ + if not access_grants: + return False + + if user_group_ids is None: + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} + + for grant in access_grants: + if not isinstance(grant, dict): + continue + if grant.get('permission') != permission: + continue + principal_type = grant.get('principal_type') + principal_id = grant.get('principal_id') + if principal_type == 'user' and (principal_id == '*' or principal_id == user_id): + return True + if principal_type == 'group' and user_group_ids and principal_id in user_group_ids: + return True + + return False + + +async def has_connection_access( + user: UserModel, + connection: dict, + user_group_ids: set[str] | None = None, +) -> bool: + """ + Check if a user can access a server connection (tool server, terminal, etc.) + based on ``config.access_grants`` within the connection dict. + + - Admin with BYPASS_ADMIN_ACCESS_CONTROL → always allowed + - Missing, None, or empty access_grants → private, admin-only + - access_grants has entries → delegates to ``has_access`` + """ + from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL + + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: + return True + + if user_group_ids is None: + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id)} + + access_grants = (connection.get('config') or {}).get('access_grants', []) + return await has_access(user.id, 'read', access_grants, user_group_ids) + + +def migrate_access_control(data: dict, ac_key: str = 'access_control', grants_key: str = 'access_grants') -> None: + """ + Auto-migrate a config dict in-place from legacy access_control dict to access_grants list. + + If `grants_key` already exists, does nothing. + If `ac_key` exists (old format), converts it and stores as `grants_key`, then removes `ac_key`. + """ + if grants_key in data: + return + + access_control = data.get(ac_key) + if access_control is None and ac_key not in data: + return + + grants: list[dict[str, str]] = [] + if access_control and isinstance(access_control, dict): + for perm in ['read', 'write']: + perm_data = access_control.get(perm, {}) + if not perm_data: + continue + for group_id in perm_data.get('group_ids', []): + grants.append( + { + 'principal_type': 'group', + 'principal_id': group_id, + 'permission': perm, + } + ) + for uid in perm_data.get('user_ids', []): + grants.append( + { + 'principal_type': 'user', + 'principal_id': uid, + 'permission': perm, + } + ) + + data[grants_key] = grants + data.pop(ac_key, None) + + +async def filter_allowed_access_grants( + default_permissions: dict[str, Any], + user_id: str, + user_role: str, + access_grants: list, + public_permission_key: str, + db: AsyncSession | None = None, +) -> list: + """ + Checks if the user has the required permissions to grant access to a resource. + Returns the filtered list of access grants if permissions are missing. + """ + if user_role == 'admin' or not access_grants: + return access_grants + + # Check if user can share publicly + if ( + has_public_read_access_grant(access_grants) or has_public_write_access_grant(access_grants) + ) and not await has_permission( + user_id, + public_permission_key, + default_permissions, + db=db, + ): + access_grants = [ + grant + for grant in access_grants + if not ( + (grant.get('principal_type') if isinstance(grant, dict) else getattr(grant, 'principal_type', None)) + == 'user' + and (grant.get('principal_id') if isinstance(grant, dict) else getattr(grant, 'principal_id', None)) + == '*' + ) + ] + + # Strip individual user sharing if user lacks permission + if has_user_access_grant(access_grants) and not await has_permission( + user_id, + 'access_grants.allow_users', + default_permissions, + db=db, + ): + access_grants = strip_user_access_grants(access_grants) + + return access_grants + + +async def has_base_model_access( + user_id: str, + model_info, + *, + user_group_ids: set[str] | None = None, + db=None, +) -> bool: + """ + Walk the ``base_model_id`` chain and verify the caller has read access + at every hop. + + Returns ``True`` when access is granted (or the chain ends at a raw + provider model that has no per-model ACL). Returns ``False`` the + moment a registered base model denies access. + """ + from open_webui.models.models import Models + from open_webui.models.access_grants import AccessGrants + + base_model_id = getattr(model_info, 'base_model_id', None) + seen = {model_info.id} + while base_model_id and base_model_id not in seen: + seen.add(base_model_id) + base_model_info = await Models.get_model_by_id(base_model_id, db=db) + if base_model_info is None: + break # Raw provider model — no per-model ACL + if not ( + user_id == base_model_info.user_id + or await AccessGrants.has_access( + user_id=user_id, + resource_type='model', + resource_id=base_model_info.id, + permission='read', + user_group_ids=user_group_ids, + db=db, + ) + ): + return False + base_model_id = getattr(base_model_info, 'base_model_id', None) + return True + + +async def check_model_access( + user: UserModel, + model_info, + bypass_filter: bool = False, +) -> None: + """ + Enforce per-model read access for the given user. + + Raises HTTPException(403) if the user is not authorized. + Does nothing if bypass_filter is True. + + Args: + user: The authenticated user. + model_info: The model record from await Models.get_model_by_id(), + or None if the model is not registered. + bypass_filter: If True, skip all access checks (used by + internal callers and BYPASS_MODEL_ACCESS_CONTROL). + """ + from fastapi import HTTPException + + if bypass_filter: + return + + if model_info: + if user.role == 'user': + from open_webui.models.access_grants import AccessGrants + + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id)} + if not ( + user.id == model_info.user_id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='model', + resource_id=model_info.id, + permission='read', + user_group_ids=user_group_ids, + ) + ): + raise HTTPException(status_code=403, detail='Model not found') + + # Enforce access on chained base models + if not await has_base_model_access(user.id, model_info, user_group_ids=user_group_ids): + raise HTTPException(status_code=403, detail='Model not found') + else: + if user.role != 'admin': + raise HTTPException(status_code=403, detail='Model not found') diff --git a/backend/open_webui/utils/access_control/files.py b/backend/open_webui/utils/access_control/files.py new file mode 100644 index 0000000000000000000000000000000000000000..a48dfeb0f1096fac6b8ed55e258d65578f3a4222 --- /dev/null +++ b/backend/open_webui/utils/access_control/files.py @@ -0,0 +1,89 @@ +import logging + +from open_webui.models.users import UserModel +from open_webui.models.files import Files +from open_webui.models.knowledge import Knowledges +from open_webui.models.channels import Channels +from open_webui.models.chats import Chats +from open_webui.models.groups import Groups +from open_webui.models.models import Models +from open_webui.models.access_grants import AccessGrants + +from sqlalchemy.ext.asyncio import AsyncSession + +log = logging.getLogger(__name__) + + +async def has_access_to_file( + file_id: str | None, + access_type: str, + user: UserModel, + db: AsyncSession | None = None, +) -> bool: + """ + Check if a user has the specified access to a file through any of: + - Knowledge bases (ownership or access grants) + - Shared workspace models that attach the file directly + - Channels the user is a member of + - Shared chats + + NOTE: This does NOT check direct file ownership — callers should check + file.user_id == user.id separately before calling this. + """ + file = await Files.get_file_by_id(file_id, db=db) + log.debug(f'Checking if user has {access_type} access to file') + if not file: + return False + + # Direct ownership + if file.user_id == user.id: + return True + + # Check if the file is associated with any knowledge bases the user has access to + knowledge_bases = await Knowledges.get_knowledges_by_file_id(file_id, db=db) + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} + for knowledge_base in knowledge_bases: + if knowledge_base.user_id == user.id or await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge_base.id, + permission=access_type, + user_group_ids=user_group_ids, + db=db, + ): + return True + + knowledge_base_id = file.meta.get('collection_name') if file.meta else None + if knowledge_base_id: + knowledge_bases = await Knowledges.get_knowledge_bases_by_user_id(user.id, access_type, db=db) + for knowledge_base in knowledge_bases: + if knowledge_base.id == knowledge_base_id: + return True + + # Check if the file is associated with any channels the user has access to + channels = await Channels.get_channels_by_file_id_and_user_id(file_id, user.id, db=db) + if access_type == 'read' and channels: + return True + + # Check if the file is associated with any chats the user has access to + shared_chat_ids = await Chats.get_shared_chat_ids_by_file_id(file_id, db=db) + if shared_chat_ids: + accessible_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type='shared_chat', + resource_ids=shared_chat_ids, + permission='read', + user_group_ids=user_group_ids, + db=db, + ) + if accessible_ids: + return True + + # Check if the file is directly attached to a shared workspace model + for model in await Models.get_models_by_user_id(user.id, permission=access_type, db=db): + knowledge_items = getattr(model.meta, 'knowledge', None) or [] + for item in knowledge_items: + if isinstance(item, dict) and item.get('type') == 'file' and item.get('id') == file.id: + return True + + return False diff --git a/backend/open_webui/utils/actions.py b/backend/open_webui/utils/actions.py new file mode 100644 index 0000000000000000000000000000000000000000..7b1789580b0aabaac8a1a2bfec134d3e2f0cf5c5 --- /dev/null +++ b/backend/open_webui/utils/actions.py @@ -0,0 +1,137 @@ +import logging +import sys +import inspect + +from typing import Any + +from fastapi import Request + +from open_webui.models.users import UserModel +from open_webui.models.functions import Functions + +from open_webui.socket.main import get_event_call, get_event_emitter +from open_webui.utils.plugin import get_function_module_from_cache +from open_webui.utils.models import get_all_models +from open_webui.utils.middleware import process_tool_result + +from open_webui.env import GLOBAL_LOG_LEVEL + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) +log = logging.getLogger(__name__) + + +async def chat_action(request: Request, action_id: str, form_data: dict, user: Any): + if '.' in action_id: + action_id, sub_action_id = action_id.split('.') + else: + sub_action_id = None + + action = await Functions.get_function_by_id(action_id) + if not action: + raise Exception(f'Action not found: {action_id}') + + if not request.app.state.MODELS: + await get_all_models(request, user=user) + + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): + models = { + request.state.model['id']: request.state.model, + } + else: + models = request.app.state.MODELS + + data = form_data + model_id = data['model'] + + if model_id not in models: + raise Exception('Model not found') + model = models[model_id] + + __event_emitter__ = await get_event_emitter( + { + 'chat_id': data['chat_id'], + 'message_id': data['id'], + 'session_id': data['session_id'], + 'user_id': user.id, + } + ) + __event_call__ = await get_event_call( + { + 'chat_id': data['chat_id'], + 'message_id': data['id'], + 'session_id': data['session_id'], + 'user_id': user.id, + } + ) + + function_module, _, _ = await get_function_module_from_cache(request, action_id) + + if hasattr(function_module, 'valves') and hasattr(function_module, 'Valves'): + valves = await Functions.get_function_valves_by_id(action_id) + function_module.valves = function_module.Valves(**(valves if valves else {})) + + if hasattr(function_module, 'action'): + try: + action = function_module.action + + # Get the signature of the function + sig = inspect.signature(action) + params = {'body': data} + + # Extra parameters to be passed to the function + extra_params = { + '__model__': model, + '__id__': sub_action_id if sub_action_id is not None else action_id, + '__event_emitter__': __event_emitter__, + '__event_call__': __event_call__, + '__request__': request, + } + + # Add extra params in contained in function signature + for key, value in extra_params.items(): + if key in sig.parameters: + params[key] = value + + if '__user__' in sig.parameters: + __user__ = user.model_dump() if isinstance(user, UserModel) else {} + + try: + if hasattr(function_module, 'UserValves'): + __user__['valves'] = function_module.UserValves( + **await Functions.get_user_valves_by_id_and_user_id(action_id, user.id) + ) + except Exception as e: + log.exception(f'Failed to get user values: {e}') + + params = {**params, '__user__': __user__} + + if inspect.iscoroutinefunction(action): + data = await action(**params) + else: + data = action(**params) + + # Process action result for Rich UI embeds (HTMLResponse, tuple with headers) + processed_result, _, action_embeds = await process_tool_result( + request, + action_id, + data, + 'action', + ) + + if action_embeds: + await __event_emitter__( + { + 'type': 'embeds', + 'data': { + 'embeds': action_embeds, + }, + } + ) + # Replace data with the processed status dict so we don't + # try to serialize the raw HTMLResponse / tuple back to the client + data = processed_result + + except Exception as e: + raise Exception(f'Error: {e}') + + return data diff --git a/backend/open_webui/utils/anthropic.py b/backend/open_webui/utils/anthropic.py new file mode 100644 index 0000000000000000000000000000000000000000..a01184143f975b2b1b85b585ac494e2e4aa8ce35 --- /dev/null +++ b/backend/open_webui/utils/anthropic.py @@ -0,0 +1,608 @@ +import json +import logging + +import aiohttp + +from open_webui.env import ( + AIOHTTP_CLIENT_SESSION_SSL, + AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST, + ENABLE_FORWARD_USER_INFO_HEADERS, +) +from open_webui.models.users import UserModel +from open_webui.utils.headers import include_user_info_headers + +log = logging.getLogger(__name__) + + +def is_anthropic_url(url: str) -> bool: + """Check if the URL is an Anthropic API endpoint.""" + return 'api.anthropic.com' in url + + +async def get_anthropic_models(url: str, key: str, user: UserModel = None) -> dict: + """ + Fetch models from Anthropic's /v1/models endpoint with pagination. + Normalizes the response to OpenAI format. + """ + timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) + all_models = [] + after_id = None + + try: + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + headers = { + 'x-api-key': key, + 'anthropic-version': '2023-06-01', + } + + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) + + while True: + params = {'limit': 1000} + if after_id: + params['after_id'] = after_id + + async with session.get( + f'{url}/models', + headers=headers, + params=params, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + if response.status != 200: + error_detail = f'HTTP Error: {response.status}' + try: + res = await response.json() + if 'error' in res: + error_detail = f'External Error: {res["error"]}' + except Exception: + pass + return {'object': 'list', 'data': [], 'error': error_detail} + + data = await response.json() + + for model in data.get('data', []): + all_models.append( + { + 'id': model.get('id'), + 'object': 'model', + 'created': 0, + 'owned_by': 'anthropic', + 'name': model.get('display_name', model.get('id')), + } + ) + + if not data.get('has_more', False): + break + after_id = data.get('last_id') + + except Exception as e: + log.error(f'Anthropic connection error: {e}') + return None + + return {'object': 'list', 'data': all_models} + + +############################## +# +# Anthropic Messages API Conversion Utilities +# +############################## + + +def convert_anthropic_to_openai_payload(anthropic_payload: dict) -> dict: + """ + Convert an Anthropic Messages API request to OpenAI Chat Completions format. + + Anthropic format: + {model, messages: [{role, content}], system, max_tokens, ...} + OpenAI format: + {model, messages: [{role, content}], max_tokens, ...} + """ + openai_payload = {} + + # Model + openai_payload['model'] = anthropic_payload.get('model', '') + + # Build messages list + messages = [] + + # System prompt (Anthropic has it as top-level, OpenAI as a system message) + system = anthropic_payload.get('system') + if system: + if isinstance(system, str): + messages.append({'role': 'system', 'content': system}) + elif isinstance(system, list): + # Anthropic supports system as list of content blocks + text_parts = [] + for block in system: + if isinstance(block, dict) and block.get('type') == 'text': + text_parts.append(block.get('text', '')) + elif isinstance(block, str): + text_parts.append(block) + messages.append({'role': 'system', 'content': '\n'.join(text_parts)}) + + # Convert messages + for msg in anthropic_payload.get('messages', []): + role = msg.get('role', 'user') + content = msg.get('content') + + if isinstance(content, str): + messages.append({'role': role, 'content': content}) + elif isinstance(content, list): + # Convert Anthropic content blocks to OpenAI format + openai_content = [] + tool_calls = [] + + for block in content: + block_type = block.get('type', 'text') + + if block_type == 'text': + openai_content.append( + { + 'type': 'text', + 'text': block.get('text', ''), + } + ) + elif block_type == 'image': + source = block.get('source', {}) + if source.get('type') == 'base64': + media_type = source.get('media_type', 'image/png') + data = source.get('data', '') + openai_content.append( + { + 'type': 'image_url', + 'image_url': { + 'url': f'data:{media_type};base64,{data}', + }, + } + ) + elif source.get('type') == 'url': + openai_content.append( + { + 'type': 'image_url', + 'image_url': {'url': source.get('url', '')}, + } + ) + elif block_type == 'tool_use': + tool_calls.append( + { + 'id': block.get('id', ''), + 'type': 'function', + 'function': { + 'name': block.get('name', ''), + 'arguments': ( + json.dumps(block.get('input', {})) + if isinstance(block.get('input'), dict) + else str(block.get('input', '{}')) + ), + }, + } + ) + elif block_type == 'tool_result': + # Tool results become separate tool messages in OpenAI format + tool_result_content = block.get('content', '') + tool_content: str | list = '' + + if isinstance(tool_result_content, str): + tool_content = tool_result_content + elif isinstance(tool_result_content, list): + # Build a multimodal content array to preserve + # images and other non-text content types. + converted_parts = [] + for content_block in tool_result_content: + if not isinstance(content_block, dict): + continue + content_type = content_block.get('type', 'text') + + if content_type == 'text': + converted_parts.append( + { + 'type': 'text', + 'text': content_block.get('text', ''), + } + ) + elif content_type == 'image': + source = content_block.get('source', {}) + if source.get('type') == 'base64': + media_type = source.get('media_type', 'image/png') + data = source.get('data', '') + converted_parts.append( + { + 'type': 'image_url', + 'image_url': { + 'url': f'data:{media_type};base64,{data}', + }, + } + ) + elif source.get('type') == 'url': + converted_parts.append( + { + 'type': 'image_url', + 'image_url': { + 'url': source.get('url', ''), + }, + } + ) + elif content_type == 'document': + # Documents have no direct OpenAI equivalent; + # convert to a text representation. + document_source = content_block.get('source', {}) + document_title = content_block.get('title', 'Document') + document_context = content_block.get('context', '') + document_text = f'[Document: {document_title}]' + if document_context: + document_text += f'\n{document_context}' + if document_source.get('type') == 'text' and document_source.get('data'): + document_text += f'\n{document_source["data"]}' + converted_parts.append({'type': 'text', 'text': document_text}) + elif content_type == 'search_result': + # Convert search results to a text + # representation with source attribution. + search_title = content_block.get('title', '') + search_url = content_block.get('source', '') + search_content_blocks = content_block.get('content', []) + search_texts = [] + for search_block in search_content_blocks: + if isinstance(search_block, dict) and search_block.get('type') == 'text': + search_texts.append(search_block.get('text', '')) + search_body = '\n'.join(search_texts) + search_text = f'[Search Result: {search_title}]' + if search_url: + search_text += f'\nSource: {search_url}' + if search_body: + search_text += f'\n{search_body}' + converted_parts.append({'type': 'text', 'text': search_text}) + + # Flatten to string when only text parts are present + if all(part.get('type') == 'text' for part in converted_parts): + tool_content = '\n'.join(part.get('text', '') for part in converted_parts) + elif converted_parts: + tool_content = converted_parts + else: + tool_content = '' + + # Propagate error status if present + if block.get('is_error'): + if isinstance(tool_content, str): + tool_content = f'Error: {tool_content}' + elif isinstance(tool_content, list): + tool_content.insert( + 0, + { + 'type': 'text', + 'text': 'Error: ', + }, + ) + + messages.append( + { + 'role': 'tool', + 'tool_call_id': block.get('tool_use_id', ''), + 'content': tool_content, + } + ) + + # Build the message + if tool_calls: + # Assistant message with tool calls + msg_dict = {'role': role} + if openai_content: + # If there's only text, flatten it + if len(openai_content) == 1 and openai_content[0]['type'] == 'text': + msg_dict['content'] = openai_content[0]['text'] + else: + msg_dict['content'] = openai_content + else: + msg_dict['content'] = '' + msg_dict['tool_calls'] = tool_calls + messages.append(msg_dict) + elif openai_content: + # If there's only a single text block, flatten it to a string + if len(openai_content) == 1 and openai_content[0]['type'] == 'text': + messages.append({'role': role, 'content': openai_content[0]['text']}) + else: + messages.append({'role': role, 'content': openai_content}) + else: + messages.append({'role': role, 'content': str(content) if content else ''}) + + openai_payload['messages'] = messages + + # max_tokens + if 'max_tokens' in anthropic_payload: + openai_payload['max_tokens'] = anthropic_payload['max_tokens'] + + # Common parameters + for param in ('temperature', 'top_p', 'stop_sequences', 'stream'): + if param in anthropic_payload: + if param == 'stop_sequences': + openai_payload['stop'] = anthropic_payload[param] + else: + openai_payload[param] = anthropic_payload[param] + + # Tools conversion: Anthropic → OpenAI + if 'tools' in anthropic_payload: + openai_tools = [] + for tool in anthropic_payload['tools']: + openai_tools.append( + { + 'type': 'function', + 'function': { + 'name': tool.get('name', ''), + 'description': tool.get('description', ''), + 'parameters': tool.get('input_schema', {}), + }, + } + ) + openai_payload['tools'] = openai_tools + + # tool_choice + if 'tool_choice' in anthropic_payload: + tc = anthropic_payload['tool_choice'] + if isinstance(tc, dict): + tc_type = tc.get('type', 'auto') + if tc_type == 'auto': + openai_payload['tool_choice'] = 'auto' + elif tc_type == 'any': + openai_payload['tool_choice'] = 'required' + elif tc_type == 'tool': + openai_payload['tool_choice'] = { + 'type': 'function', + 'function': {'name': tc.get('name', '')}, + } + + return openai_payload + + +def convert_openai_to_anthropic_response(openai_response: dict, model: str = '') -> dict: + """ + Convert a non-streaming OpenAI Chat Completions response to Anthropic Messages format. + """ + import uuid as _uuid + + choice = {} + if openai_response.get('choices'): + choice = openai_response['choices'][0] + + message = choice.get('message', {}) + finish_reason = choice.get('finish_reason', 'stop') + + # Map finish_reason to stop_reason + stop_reason_map = { + 'stop': 'end_turn', + 'length': 'max_tokens', + 'tool_calls': 'tool_use', + 'content_filter': 'end_turn', + } + stop_reason = stop_reason_map.get(finish_reason, 'end_turn') + + # Build content blocks + content = [] + msg_content = message.get('content') + if msg_content: + content.append({'type': 'text', 'text': msg_content}) + + # Tool calls → tool_use blocks + tool_calls = message.get('tool_calls', []) + for tc in tool_calls: + func = tc.get('function', {}) + try: + tool_input = json.loads(func.get('arguments', '{}')) + except (json.JSONDecodeError, TypeError): + tool_input = {} + content.append( + { + 'type': 'tool_use', + 'id': tc.get('id', f'toolu_{_uuid.uuid4().hex[:24]}'), + 'name': func.get('name', ''), + 'input': tool_input, + } + ) + + # Usage + openai_usage = openai_response.get('usage', {}) + usage = { + 'input_tokens': openai_usage.get('prompt_tokens', 0), + 'output_tokens': openai_usage.get('completion_tokens', 0), + } + + return { + 'id': openai_response.get('id', f'msg_{_uuid.uuid4().hex[:24]}'), + 'type': 'message', + 'role': 'assistant', + 'content': content, + 'model': model or openai_response.get('model', ''), + 'stop_reason': stop_reason, + 'stop_sequence': None, + 'usage': usage, + } + + +async def openai_stream_to_anthropic_stream(openai_stream_generator, model: str = ''): + """ + Convert an OpenAI SSE streaming response to Anthropic Messages SSE format. + + OpenAI sends: data: {"choices": [{"delta": {"content": "..."}}]} + Anthropic sends: event: content_block_delta\\ndata: {"type": "content_block_delta", ...} + + Handles text content, tool calls, and mixed content with proper + multi-block indexing as required by Anthropic's streaming protocol. + """ + import uuid as _uuid + + msg_id = f'msg_{_uuid.uuid4().hex[:24]}' + input_tokens = 0 + output_tokens = 0 + stop_reason = 'end_turn' + + # Track content blocks with a running index. + # Each text block or tool_use block gets its own index. + current_block_index = 0 + text_block_open = False + + # Track tool call state: maps OpenAI tool_call index -> Anthropic block index + # This allows handling multiple concurrent tool calls. + tool_call_blocks = {} # {openai_tc_index: anthropic_block_index} + tool_call_started = {} # {openai_tc_index: bool} + + # Emit message_start + message_start = { + 'type': 'message_start', + 'message': { + 'id': msg_id, + 'type': 'message', + 'role': 'assistant', + 'content': [], + 'model': model, + 'stop_reason': None, + 'stop_sequence': None, + 'usage': {'input_tokens': 0, 'output_tokens': 0}, + }, + } + yield f'event: message_start\ndata: {json.dumps(message_start)}\n\n'.encode() + + try: + async for chunk in openai_stream_generator: + if isinstance(chunk, bytes): + chunk = chunk.decode('utf-8', errors='ignore') + + for line in chunk.strip().split('\n'): + line = line.strip() + + if not line or not line.startswith('data:'): + continue + + data_str = line[5:].strip() + if data_str == '[DONE]': + continue + if data_str == '{}': + continue + + try: + data = json.loads(data_str) + except (json.JSONDecodeError, TypeError): + continue + + choices = data.get('choices', []) + if not choices: + # Check for usage in the final chunk + if data.get('usage'): + input_tokens = data['usage'].get('prompt_tokens', input_tokens) + output_tokens = data['usage'].get('completion_tokens', output_tokens) + continue + + delta = choices[0].get('delta', {}) + finish_reason = choices[0].get('finish_reason') + + # Update usage if present + if data.get('usage'): + input_tokens = data['usage'].get('prompt_tokens', input_tokens) + output_tokens = data['usage'].get('completion_tokens', output_tokens) + + # --- Handle text content --- + content = delta.get('content') + if content is not None: + if not text_block_open: + # Start a new text content block + block_start = { + 'type': 'content_block_start', + 'index': current_block_index, + 'content_block': {'type': 'text', 'text': ''}, + } + yield f'event: content_block_start\ndata: {json.dumps(block_start)}\n\n'.encode() + text_block_open = True + + # Send text delta + block_delta = { + 'type': 'content_block_delta', + 'index': current_block_index, + 'delta': {'type': 'text_delta', 'text': content}, + } + yield f'event: content_block_delta\ndata: {json.dumps(block_delta)}\n\n'.encode() + + # --- Handle tool calls --- + tool_calls = delta.get('tool_calls') + if tool_calls: + # Close text block if one is open (text comes before tools) + if text_block_open: + block_stop = { + 'type': 'content_block_stop', + 'index': current_block_index, + } + yield f'event: content_block_stop\ndata: {json.dumps(block_stop)}\n\n'.encode() + text_block_open = False + current_block_index += 1 + + for tc in tool_calls: + tc_index = tc.get('index', 0) + + if tc_index not in tool_call_started: + # First time seeing this tool call — emit content_block_start + tool_call_blocks[tc_index] = current_block_index + tool_call_started[tc_index] = True + + # Extract tool call ID and name from the first chunk + tc_id = tc.get('id', f'toolu_{_uuid.uuid4().hex[:24]}') + tc_name = tc.get('function', {}).get('name', '') + + block_start = { + 'type': 'content_block_start', + 'index': current_block_index, + 'content_block': { + 'type': 'tool_use', + 'id': tc_id, + 'name': tc_name, + 'input': {}, + }, + } + yield f'event: content_block_start\ndata: {json.dumps(block_start)}\n\n'.encode() + current_block_index += 1 + + # Emit argument chunks as input_json_delta + args_chunk = tc.get('function', {}).get('arguments', '') + if args_chunk: + block_delta = { + 'type': 'content_block_delta', + 'index': tool_call_blocks[tc_index], + 'delta': { + 'type': 'input_json_delta', + 'partial_json': args_chunk, + }, + } + yield f'event: content_block_delta\ndata: {json.dumps(block_delta)}\n\n'.encode() + + # --- Handle finish reason --- + if finish_reason is not None: + stop_reason_map = { + 'stop': 'end_turn', + 'length': 'max_tokens', + 'tool_calls': 'tool_use', + } + stop_reason = stop_reason_map.get(finish_reason, 'end_turn') + + except Exception as e: + log.error(f'Error in Anthropic stream conversion: {e}') + + # Close any open text block + if text_block_open: + block_stop = {'type': 'content_block_stop', 'index': current_block_index} + yield f'event: content_block_stop\ndata: {json.dumps(block_stop)}\n\n'.encode() + + # Close any open tool call blocks + for tc_index, block_index in tool_call_blocks.items(): + block_stop = {'type': 'content_block_stop', 'index': block_index} + yield f'event: content_block_stop\ndata: {json.dumps(block_stop)}\n\n'.encode() + + # Emit message_delta with stop reason + message_delta = { + 'type': 'message_delta', + 'delta': { + 'stop_reason': stop_reason, + 'stop_sequence': None, + }, + 'usage': {'output_tokens': output_tokens}, + } + yield f'event: message_delta\ndata: {json.dumps(message_delta)}\n\n'.encode() + + # Emit message_stop + yield f'event: message_stop\ndata: {json.dumps({"type": "message_stop"})}\n\n'.encode() diff --git a/backend/open_webui/utils/asgi_middleware.py b/backend/open_webui/utils/asgi_middleware.py new file mode 100644 index 0000000000000000000000000000000000000000..e3872dd231b9c90cdb0125361b54f382177cd3e0 --- /dev/null +++ b/backend/open_webui/utils/asgi_middleware.py @@ -0,0 +1,276 @@ +""" +Pure-ASGI replacements for the project's previous +`@app.middleware('http')` / `BaseHTTPMiddleware` middlewares. + +Why this matters +---------------- +Starlette's `BaseHTTPMiddleware` (which `@app.middleware('http')` is +sugar for) runs the downstream app inside an `anyio` task group. When +the wrapper exits — for any reason: response complete, client +disconnect, an outer middleware bailing out — the task group cancels +the inner task. That `CancelledError` then propagates into whatever +the inner task was doing, including in-flight DB queries, embedding +calls and disk I/O. + +In Open WebUI this surfaces as: + +* SQLAlchemy logging multi-page `NotImplementedError: + terminate_force_close()` tracebacks at ERROR every time a request is + cancelled mid-DB-call (the aiosqlite connector cleanup path). +* Spurious cancellations cascading through the four stacked + `@app.middleware('http')` wrappers. + +Pure ASGI middleware does not introduce a cancel scope around the +downstream app, so client disconnects propagate the way ASGI was +designed to (via `receive()` returning `http.disconnect`) instead of +being injected as `CancelledError` into arbitrary `await` points. + +Reference: https://www.starlette.io/middleware/#limitations +""" + +from __future__ import annotations + +import logging +import re +import time +from urllib.parse import parse_qs, urlencode + +from fastapi.responses import JSONResponse, RedirectResponse +from fastapi.security import HTTPAuthorizationCredentials +from starlette.datastructures import MutableHeaders +from starlette.requests import Request +from starlette.types import ASGIApp, Message, Receive, Scope, Send + +from open_webui.env import CUSTOM_API_KEY_HEADER +from open_webui.internal.db import ScopedSession +from open_webui.utils.auth import get_http_authorization_cred + +log = logging.getLogger(__name__) + + +class CommitSessionMiddleware: + """Commit and release the thread-local sync `ScopedSession` after each + HTTP request. + + Most requests now use the async session; the sync ScopedSession is + only touched by startup, healthchecks, and a handful of legacy + helpers (notably the pgvector / opengauss vector-DB clients). The + middleware exists so that PostgreSQL connections do not accumulate + as "idle in transaction" and so that any pending sync work made + inside the request is durably persisted. + + Failure semantics + ----------------- + * Downstream raised → roll back any pending sync work, release the + connection, and re-raise so the outer exception middleware can + turn it into an error response. We never commit work on a + request that did not complete successfully. + * Downstream returned → commit pending sync work; on commit + failure, log loudly, roll back, and re-raise. Note that in pure + ASGI the response messages have already been emitted by the + time `await self.app(...)` returns, so a commit failure cannot + retroactively change what the client sees on the wire — but + re-raising still surfaces the error in logs and to ASGI servers + that expose it. We deliberately do not buffer the response to + gate it on commit success, because that would defeat streaming + responses (chat completions, SSE) which are core to the app. + + For request paths where commit-before-send is required, manage the + sync session explicitly inside the handler instead of relying on + this middleware. + """ + + def __init__(self, app: ASGIApp) -> None: + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope['type'] != 'http': + await self.app(scope, receive, send) + return + + try: + await self.app(scope, receive, send) + except BaseException: + # Downstream did not complete successfully. Roll back any + # pending sync writes, release the connection, and let the + # exception propagate. + try: + ScopedSession.rollback() + except Exception: + log.exception('CommitSessionMiddleware: rollback failed after downstream error') + finally: + ScopedSession.remove() + raise + + # Downstream completed. Commit pending sync work. + try: + ScopedSession.commit() + except Exception: + log.exception('CommitSessionMiddleware: post-request commit failed; response was already sent to client') + try: + ScopedSession.rollback() + except Exception: + log.exception('CommitSessionMiddleware: rollback failed after commit failure') + raise + finally: + # CRITICAL: remove() returns the connection to the pool. + # Without this, connections remain "checked out" and + # accumulate as "idle in transaction" in PostgreSQL. + ScopedSession.remove() + + +class AuthTokenMiddleware: + """Extract the bearer/cookie/API-key credential and stash it on + `request.state.token`. + + The header used for API-key transport is controlled by the + ``CUSTOM_API_KEY_HEADER`` environment variable (default ``x-api-key``). + This is useful when Open WebUI sits behind a reverse proxy that + consumes the ``Authorization`` header for its own authentication — + set the env var to a unique header (e.g. ``X-OpenWebUI-Key``) so + the middleware checks that instead and avoids the 401 short-circuit. + + Routes that depend on `get_verified_user` etc. read this state. + Also exposes `request.state.enable_api_keys` (snapshotted at request + entry from runtime config) and stamps an `X-Process-Time` response + header. + """ + + def __init__(self, app: ASGIApp, *, fastapi_app) -> None: + self.app = app + self._fastapi_app = fastapi_app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope['type'] != 'http': + await self.app(scope, receive, send) + return + + start_time = time.monotonic() + request = Request(scope) + + token = get_http_authorization_cred(request.headers.get('Authorization')) + if token is None: + cookie_token = request.cookies.get('token') + if cookie_token: + token = HTTPAuthorizationCredentials(scheme='Bearer', credentials=cookie_token) + if token is None: + api_key = request.headers.get(CUSTOM_API_KEY_HEADER) + if api_key: + token = HTTPAuthorizationCredentials(scheme='Bearer', credentials=api_key) + + request.state.token = token + request.state.enable_api_keys = self._fastapi_app.state.config.ENABLE_API_KEYS + + async def send_with_timing(message: Message) -> None: + if message['type'] == 'http.response.start': + process_time = int(time.monotonic() - start_time) + headers = MutableHeaders(scope=message) + headers['X-Process-Time'] = str(process_time) + await send(message) + + await self.app(scope, receive, send_with_timing) + + +class WebsocketUpgradeGuardMiddleware: + """Reject HTTP requests to `/ws/socket.io` that claim + `transport=websocket` but lack the proper `Upgrade`/`Connection` + headers. + + Works around https://github.com/miguelgrinberg/python-engineio/issues/367 + where engineio mishandles such requests. + """ + + def __init__(self, app: ASGIApp) -> None: + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope['type'] != 'http': + await self.app(scope, receive, send) + return + + path = scope.get('path', '') + if '/ws/socket.io' in path: + query_string = scope.get('query_string', b'').decode('latin-1', errors='replace') + query_params = parse_qs(query_string) + if query_params.get('transport', [''])[0] == 'websocket': + headers = _scope_headers(scope) + upgrade = headers.get('upgrade', '').lower() + connection_tokens = [token.strip() for token in headers.get('connection', '').lower().split(',')] + if upgrade != 'websocket' or 'upgrade' not in connection_tokens: + response = JSONResponse( + status_code=400, + content={'detail': 'Invalid WebSocket upgrade request'}, + ) + await response(scope, receive, send) + return + + await self.app(scope, receive, send) + + +class RedirectMiddleware: + """Rewrites a couple of legacy entry-points to the SPA's own routes: + + * ``GET /watch?v=ID`` (YouTube) → ``/?youtube=ID`` + * ``GET /?shared=…`` (PWA share-target) → ``/?youtube=…`` / + ``/?load-url=…`` / ``/?q=…`` + """ + + def __init__(self, app: ASGIApp) -> None: + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope['type'] != 'http' or scope.get('method', '').upper() != 'GET': + await self.app(scope, receive, send) + return + + path = scope.get('path', '') + query_string = scope.get('query_string', b'').decode('latin-1', errors='replace') + query_params = parse_qs(query_string) + + redirect_params: dict[str, str] = {} + if path.endswith('/watch') and 'v' in query_params and query_params['v']: + redirect_params['youtube'] = query_params['v'][0] + + if 'shared' in query_params and query_params['shared']: + text = query_params['shared'][0] + if text: + url_match = re.match(r'https://\S+', text) + if url_match: + # Local import: youtube loader pulls heavy deps and is + # only needed when a share-target actually contains a + # YouTube URL. + from open_webui.retrieval.loaders.youtube import _parse_video_id + + youtube_video_id = _parse_video_id(url_match[0]) + if youtube_video_id: + redirect_params['youtube'] = youtube_video_id + else: + redirect_params['load-url'] = url_match[0] + else: + redirect_params['q'] = text + + if redirect_params: + redirect_url = f'/?{urlencode(redirect_params)}' + response = RedirectResponse(url=redirect_url) + await response(scope, receive, send) + return + + await self.app(scope, receive, send) + + +def _scope_headers(scope: Scope) -> dict[str, str]: + """Return ASGI scope headers as a lower-cased str→str dict. + + ASGI delivers headers as a list of (bytes, bytes) pairs. For + convenience, fold duplicate keys with comma-joining (matching + HTTP/1.1 semantics). + """ + decoded: dict[str, str] = {} + for raw_key, raw_value in scope.get('headers', []): + key = raw_key.decode('latin-1').lower() + value = raw_value.decode('latin-1') + if key in decoded: + decoded[key] = f'{decoded[key]}, {value}' + else: + decoded[key] = value + return decoded diff --git a/backend/open_webui/utils/audit.py b/backend/open_webui/utils/audit.py new file mode 100644 index 0000000000000000000000000000000000000000..5686c88d5d91f2bb3c747fd584ede9c752fabc7a --- /dev/null +++ b/backend/open_webui/utils/audit.py @@ -0,0 +1,289 @@ +from contextlib import asynccontextmanager +from dataclasses import asdict, dataclass +from enum import Enum +import re +from typing import ( + TYPE_CHECKING, + Any, + AsyncGenerator, + Dict, + MutableMapping, + Optional, + cast, +) +import uuid + +from asgiref.typing import ( + ASGI3Application, + ASGIReceiveCallable, + ASGIReceiveEvent, + ASGISendCallable, + ASGISendEvent, + Scope as ASGIScope, +) +from loguru import logger +from starlette.requests import Request + +from open_webui.env import AUDIT_LOG_LEVEL, ENABLE_AUDIT_GET_REQUESTS, AUDIT_INCLUDED_PATHS, MAX_BODY_LOG_SIZE +from open_webui.utils.auth import get_current_user, get_http_authorization_cred +from open_webui.models.users import UserModel + +if TYPE_CHECKING: + from loguru import Logger + + +@dataclass(frozen=True) +class AuditLogEntry: + # `Metadata` audit level properties + id: str + user: Optional[dict[str, Any]] + audit_level: str + verb: str + request_uri: str + user_agent: Optional[str] = None + source_ip: Optional[str] = None + # `Request` audit level properties + request_object: Any = None + # `Request Response` level + response_object: Any = None + response_status_code: Optional[int] = None + + +class AuditLevel(str, Enum): + NONE = 'NONE' + METADATA = 'METADATA' + REQUEST = 'REQUEST' + REQUEST_RESPONSE = 'REQUEST_RESPONSE' + + +class AuditLogger: + """ + A helper class that encapsulates audit logging functionality. It uses Loguru’s logger with an auditable binding to ensure that audit log entries are filtered correctly. + + Parameters: + logger (Logger): An instance of Loguru’s logger. + """ + + def __init__(self, logger: 'Logger'): + self.logger = logger.bind(auditable=True) + + def write( + self, + audit_entry: AuditLogEntry, + *, + log_level: str = 'INFO', + extra: Optional[dict] = None, + ): + entry = asdict(audit_entry) + + if extra: + entry['extra'] = extra + + self.logger.log( + log_level, + '', + **entry, + ) + + +class AuditContext: + """ + Captures and aggregates the HTTP request and response bodies during the processing of a request. It ensures that only a configurable maximum amount of data is stored to prevent excessive memory usage. + + Attributes: + request_body (bytearray): Accumulated request payload. + response_body (bytearray): Accumulated response payload. + max_body_size (int): Maximum number of bytes to capture. + metadata (Dict[str, Any]): A dictionary to store additional audit metadata (user, http verb, user agent, etc.). + """ + + def __init__(self, max_body_size: int = MAX_BODY_LOG_SIZE): + self.request_body = bytearray() + self.response_body = bytearray() + self.max_body_size = max_body_size + self.metadata: Dict[str, Any] = {} + + def add_request_chunk(self, chunk: bytes): + if len(self.request_body) < self.max_body_size: + self.request_body.extend(chunk[: self.max_body_size - len(self.request_body)]) + + def add_response_chunk(self, chunk: bytes): + if len(self.response_body) < self.max_body_size: + self.response_body.extend(chunk[: self.max_body_size - len(self.response_body)]) + + +class AuditLoggingMiddleware: + """ + ASGI middleware that intercepts HTTP requests and responses to perform audit logging. It captures request/response bodies (depending on audit level), headers, HTTP methods, and user information, then logs a structured audit entry at the end of the request cycle. + """ + + DEFAULT_AUDITED_METHODS = {'PUT', 'PATCH', 'DELETE', 'POST'} + + def __init__( + self, + app: ASGI3Application, + *, + excluded_paths: Optional[list[str]] = None, + included_paths: Optional[list[str]] = None, + max_body_size: int = MAX_BODY_LOG_SIZE, + audit_level: AuditLevel = AuditLevel.NONE, + audit_get_requests: bool = False, + ) -> None: + self.app = app + self.audit_logger = AuditLogger(logger) + self.excluded_paths = excluded_paths or [] + self.included_paths = included_paths or [] + self.max_body_size = max_body_size + self.audited_methods = set(self.DEFAULT_AUDITED_METHODS) + if audit_get_requests: + self.audited_methods.add('GET') + self.audit_level = audit_level + + if self.included_paths and self.excluded_paths: + logger.warning( + 'Both AUDIT_INCLUDED_PATHS and AUDIT_EXCLUDED_PATHS are set. ' + 'AUDIT_INCLUDED_PATHS (whitelist) takes precedence.' + ) + + async def __call__( + self, + scope: ASGIScope, + receive: ASGIReceiveCallable, + send: ASGISendCallable, + ) -> None: + if scope['type'] != 'http': + return await self.app(scope, receive, send) + + request = Request(scope=cast(MutableMapping, scope)) + + if self._should_skip_auditing(request): + return await self.app(scope, receive, send) + + async with self._audit_context(request) as context: + + async def send_wrapper(message: ASGISendEvent) -> None: + if self.audit_level == AuditLevel.REQUEST_RESPONSE: + await self._capture_response(message, context) + + await send(message) + + original_receive = receive + + async def receive_wrapper() -> ASGIReceiveEvent: + nonlocal original_receive + message = await original_receive() + + if self.audit_level in ( + AuditLevel.REQUEST, + AuditLevel.REQUEST_RESPONSE, + ): + await self._capture_request(message, context) + + return message + + await self.app(scope, receive_wrapper, send_wrapper) + + @asynccontextmanager + async def _audit_context(self, request: Request) -> AsyncGenerator[AuditContext, None]: + """ + async context manager that ensures that an audit log entry is recorded after the request is processed. + """ + context = AuditContext() + try: + yield context + finally: + await self._log_audit_entry(request, context) + + async def _get_authenticated_user(self, request: Request) -> Optional[UserModel]: + auth_header = request.headers.get('Authorization') + + try: + user = await get_current_user(request, None, None, get_http_authorization_cred(auth_header)) + return user + except Exception as e: + logger.debug(f'Failed to get authenticated user: {str(e)}') + + return None + + def _should_skip_auditing(self, request: Request) -> bool: + if AUDIT_LOG_LEVEL == 'NONE': + return True + + if request.method not in self.audited_methods: + return True + + ALWAYS_LOG_ENDPOINTS = { + '/api/v1/auths/signin', + '/api/v1/auths/signout', + '/api/v1/auths/signup', + } + path = request.url.path.lower() + for endpoint in ALWAYS_LOG_ENDPOINTS: + if path.startswith(endpoint): + return False # Do NOT skip logging for auth endpoints + + # Skip logging if the request is not authenticated + # Check both Authorization header (API keys) and token cookie (browser sessions) + if not request.headers.get('authorization') and not request.cookies.get('token'): + return True + + # Whitelist mode: only log paths that match included_paths + if self.included_paths: + pattern = re.compile(r'^/api(?:/v1)?/(' + '|'.join(self.included_paths) + r')\b') + if not pattern.match(request.url.path): + return True # Skip: path not in whitelist + return False # Do NOT skip: path is in whitelist + + # Blacklist mode: skip paths that match excluded_paths + pattern = re.compile(r'^/api(?:/v1)?/(' + '|'.join(self.excluded_paths) + r')\b') + if pattern.match(request.url.path): + return True + + return False + + async def _capture_request(self, message: ASGIReceiveEvent, context: AuditContext): + if message['type'] == 'http.request': + body = message.get('body', b'') + context.add_request_chunk(body) + + async def _capture_response(self, message: ASGISendEvent, context: AuditContext): + if message['type'] == 'http.response.start': + context.metadata['response_status_code'] = message['status'] + + elif message['type'] == 'http.response.body': + body = message.get('body', b'') + context.add_response_chunk(body) + + async def _log_audit_entry(self, request: Request, context: AuditContext): + try: + user = await self._get_authenticated_user(request) + + user = user.model_dump(include={'id', 'name', 'email', 'role'}) if user else {} + + request_body = context.request_body.decode('utf-8', errors='replace') + response_body = context.response_body.decode('utf-8', errors='replace') + + # Redact sensitive information + if 'password' in request_body: + request_body = re.sub( + r'"password":\s*"(.*?)"', + '"password": "********"', + request_body, + ) + + entry = AuditLogEntry( + id=str(uuid.uuid4()), + user=user, + audit_level=self.audit_level.value, + verb=request.method, + request_uri=str(request.url), + response_status_code=context.metadata.get('response_status_code', None), + source_ip=request.client.host if request.client else None, + user_agent=request.headers.get('user-agent'), + request_object=request_body, + response_object=response_body, + ) + + self.audit_logger.write(entry) + except Exception as e: + logger.error(f'Failed to log audit entry: {str(e)}') diff --git a/backend/open_webui/utils/auth.py b/backend/open_webui/utils/auth.py new file mode 100644 index 0000000000000000000000000000000000000000..e0f331a9df6a68a2981cdbfec476816da236b0fb --- /dev/null +++ b/backend/open_webui/utils/auth.py @@ -0,0 +1,507 @@ +import logging +import uuid +import jwt +import base64 +import hmac +import hashlib +import requests +import os +import bcrypt + +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives.asymmetric import ed25519 +from cryptography.hazmat.primitives import serialization +import json + + +from datetime import datetime, timedelta +import pytz +from pytz import UTC +from typing import Optional, Union, List, Dict + + +from open_webui.utils.access_control import has_permission +from open_webui.models.users import Users +from open_webui.models.auths import Auths + + +from open_webui.constants import ERROR_MESSAGES + +from open_webui.env import ( + ENABLE_OTEL, + ENABLE_PASSWORD_VALIDATION, + OFFLINE_MODE, + LICENSE_BLOB, + PASSWORD_VALIDATION_HINT, + PASSWORD_VALIDATION_REGEX_PATTERN, + REDIS_KEY_PREFIX, + pk, + WEBUI_SECRET_KEY, + TRUSTED_SIGNATURE_KEY, + STATIC_DIR, + WEBUI_AUTH_TRUSTED_EMAIL_HEADER, +) + +from fastapi import BackgroundTasks, Depends, HTTPException, Request, Response, status +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer + +log = logging.getLogger(__name__) + +SESSION_SECRET = WEBUI_SECRET_KEY +ALGORITHM = 'HS256' + +############## +# Auth Utils +############## + + +def verify_signature(payload: str, signature: str) -> bool: + """ + Verifies the HMAC signature of the received payload. + """ + try: + expected_signature = base64.b64encode( + hmac.new(TRUSTED_SIGNATURE_KEY, payload.encode(), hashlib.sha256).digest() + ).decode() + + # Compare securely to prevent timing attacks + return hmac.compare_digest(expected_signature, signature) + + except Exception: + return False + + +def override_static(path: str, content: str): + # Ensure path is safe + if '/' in path or '..' in path: + log.error(f'Invalid path: {path}') + return + + file_path = os.path.join(STATIC_DIR, path) + os.makedirs(os.path.dirname(file_path), exist_ok=True) + + with open(file_path, 'wb') as f: + f.write(base64.b64decode(content)) # Convert Base64 back to raw binary + + +def get_license_data(app, key): + def data_handler(data): + for k, v in data.items(): + if k == 'resources': + for p, c in v.items(): + globals().get('override_static', lambda a, b: None)(p, c) + elif k == 'count': + setattr(app.state, 'USER_COUNT', v) + elif k == 'name': + setattr(app.state, 'WEBUI_NAME', v) + elif k == 'metadata': + setattr(app.state, 'LICENSE_METADATA', v) + + def handler(u): + res = requests.post( + f'{u}/api/v1/license/', + json={'key': key, 'version': '1'}, + timeout=5, + ) + + if getattr(res, 'ok', False): + payload = getattr(res, 'json', lambda: {})() + data_handler(payload) + return True + else: + log.error(f'License: retrieval issue: {getattr(res, "text", "unknown error")}') + + if key: + us = [ + 'https://api.openwebui.com', + 'https://licenses.api.openwebui.com', + ] + try: + for u in us: + if handler(u): + return True + except Exception as ex: + log.exception(f'License: Uncaught Exception: {ex}') + + try: + if LICENSE_BLOB: + nl = 12 + kb = hashlib.sha256((key.replace('-', '').upper()).encode()).digest() + + def nt(b): + return b[:nl], b[nl:] + + lb = base64.b64decode(LICENSE_BLOB) + ln, lt = nt(lb) + + aesgcm = AESGCM(kb) + p = json.loads(aesgcm.decrypt(ln, lt, None)) + pk.verify(base64.b64decode(p['s']), p['p'].encode()) + + pb = base64.b64decode(p['p']) + pn, pt = nt(pb) + + data = json.loads(aesgcm.decrypt(pn, pt, None).decode()) + + exp = data.get('exp') + if exp: + if isinstance(exp, str): + from datetime import date + + exp = date.fromisoformat(exp) + if exp < datetime.now().date(): + return False + + data_handler(data) + return True + except Exception as e: + log.error(f'License: {e}') + + return False + + +bearer_security = HTTPBearer(auto_error=False) + + +def get_password_hash(password: str) -> str: + """Hash a password using bcrypt""" + return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') + + +def validate_password(password: str) -> bool: + # The password passed to bcrypt must be 72 bytes or fewer. If it is longer, it will be truncated before hashing. + if len(password.encode('utf-8')) > 72: + raise Exception( + ERROR_MESSAGES.PASSWORD_TOO_LONG, + ) + + if ENABLE_PASSWORD_VALIDATION: + if not PASSWORD_VALIDATION_REGEX_PATTERN.match(password): + raise Exception(ERROR_MESSAGES.INVALID_PASSWORD(PASSWORD_VALIDATION_HINT)) + + return True + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """Verify a password against its hash""" + return ( + bcrypt.checkpw( + plain_password.encode('utf-8'), + hashed_password.encode('utf-8'), + ) + if hashed_password + else None + ) + + +# 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. +def create_token(data: dict, expires_delta: Union[timedelta, None] = None) -> str: + payload = data.copy() + + if expires_delta: + expire = datetime.now(UTC) + expires_delta + payload.update({'exp': expire}) + + jti = str(uuid.uuid4()) + payload.update({'jti': jti, 'iat': datetime.now(UTC)}) + + encoded_jwt = jwt.encode(payload, SESSION_SECRET, algorithm=ALGORITHM) + return encoded_jwt + + +def decode_token(token: str) -> Optional[dict]: + try: + decoded = jwt.decode(token, SESSION_SECRET, algorithms=[ALGORITHM]) + return decoded + except Exception: + return None + + +async def is_valid_token(request, decoded) -> bool: + """ + Check whether a JWT has been revoked. Two mechanisms: + 1. Per-token (jti) — used by user-initiated sign-out (known jti). + 2. Per-user (revoked_at) — used by OIDC back-channel logout when + individual jti values are unknown; rejects tokens with iat <= revoked_at. + """ + if request.app.state.redis: + # Per-token revocation + jti = decoded.get('jti') + if jti: + revoked = await request.app.state.redis.get(f'{REDIS_KEY_PREFIX}:auth:token:{jti}:revoked') + if revoked: + return False + + # Per-user revocation (OIDC back-channel logout) + user_id = decoded.get('id') + if user_id: + revoked_at = await request.app.state.redis.get(f'{REDIS_KEY_PREFIX}:auth:user:{user_id}:revoked_at') + if revoked_at: + try: + revoked_at_ts = int(revoked_at) + token_iat = decoded.get('iat') + # No iat means legacy token — reject since we can't verify issue time + if token_iat is None or token_iat <= revoked_at_ts: + return False + except (ValueError, TypeError): + pass + + return True + + +async def invalidate_token(request, token): + decoded = decode_token(token) + + # If token is invalid/expired, nothing to revoke + if not decoded: + return + + # Require Redis to store revoked tokens + if request.app.state.redis: + jti = decoded.get('jti') + exp = decoded.get('exp') + + if jti and exp: + ttl = exp - int(datetime.now(UTC).timestamp()) # Calculate time-to-live for the token + + if ttl > 0: + # Store the revoked token in Redis with an expiration time + await request.app.state.redis.set( + f'{REDIS_KEY_PREFIX}:auth:token:{jti}:revoked', + '1', + ex=ttl, + ) + + +def extract_token_from_auth_header(auth_header: str): + return auth_header[len('Bearer ') :] + + +def create_api_key(): + key = str(uuid.uuid4()).replace('-', '') + return f'sk-{key}' + + +def get_http_authorization_cred(auth_header: Optional[str]): + if not auth_header: + return None + try: + scheme, credentials = auth_header.split(' ') + return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) + except Exception: + return None + + +async def get_current_user( + request: Request, + 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). +): + token = None + + if auth_token is not None: + token = auth_token.credentials + + if token is None and 'token' in request.cookies: + token = request.cookies.get('token') + + # Fallback to request.state.token (set by middleware, e.g. for x-api-key) + if token is None and hasattr(request.state, 'token') and request.state.token: + token = request.state.token.credentials + + if token is None: + raise HTTPException(status_code=401, detail='Not authenticated') + + # auth by api key + if token.startswith('sk-'): + user = await get_current_user_by_api_key(request, token) + + # Add user info to current span + if ENABLE_OTEL: + from opentelemetry import trace + + current_span = trace.get_current_span() + if current_span: + current_span.set_attribute('client.user.id', user.id) + current_span.set_attribute('client.user.email', user.email) + current_span.set_attribute('client.user.role', user.role) + current_span.set_attribute('client.auth.type', 'api_key') + + return user + + # auth by jwt token + try: + try: + data = decode_token(token) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Invalid token', + ) + + if data is not None and 'id' in data: + if data.get('jti') and not await is_valid_token(request, data): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='Invalid token', + ) + + user = await Users.get_user_by_id(data['id']) + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) + else: + if WEBUI_AUTH_TRUSTED_EMAIL_HEADER: + trusted_email = request.headers.get(WEBUI_AUTH_TRUSTED_EMAIL_HEADER, '').lower() + if trusted_email and user.email != trusted_email: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail='User mismatch. Please sign in again.', + ) + + # Add user info to current span + if ENABLE_OTEL: + from opentelemetry import trace + + current_span = trace.get_current_span() + if current_span: + current_span.set_attribute('client.user.id', user.id) + current_span.set_attribute('client.user.email', user.email) + current_span.set_attribute('client.user.role', user.role) + current_span.set_attribute('client.auth.type', 'jwt') + + # Refresh the user's last active timestamp + # Fire-and-forget via asyncio.create_task to avoid blocking + import asyncio + + asyncio.create_task(Users.update_last_active_by_id(user.id)) + return user + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + except Exception as e: + # Delete the token cookie + if request.cookies.get('token'): + response.delete_cookie('token') + + if request.cookies.get('oauth_id_token'): + response.delete_cookie('oauth_id_token') + + # Delete OAuth session if present + if request.cookies.get('oauth_session_id'): + response.delete_cookie('oauth_session_id') + + raise e + + +async def get_current_user_by_api_key(request, api_key: str): + # Each function call manages its own short-lived session internally + user = await Users.get_user_by_api_key(api_key) + + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) + + if not request.state.enable_api_keys or ( + user.role != 'admin' + and not await has_permission( + user.id, + 'features.api_keys', + request.app.state.config.USER_PERMISSIONS, + ) + ): + raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.API_KEY_NOT_ALLOWED) + + # Enforce endpoint restrictions — checked here (not in middleware) + # so it applies regardless of how the API key was transported + # (Authorization header, cookie, x-api-key header, etc.). + if request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS: + allowed_paths = [ + path.strip() for path in str(request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS).split(',') if path.strip() + ] + request_path = request.url.path + is_allowed = any(request_path == allowed or request_path.startswith(allowed + '/') for allowed in allowed_paths) + if not is_allowed: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + # Add user info to current span + if ENABLE_OTEL: + from opentelemetry import trace + + current_span = trace.get_current_span() + if current_span: + current_span.set_attribute('client.user.id', user.id) + current_span.set_attribute('client.user.email', user.email) + current_span.set_attribute('client.user.role', user.role) + current_span.set_attribute('client.auth.type', 'api_key') + + await Users.update_last_active_by_id(user.id) + return user + + +def get_verified_user(user=Depends(get_current_user)): + if user.role not in {'user', 'admin'}: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + return user + + +def get_admin_user(user=Depends(get_current_user)): + if user.role != 'admin': + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + return user + + +async def create_admin_user(email: str, password: str, name: str = 'Admin'): + """ + Create an admin user from environment variables. + Used for headless/automated deployments. + Returns the created user or None if creation failed. + """ + + if not email or not password: + return None + + if await Users.has_users(): + log.debug('Users already exist, skipping admin creation') + return None + + log.info(f'Creating admin account from environment variables: {email}') + try: + hashed = get_password_hash(password) + user = await Auths.insert_new_auth( + email=email.lower(), + password=hashed, + name=name, + role='admin', + ) + if user: + log.info(f'Admin account created successfully: {email}') + return user + else: + log.error('Failed to create admin account from environment variables') + return None + except Exception as e: + log.error(f'Error creating admin account: {e}') + return None diff --git a/backend/open_webui/utils/automations.py b/backend/open_webui/utils/automations.py new file mode 100644 index 0000000000000000000000000000000000000000..95b931e320e37018f198264cea1fa6b292a3868a --- /dev/null +++ b/backend/open_webui/utils/automations.py @@ -0,0 +1,593 @@ +""" +Automation utilities and unified scheduler. + +RRULE helpers, scheduler worker loop, and execution logic. +Follows the utils/.py pattern (cf. utils/channels.py, utils/task.py). + +The scheduler_worker_loop handles all time-based background work: + - Automation execution (claim_due → execute) + - Calendar event alerts (upcoming events → socket + webhook notifications) + +Environment: + SCHEDULER_POLL_INTERVAL – seconds between polls (default: 10) + CALENDAR_ALERT_LOOKAHEAD_MINUTES – default alert window (default: 5) +""" + +import asyncio +import logging +import os +import random +import time +from datetime import datetime +from typing import Optional +from uuid import uuid4 +from zoneinfo import ZoneInfo + +from dateutil.rrule import rrulestr +from fastapi import Request +from starlette.datastructures import Headers + +from open_webui.constants import ERROR_MESSAGES +from open_webui.models.automations import Automations, AutomationRuns, AutomationModel +from open_webui.models.chats import ChatForm, Chats +from open_webui.models.users import Users +from open_webui.utils.task import prompt_template +from open_webui.internal.db import get_async_db + +log = logging.getLogger(__name__) + +SCHEDULER_POLL_INTERVAL = int(os.getenv('SCHEDULER_POLL_INTERVAL', os.getenv('AUTOMATION_POLL_INTERVAL', '10'))) +CALENDAR_ALERT_LOOKAHEAD_MINUTES = int(os.getenv('CALENDAR_ALERT_LOOKAHEAD_MINUTES', '10')) + + +#################### +# RRULE Helpers +#################### + + +def _resolve_tz(tz: str = None) -> Optional[ZoneInfo]: + """Safely resolve a timezone string to ZoneInfo. + + Returns None (→ server-local fallback) when *tz* is empty, None, + or an unrecognised IANA key. Logs a warning on bad keys so + misconfiguration is visible in the server logs. + """ + if not tz: + return None + try: + return ZoneInfo(tz) + except (KeyError, Exception): + log.warning('Unknown timezone %r — falling back to server time', tz) + return None + + +def _parse_rule(s: str): + """Parse RRULE with clock-aligned DTSTART for sub-daily frequencies. + + MINUTELY/HOURLY rules use a fixed epoch DTSTART (2000-01-01 00:00) + so intervals snap to clock boundaries (e.g. every 5min = :00, :05, :10). + """ + raw = s.replace('RRULE:', '') + parts = dict(p.split('=', 1) for p in raw.split(';') if '=' in p) + freq = parts.get('FREQ', '') + + if freq in ('MINUTELY', 'HOURLY'): + epoch = datetime(2000, 1, 1, 0, 0, 0) + return rrulestr(s, dtstart=epoch, ignoretz=True) + return rrulestr(s, ignoretz=True) + + +def validate_rrule(s: str, tz: str = None) -> None: + """Raise ValueError if the RRULE is malformed or exhausted. + + When *tz* is provided the "now" reference uses the user's local + clock so that near-future schedules are not incorrectly rejected + on servers whose system clock is ahead (e.g. UTC vs US timezones). + """ + try: + rule = _parse_rule(s) + except Exception as e: + raise ValueError(ERROR_MESSAGES.AUTOMATION_INVALID_RRULE(e)) + zi = _resolve_tz(tz) + now = datetime.now(zi).replace(tzinfo=None) if zi else datetime.now() + if rule.after(now) is None: + raise ValueError(ERROR_MESSAGES.AUTOMATION_NO_FUTURE_RUNS) + + +def next_run_ns(s: str, tz: str = None) -> Optional[int]: + """Next occurrence as epoch nanoseconds, respecting user timezone.""" + zi = _resolve_tz(tz) + now = datetime.now(zi) if zi else datetime.now() + dt = _parse_rule(s).after(now.replace(tzinfo=None)) + if dt is None: + return None + if zi: + dt = dt.replace(tzinfo=zi) + return int(dt.timestamp() * 1_000_000_000) + + +def next_n_runs_ns(s: str, n: int = 5, tz: str = None) -> list[int]: + """Compute next N occurrences for UI preview. + + Uses the user's timezone for the starting "now" so that the + preview matches the user's local clock (same as next_run_ns). + """ + zi = _resolve_tz(tz) + rule = _parse_rule(s) + result = [] + now = datetime.now(zi).replace(tzinfo=None) if zi else datetime.now() + dt = now + for _ in range(n): + dt = rule.after(dt) + if not dt: + break + if zi: + dt_tz = dt.replace(tzinfo=zi) + result.append(int(dt_tz.timestamp() * 1_000_000_000)) + else: + result.append(int(dt.timestamp() * 1_000_000_000)) + return result + + +def rrule_interval_seconds(s: str) -> Optional[int]: + """Approximate interval between recurrences in seconds. + + Returns None for one-shot (COUNT=1) schedules or rules + with fewer than two future occurrences. + """ + if 'COUNT=1' in s: + return None + rule = _parse_rule(s) + now = datetime.now() + first = rule.after(now) + if first is None: + return None + second = rule.after(first) + if second is None: + return None + return int((second - first).total_seconds()) + + +############################ +# Worker Loop +############################ + + +# Keep the old name as an alias so any stale imports still work. +async def automation_worker_loop(app) -> None: + """Deprecated alias — use scheduler_worker_loop.""" + await scheduler_worker_loop(app) + + +async def scheduler_worker_loop(app) -> None: + """Unified background scheduler for all time-based work. + + Handles: + 1. Automation execution (ENABLE_AUTOMATIONS) + 2. Calendar event alerts (ENABLE_CALENDAR) + + Runs on every instance. Poll interval is configurable via + SCHEDULER_POLL_INTERVAL env var (default: 10 seconds). + """ + log.info(f'Scheduler worker started (poll interval: {SCHEDULER_POLL_INTERVAL}s)') + while True: + try: + # ── Automations ── + if getattr(app.state.config, 'ENABLE_AUTOMATIONS', False): + try: + async with get_async_db() as db: + batch = await Automations.claim_due(int(time.time_ns()), limit=10, db=db) + if batch: + log.info(f'Claimed {len(batch)} due automation(s)') + for automation in batch: + asyncio.create_task(execute_automation(app, automation)) + except Exception: + log.exception('Scheduler: automation error') + + # ── Calendar Alerts ── + if getattr(app.state.config, 'ENABLE_CALENDAR', False): + try: + await _check_calendar_alerts(app) + except Exception: + log.exception('Scheduler: calendar alert error') + + except Exception: + log.exception('Scheduler worker error') + + # Jitter to spread load across instances + await asyncio.sleep(SCHEDULER_POLL_INTERVAL + random.uniform(0, 2)) + + +########################## +# Execute +#################### + + +def _build_request(app) -> Request: + """Build a minimal ASGI Request for chat_completion. + + Mirrors the mock-request pattern used in main.py lifespan + (model pre-fetch, tool server init) for consistency. + """ + scope = { + 'type': 'http', + 'asgi': {'version': '3.0', 'spec_version': '2.0'}, + 'method': 'POST', + 'path': '/api/v1/automations/internal', + 'query_string': b'', + 'headers': Headers({}).raw, + 'client': ('127.0.0.1', 0), + 'server': ('127.0.0.1', 80), + 'scheme': 'http', + 'app': app, + } + request = Request(scope) + # Ensure request.state is initialized with required attributes + request.state.token = None + request.state.enable_api_keys = False + return request + + +def _resolve_model_tool_ids(app, model_id: str) -> list[str]: + """Read model-attached tool_ids from model config. + + The frontend does this in Chat.svelte (model.info.meta.toolIds). + The backend never auto-resolves them, so we must do it explicitly. + """ + models = getattr(app.state, 'MODELS', {}) + model = models.get(model_id, {}) + tool_ids = model.get('info', {}).get('meta', {}).get('toolIds', []) + return list(tool_ids) if tool_ids else [] + + +def _resolve_model_features(app, model_id: str) -> dict: + """Read model default features from model config. + + The frontend does this in Chat.svelte (model.info.meta.defaultFeatureIds + + model.info.meta.capabilities). Enables features like web_search, + code_interpreter, image_generation when the model has them as defaults + AND the capability is enabled AND the admin has enabled the feature. + """ + models = getattr(app.state, 'MODELS', {}) + model = models.get(model_id, {}) + meta = model.get('info', {}).get('meta', {}) + + default_feature_ids = meta.get('defaultFeatureIds', []) + if not default_feature_ids: + return {} + + capabilities = meta.get('capabilities', {}) + config = app.state.config + features = {} + + # code_interpreter is excluded: it requires the frontend event emitter + # and does not work in headless backend execution. + feature_checks = { + 'web_search': getattr(config, 'ENABLE_WEB_SEARCH', False), + 'image_generation': getattr(config, 'ENABLE_IMAGE_GENERATION', False), + } + + for feature_id in default_feature_ids: + if feature_id in feature_checks: + # Feature must be: in defaultFeatureIds + capability enabled + admin enabled + if capabilities.get(feature_id) and feature_checks[feature_id]: + features[feature_id] = True + + return features + + +def _resolve_model_filter_ids(app, model_id: str) -> list[str]: + """Read model default filter_ids from model config.""" + models = getattr(app.state, 'MODELS', {}) + model = models.get(model_id, {}) + filter_ids = model.get('info', {}).get('meta', {}).get('defaultFilterIds', []) + return list(filter_ids) if filter_ids else [] + + +def _resolve_model_terminal_id(app, model_id: str) -> Optional[str]: + """Read model default terminal_id from model config. + + The frontend does this in Chat.svelte (model.info.meta.terminalId). + """ + models = getattr(app.state, 'MODELS', {}) + model = models.get(model_id, {}) + return model.get('info', {}).get('meta', {}).get('terminalId') or None + + +async def _set_terminal_cwd(app, server_id: str, user, cwd: str, chat_id: str) -> None: + """Set the working directory on a terminal server via the proxy. + + Routes through the open-webui terminal proxy endpoint so that + auth headers, orchestrator policy routing, and X-User-Id are + handled correctly — same path the frontend uses. + """ + import aiohttp + from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL + + connections = getattr(getattr(app, 'state', None), 'config', None) + if connections is None: + return + connections = getattr(connections, 'TERMINAL_SERVER_CONNECTIONS', None) or [] + connection = next((c for c in connections if c.get('id') == server_id), None) + if connection is None: + log.warning(f'Terminal server {server_id} not found for CWD set') + return + + base_url = (connection.get('url') or '').rstrip('/') + if not base_url: + return + + # Build target URL — route through orchestrator policy if configured + policy_id = connection.get('policy_id') + if connection.get('server_type') == 'orchestrator' and policy_id: + target_url = f'{base_url}/p/{policy_id}/files/cwd' + else: + target_url = f'{base_url}/files/cwd' + + headers = {'Content-Type': 'application/json', 'X-User-Id': user.id} + if chat_id: + headers['X-Session-Id'] = chat_id + + auth_type = connection.get('auth_type', 'bearer') + if auth_type == 'bearer': + headers['Authorization'] = f'Bearer {connection.get("key", "")}' + + try: + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session: + async with session.post( + target_url, + json={'path': cwd}, + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as resp: + if resp.status != 200: + body = await resp.text() + log.warning(f'Failed to set terminal CWD to {cwd}: HTTP {resp.status} — {body[:200]}') + except Exception as e: + log.warning(f'Failed to set terminal CWD: {e}') + + +async def execute_automation(app, automation: AutomationModel) -> None: + """Execute an automation through the full chat completion pipeline. + + Creates a real chat, then calls chat_completion exactly like the frontend: + session_id + chat_id + message_id → async task → pipeline handles everything + (filters, model params, knowledge/RAG, tools, DB saves, webhooks). + """ + try: + user = await Users.get_user_by_id(automation.user_id) + if not user: + await _record_run(automation.id, 'error', error='User not found') + return + + prompt = prompt_template(automation.data['prompt'], user) + model_id = automation.data['model_id'] + terminal_config = automation.data.get('terminal') + + # Generate proper UUIDs for messages (same as frontend) + user_msg_id = str(uuid4()) + assistant_msg_id = str(uuid4()) + + chat_id = str(uuid4()) + chat = await Chats.insert_new_chat( + chat_id, + automation.user_id, + ChatForm( + chat={ + 'title': automation.name, + 'models': [model_id], + 'history': { + 'currentId': assistant_msg_id, + 'messages': { + user_msg_id: { + 'id': user_msg_id, + 'parentId': None, + 'role': 'user', + 'content': prompt, + 'childrenIds': [assistant_msg_id], + 'timestamp': int(time.time()), + 'models': [model_id], + }, + assistant_msg_id: { + 'id': assistant_msg_id, + 'parentId': user_msg_id, + 'role': 'assistant', + 'content': '', + 'done': False, + 'model': model_id, + 'childrenIds': [], + 'timestamp': int(time.time()), + }, + }, + }, + 'messages': [ + {'role': 'user', 'content': prompt}, + ], + 'meta': {'automation_id': automation.id}, + } + ), + ) + + if not chat: + await _record_run(automation.id, 'error', error='Failed to create chat') + return + + # Notify frontend to refresh chat list + from open_webui.socket.main import sio + + await sio.emit( + 'events', + { + 'chat_id': chat.id, + 'message_id': user_msg_id, + 'data': {'type': 'chat:list'}, + }, + room=f'user:{automation.user_id}', + ) + + # Resolve model defaults (frontend does this, backend doesn't) + tool_ids = _resolve_model_tool_ids(app, model_id) + features = _resolve_model_features(app, model_id) + filter_ids = _resolve_model_filter_ids(app, model_id) + + # Resolve terminal from model config + terminal_id = _resolve_model_terminal_id(app, model_id) + + # Build the same payload the frontend sends to /api/chat/completions + form_data = { + 'model': model_id, + 'messages': [{'role': 'user', 'content': prompt}], + 'stream': True, + 'chat_id': chat.id, + 'id': assistant_msg_id, + 'parent_id': None, # Root message (chat already created above) + 'user_message': { + 'id': user_msg_id, + 'parentId': None, + 'role': 'user', + 'content': prompt, + }, + 'session_id': f'automation:{automation.id}', + 'background_tasks': {}, + } + if tool_ids: + form_data['tool_ids'] = tool_ids + if features: + form_data['features'] = features + if filter_ids: + form_data['filter_ids'] = filter_ids + if terminal_id: + form_data['terminal_id'] = terminal_id + + # Call the full chat completion pipeline (same as POST /api/chat/completions). + # The handler reference is stored on app.state to avoid circular imports. + request = _build_request(app) + await app.state.CHAT_COMPLETION_HANDLER(request, form_data, user=user) + + # Notify user + from open_webui.socket.main import sio + + await sio.emit( + 'automation:result', + { + 'automation_id': automation.id, + 'name': automation.name, + 'chat_id': chat.id, + 'status': 'success', + }, + room=f'user:{automation.user_id}', + ) + + await _record_run(automation.id, 'success', chat_id=chat.id) + + except Exception as e: + log.exception(f'Automation {automation.id} failed') + await _record_run(automation.id, 'error', error=str(e)[:4000]) + + +#################### +# Internals +#################### + + +async def _check_calendar_alerts(app) -> None: + """Check for upcoming calendar events and send alert notifications. + + De-duplication is DB-backed via meta.alerted_at — survives restarts + and works across multiple instances. + """ + from open_webui.models.calendar import CalendarEvents, CalendarEventUpdateForm + from open_webui.socket.main import sio + + now_ns = int(time.time_ns()) + default_lookahead_ns = CALENDAR_ALERT_LOOKAHEAD_MINUTES * 60 * 1_000_000_000 + + async with get_async_db() as db: + upcoming = await CalendarEvents.get_upcoming_events(now_ns, default_lookahead_ns, db=db) + + if not upcoming: + return + + for event, user_tz in upcoming: + # Skip if already alerted for this start time + if event.meta and event.meta.get('alerted_at'): + continue + + # Compute minutes until event starts + minutes_until = max(0, int((event.start_at - now_ns) / (60 * 1_000_000_000))) + + alert_data = { + 'event_id': event.id, + 'title': event.title, + 'description': event.description or '', + 'start_at': event.start_at, + 'minutes_until': minutes_until, + 'calendar_id': event.calendar_id, + 'location': event.location or '', + } + + await sio.emit( + 'events', + { + 'data': { + 'type': 'calendar:alert', + 'data': alert_data, + }, + }, + room=f'user:{event.user_id}', + ) + + # Mark as alerted in DB so it survives restarts / multi-instance + try: + await CalendarEvents.update_event_by_id( + event.id, + CalendarEventUpdateForm(meta={'alerted_at': now_ns}), + ) + except Exception: + log.debug(f'Failed to mark event {event.id} as alerted', exc_info=True) + + # Send webhook notification if user has one configured + try: + webui_name = getattr(app.state, 'WEBUI_NAME', 'Open WebUI') + enable_user_webhooks = getattr(app.state.config, 'ENABLE_USER_WEBHOOKS', False) + + if enable_user_webhooks: + user = await Users.get_user_by_id(event.user_id) + if user and user.settings: + webhook_url = ( + user.settings.get('ui', {}).get('notifications', {}).get('webhook_url', None) + if isinstance(user.settings, dict) + else getattr(getattr(user.settings, 'ui', None), 'get', lambda *a: None)( + 'notifications', {} + ).get('webhook_url', None) + if hasattr(user.settings, 'ui') + else None + ) + if webhook_url: + from open_webui.utils.webhook import post_webhook + + time_str = f'in {minutes_until} min' if minutes_until > 0 else 'now' + await post_webhook( + webui_name, + webhook_url, + f'{event.title} — starting {time_str}', + { + 'action': 'calendar_alert', + 'title': event.title, + 'minutes_until': minutes_until, + 'event_id': event.id, + }, + ) + except Exception: + log.debug(f'Failed to send webhook for calendar alert {event.id}', exc_info=True) + + +async def _record_run( + automation_id: str, + status: str, + chat_id: str = None, + error: str = None, +): + """Insert a run record into automation_run.""" + async with get_async_db() as db: + await AutomationRuns.insert(automation_id, status, chat_id=chat_id, error=error, db=db) diff --git a/backend/open_webui/utils/calendar.py b/backend/open_webui/utils/calendar.py new file mode 100644 index 0000000000000000000000000000000000000000..9484c58dc0fb9a27fcff52a807e2239ee4eb05f6 --- /dev/null +++ b/backend/open_webui/utils/calendar.py @@ -0,0 +1,83 @@ +""" +Calendar utilities. + +RRULE expansion reusing the automation infra. +""" + +import logging +from datetime import datetime, timedelta +from typing import Optional +from zoneinfo import ZoneInfo + +from open_webui.utils.automations import _parse_rule + +log = logging.getLogger(__name__) + + +def expand_recurring_event( + event_dict: dict, + range_start_ns: int, + range_end_ns: int, + tz: Optional[str] = None, + max_instances: int = 5000, +) -> list[dict]: + """Expand a recurring event into individual instances within a date range. + + Takes an event dict (from CalendarEventModel.model_dump()) and produces + one dict per occurrence, with adjusted start_at / end_at. + """ + from dateutil.rrule import rrulestr + + rrule_str = event_dict.get('rrule') + if not rrule_str: + return [event_dict] + + range_start_dt = datetime.fromtimestamp(range_start_ns / 1_000_000_000) + range_end_dt = datetime.fromtimestamp(range_end_ns / 1_000_000_000) + scan_start = range_start_dt - timedelta(days=1) + + try: + # Parse with dtstart near the range so we never iterate from epoch + rule = rrulestr(rrule_str, dtstart=scan_start, ignoretz=True) + except Exception: + log.warning(f'Failed to parse RRULE for event {event_dict.get("id")}: {rrule_str}') + return [event_dict] + + original_start_ns = event_dict['start_at'] + original_end_ns = event_dict.get('end_at') + duration_ns = (original_end_ns - original_start_ns) if original_end_ns else None + + instances = [] + dt = rule.after(scan_start, inc=True) + + while dt and dt < range_end_dt and len(instances) < max_instances: + if tz: + try: + dt_tz = dt.replace(tzinfo=ZoneInfo(tz)) + instance_start_ns = int(dt_tz.timestamp() * 1_000_000_000) + except Exception: + instance_start_ns = int(dt.timestamp() * 1_000_000_000) + else: + instance_start_ns = int(dt.timestamp() * 1_000_000_000) + + if instance_start_ns >= range_start_ns: + instance = { + **event_dict, + 'start_at': instance_start_ns, + 'end_at': (instance_start_ns + duration_ns) if duration_ns else None, + 'instance_id': f'{event_dict["id"]}_{instance_start_ns}', + } + instances.append(instance) + + dt = rule.after(dt) + + return instances + + +def ns_from_date(year: int, month: int, day: int, tz: Optional[str] = None) -> int: + """Create epoch nanoseconds from a date.""" + if tz: + dt = datetime(year, month, day, tzinfo=ZoneInfo(tz)) + else: + dt = datetime(year, month, day) + return int(dt.timestamp() * 1_000_000_000) diff --git a/backend/open_webui/utils/channels.py b/backend/open_webui/utils/channels.py new file mode 100644 index 0000000000000000000000000000000000000000..6f85dfae1e32778e1850935bd4428dc6cd141915 --- /dev/null +++ b/backend/open_webui/utils/channels.py @@ -0,0 +1,31 @@ +import re + + +def extract_mentions(message: str, triggerChar: str = '@'): + # Escape triggerChar in case it's a regex special character + triggerChar = re.escape(triggerChar) + pattern = rf'<{triggerChar}([A-Z]):([^|>]+)' + + matches = re.findall(pattern, message) + return [{'id_type': id_type, 'id': id_value} for id_type, id_value in matches] + + +def replace_mentions(message: str, triggerChar: str = '@', use_label: bool = True): + """ + Replace mentions in the message with either their label (after the pipe `|`) + or their id if no label exists. + + Example: + "<@M:gpt-4.1|GPT-4>" -> "GPT-4" (if use_label=True) + "<@M:gpt-4.1|GPT-4>" -> "gpt-4.1" (if use_label=False) + """ + # Escape triggerChar + triggerChar = re.escape(triggerChar) + + def replacer(match): + id_type, id_value, label = match.groups() + return label if use_label and label else id_value + + # Regex captures: idType, id, optional label + pattern = rf'<{triggerChar}([A-Z]):([^|>]+)(?:\|([^>]+))?>' + return re.sub(pattern, replacer, message) diff --git a/backend/open_webui/utils/chat.py b/backend/open_webui/utils/chat.py new file mode 100644 index 0000000000000000000000000000000000000000..9899469da96da73dfc89e9023840adf8e7a2ce56 --- /dev/null +++ b/backend/open_webui/utils/chat.py @@ -0,0 +1,369 @@ +import time +import logging +import sys + +from aiocache import cached +from typing import Any, Optional +import random +import json + +import uuid +import asyncio + +from fastapi import HTTPException, Request, status +from starlette.responses import Response, StreamingResponse, JSONResponse + + +from open_webui.models.users import UserModel + +from open_webui.socket.main import ( + sio, + get_event_call, + get_event_emitter, +) +from open_webui.functions import generate_function_chat_completion + +from open_webui.routers.openai import ( + generate_chat_completion as generate_openai_chat_completion, +) + +from open_webui.routers.ollama import ( + generate_chat_completion as generate_ollama_chat_completion, +) + +from open_webui.routers.pipelines import ( + process_pipeline_inlet_filter, + process_pipeline_outlet_filter, +) + +from open_webui.models.functions import Functions +from open_webui.models.models import Models + +from open_webui.utils.models import get_all_models, check_model_access +from open_webui.utils.payload import convert_payload_openai_to_ollama +from open_webui.utils.response import ( + convert_response_ollama_to_openai, + convert_streaming_response_ollama_to_openai, +) +from open_webui.utils.filter import ( + get_sorted_filter_ids, + process_filter_functions, +) + +from open_webui.env import GLOBAL_LOG_LEVEL, BYPASS_MODEL_ACCESS_CONTROL + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) +log = logging.getLogger(__name__) + + +# When the question has been asked, let silence not be the +# answer. But if the answer must wait, let it come honest. +async def generate_direct_chat_completion( + request: Request, + form_data: dict, + user: Any, + models: dict, +): + log.info('generate_direct_chat_completion') + + metadata = form_data.pop('metadata', {}) + + user_id = metadata.get('user_id') + session_id = metadata.get('session_id') + request_id = str(uuid.uuid4()) # Generate a unique request ID + + event_caller = await get_event_call(metadata) + + channel = f'{user_id}:{session_id}:{request_id}' + logging.info(f'WebSocket channel: {channel}') + + if form_data.get('stream'): + q = asyncio.Queue() + + async def message_listener(sid, data): + """ + Handle received socket messages and push them into the queue. + """ + await q.put(data) + + # Register the listener + sio.on(channel, message_listener) + + # Start processing chat completion in background + res = await event_caller( + { + 'type': 'request:chat:completion', + 'data': { + 'form_data': form_data, + 'model': models[form_data['model']], + 'channel': channel, + 'session_id': session_id, + }, + } + ) + + log.info(f'res: {res}') + + if res.get('status', False): + # Define a generator to stream responses + async def event_generator(): + nonlocal q + try: + while True: + data = await q.get() # Wait for new messages + if isinstance(data, dict): + if 'done' in data and data['done']: + break # Stop streaming when 'done' is received + + yield f'data: {json.dumps(data)}\n\n' + elif isinstance(data, str): + if 'data:' in data: + yield f'{data}\n\n' + else: + yield f'data: {data}\n\n' + except Exception as e: + log.debug(f'Error in event generator: {e}') + pass + + # Define a background task to run the event generator + async def background(): + try: + del sio.handlers['/'][channel] + except Exception as e: + pass + + # Return the streaming response + return StreamingResponse(event_generator(), media_type='text/event-stream', background=background) + else: + raise Exception(str(res)) + else: + res = await event_caller( + { + 'type': 'request:chat:completion', + 'data': { + 'form_data': form_data, + 'model': models[form_data['model']], + 'channel': channel, + 'session_id': session_id, + }, + } + ) + + if 'error' in res and res['error']: + raise Exception(res['error']) + + return res + + +async def generate_chat_completion( + request: Request, + form_data: dict, + user: Any, + bypass_filter: bool = False, + bypass_system_prompt: bool = False, +): + log.debug(f'generate_chat_completion: {form_data}') + if BYPASS_MODEL_ACCESS_CONTROL: + bypass_filter = True + + # Propagate bypass_filter via request.state so that downstream route + # handlers (openai/ollama) can read it without exposing it as a query param. + request.state.bypass_filter = bypass_filter + + if hasattr(request.state, 'metadata'): + if 'metadata' not in form_data: + form_data['metadata'] = request.state.metadata + else: + form_data['metadata'] = { + **form_data['metadata'], + **request.state.metadata, + } + + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): + models = { + request.state.model['id']: request.state.model, + } + log.debug(f'direct connection to model: {models}') + else: + models = request.app.state.MODELS + + model_id = form_data['model'] + if model_id not in models: + raise Exception('Model not found') + + model = models[model_id] + + if getattr(request.state, 'direct', False): + return await generate_direct_chat_completion(request, form_data, user=user, models=models) + else: + # Check if user has access to the model + if not bypass_filter and user.role == 'user': + try: + await check_model_access(user, model) + except Exception as e: + raise e + + # Arena model — sub-model was already resolved by process_chat_payload. + # Inject selected_model_id into the response for the frontend. + metadata = form_data.get('metadata', {}) + selected_model_id = metadata.pop('selected_model_id', None) + # Also clear from request.state.metadata to prevent the merge at + # lines 177-179 from re-adding it on the recursive call. + if hasattr(request.state, 'metadata'): + request.state.metadata.pop('selected_model_id', None) + + # Fallback: if generate_chat_completion is called with an arena model + # from a path that did NOT go through process_chat_payload (e.g., + # background tasks for title/follow-up/tags generation), resolve now. + if not selected_model_id and model.get('owned_by') == 'arena': + model_ids = model.get('info', {}).get('meta', {}).get('model_ids') + filter_mode = model.get('info', {}).get('meta', {}).get('filter_mode') + if model_ids and filter_mode == 'exclude': + model_ids = [ + available_model['id'] + for available_model in list(request.app.state.MODELS.values()) + if available_model.get('owned_by') != 'arena' and available_model['id'] not in model_ids + ] + + if isinstance(model_ids, list) and model_ids: + selected_model_id = random.choice(model_ids) + else: + model_ids = [ + available_model['id'] + for available_model in list(request.app.state.MODELS.values()) + if available_model.get('owned_by') != 'arena' + ] + selected_model_id = random.choice(model_ids) + + form_data['model'] = selected_model_id + + if selected_model_id: + if form_data.get('stream') == True: + + async def stream_wrapper(stream): + yield f'data: {json.dumps({"selected_model_id": selected_model_id})}\n\n' + async for chunk in stream: + yield chunk + + response = await generate_chat_completion( + request, + form_data, + user, + bypass_filter=True, + bypass_system_prompt=bypass_system_prompt, + ) + return StreamingResponse( + stream_wrapper(response.body_iterator), + media_type='text/event-stream', + background=response.background, + ) + else: + return { + **( + await generate_chat_completion( + request, + form_data, + user, + bypass_filter=True, + bypass_system_prompt=bypass_system_prompt, + ) + ), + 'selected_model_id': selected_model_id, + } + + if model.get('pipe'): + # Below does not require bypass_filter because this is the only route the uses this function and it is already bypassing the filter + return await generate_function_chat_completion(request, form_data, user=user, models=models) + if model.get('owned_by') == 'ollama': + # Using /ollama/api/chat endpoint + form_data = convert_payload_openai_to_ollama(form_data) + response = await generate_ollama_chat_completion( + request=request, + form_data=form_data, + user=user, + bypass_system_prompt=bypass_system_prompt, + ) + if form_data.get('stream'): + response.headers['content-type'] = 'text/event-stream' + return StreamingResponse( + convert_streaming_response_ollama_to_openai(response), + headers=dict(response.headers), + background=response.background, + ) + else: + return convert_response_ollama_to_openai(response) + else: + return await generate_openai_chat_completion( + request=request, + form_data=form_data, + user=user, + bypass_system_prompt=bypass_system_prompt, + ) + + +chat_completion = generate_chat_completion + + +async def chat_completed(request: Request, form_data: dict, user: Any): + if not request.app.state.MODELS: + await get_all_models(request, user=user) + + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): + models = { + request.state.model['id']: request.state.model, + } + else: + models = request.app.state.MODELS + + data = form_data + + if not data.get('id'): + raise Exception('Missing message id') + + model_id = data['model'] + if model_id not in models: + raise Exception('Model not found') + + model = models[model_id] + + try: + data = await process_pipeline_outlet_filter(request, data, user, models) + except HTTPException: + raise + except Exception as e: + raise Exception(f'Error: {e}') + + if not data.get('id'): + raise Exception('Missing message id') + + metadata = { + 'chat_id': data['chat_id'], + 'message_id': data['id'], + 'filter_ids': data.get('filter_ids', []), + 'session_id': data['session_id'], + 'user_id': user.id, + } + + extra_params = { + '__event_emitter__': await get_event_emitter(metadata), + '__event_call__': await get_event_call(metadata), + '__user__': user.model_dump() if isinstance(user, UserModel) else {}, + '__metadata__': metadata, + '__request__': request, + '__model__': model, + } + + try: + filter_ids = await get_sorted_filter_ids(request, model, metadata.get('filter_ids', [])) + filter_functions = await Functions.get_functions_by_ids(filter_ids) + + result, _ = await process_filter_functions( + request=request, + filter_functions=filter_functions, + filter_type='outlet', + form_data=data, + extra_params=extra_params, + ) + return result + except Exception as e: + raise Exception(f'Error: {e}') diff --git a/backend/open_webui/utils/code_interpreter.py b/backend/open_webui/utils/code_interpreter.py new file mode 100644 index 0000000000000000000000000000000000000000..3e30c419ae02735787c677796b2b990343ba3cf4 --- /dev/null +++ b/backend/open_webui/utils/code_interpreter.py @@ -0,0 +1,196 @@ +import asyncio +import json +import logging +import uuid +from typing import Optional + +import aiohttp +import websockets +from pydantic import BaseModel + +logger = logging.getLogger(__name__) + + +class ResultModel(BaseModel): + """ + Execute Code Result Model + """ + + stdout: Optional[str] = '' + stderr: Optional[str] = '' + result: Optional[str] = '' + + +class JupyterCodeExecuter: + """ + Execute code in jupyter notebook + """ + + def __init__( + self, + base_url: str, + code: str, + token: str = '', + password: str = '', + timeout: int = 60, + ): + """ + :param base_url: Jupyter server URL (e.g., "http://localhost:8888") + :param code: Code to execute + :param token: Jupyter authentication token (optional) + :param password: Jupyter password (optional) + :param timeout: WebSocket timeout in seconds (default: 60s) + """ + self.base_url = base_url + self.code = code + self.token = token + self.password = password + self.timeout = timeout + self.kernel_id = '' + if self.base_url[-1] != '/': + self.base_url += '/' + self.session = aiohttp.ClientSession(trust_env=True, base_url=self.base_url) + self.params = {} + self.result = ResultModel() + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self.kernel_id: + try: + async with self.session.delete(f'api/kernels/{self.kernel_id}', params=self.params) as response: + response.raise_for_status() + except Exception as err: + logger.exception('close kernel failed, %s', err) + await self.session.close() + + async def run(self) -> ResultModel: + try: + await self.sign_in() + await self.init_kernel() + await self.execute_code() + except Exception as err: + logger.exception('execute code failed, %s', err) + self.result.stderr = f'Error: {err}' + return self.result + + async def sign_in(self) -> None: + # password authentication + if self.password and not self.token: + async with self.session.get('login') as response: + response.raise_for_status() + xsrf_token = response.cookies['_xsrf'].value + if not xsrf_token: + raise ValueError('_xsrf token not found') + self.session.cookie_jar.update_cookies(response.cookies) + self.session.headers.update({'X-XSRFToken': xsrf_token}) + async with self.session.post( + 'login', + data={'_xsrf': xsrf_token, 'password': self.password}, + allow_redirects=False, + ) as response: + response.raise_for_status() + self.session.cookie_jar.update_cookies(response.cookies) + + # token authentication + if self.token: + self.params.update({'token': self.token}) + + async def init_kernel(self) -> None: + async with self.session.post(url='api/kernels', params=self.params) as response: + response.raise_for_status() + kernel_data = await response.json() + self.kernel_id = kernel_data['id'] + + def init_ws(self) -> (str, dict): + ws_base = self.base_url.replace('http', 'ws', 1) + ws_params = '?' + '&'.join([f'{key}={val}' for key, val in self.params.items()]) + websocket_url = f'{ws_base}api/kernels/{self.kernel_id}/channels{ws_params if len(ws_params) > 1 else ""}' + ws_headers = {} + if self.password and not self.token: + ws_headers = { + 'Cookie': '; '.join([f'{cookie.key}={cookie.value}' for cookie in self.session.cookie_jar]), + **self.session.headers, + } + return websocket_url, ws_headers + + async def execute_code(self) -> None: + # initialize ws + websocket_url, ws_headers = self.init_ws() + # execute + async with websockets.connect(websocket_url, additional_headers=ws_headers) as ws: + await self.execute_in_jupyter(ws) + + async def execute_in_jupyter(self, ws) -> None: + # send message + msg_id = uuid.uuid4().hex + await ws.send( + json.dumps( + { + 'header': { + 'msg_id': msg_id, + 'msg_type': 'execute_request', + 'username': 'user', + 'session': uuid.uuid4().hex, + 'date': '', + 'version': '5.3', + }, + 'parent_header': {}, + 'metadata': {}, + 'content': { + 'code': self.code, + 'silent': False, + 'store_history': True, + 'user_expressions': {}, + 'allow_stdin': False, + 'stop_on_error': True, + }, + 'channel': 'shell', + } + ) + ) + # parse message + stdout, stderr, result = '', '', [] + while True: + try: + # wait for message + message = await asyncio.wait_for(ws.recv(), self.timeout) + message_data = json.loads(message) + # msg id not match, skip + if message_data.get('parent_header', {}).get('msg_id') != msg_id: + continue + # check message type + msg_type = message_data.get('msg_type') + match msg_type: + case 'stream': + if message_data['content']['name'] == 'stdout': + stdout += message_data['content']['text'] + elif message_data['content']['name'] == 'stderr': + stderr += message_data['content']['text'] + case 'execute_result' | 'display_data': + data = message_data['content']['data'] + if 'image/png' in data: + result.append(f'data:image/png;base64,{data["image/png"]}') + elif 'text/plain' in data: + result.append(data['text/plain']) + case 'error': + stderr += '\n'.join(message_data['content']['traceback']) + case 'status': + if message_data['content']['execution_state'] == 'idle': + break + + except asyncio.TimeoutError: + stderr += '\nExecution timed out.' + break + self.result.stdout = stdout.strip() + self.result.stderr = stderr.strip() + self.result.result = '\n'.join(result).strip() if result else '' + + +async def execute_code_jupyter( + base_url: str, code: str, token: str = '', password: str = '', timeout: int = 60 +) -> dict: + async with JupyterCodeExecuter(base_url, code, token, password, timeout) as executor: + result = await executor.run() + return result.model_dump() diff --git a/backend/open_webui/utils/embeddings.py b/backend/open_webui/utils/embeddings.py new file mode 100644 index 0000000000000000000000000000000000000000..1717886326cd32bd29e5d2aa87b1812262d82f95 --- /dev/null +++ b/backend/open_webui/utils/embeddings.py @@ -0,0 +1,88 @@ +import random +import logging +import sys + +from fastapi import Request +from open_webui.models.users import UserModel +from open_webui.models.models import Models +from open_webui.utils.models import check_model_access +from open_webui.env import GLOBAL_LOG_LEVEL, BYPASS_MODEL_ACCESS_CONTROL + +from open_webui.routers.openai import embeddings as openai_embeddings +from open_webui.routers.ollama import ( + embed as ollama_embed, + GenerateEmbedForm, +) + +from open_webui.utils.payload import convert_embed_payload_openai_to_ollama +from open_webui.utils.response import convert_embedding_response_ollama_to_openai + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) +log = logging.getLogger(__name__) + + +async def generate_embeddings( + request: Request, + form_data: dict, + user: UserModel, + bypass_filter: bool = False, +): + """ + Dispatch and handle embeddings generation based on the model type (OpenAI, Ollama). + + Args: + request (Request): The FastAPI request context. + form_data (dict): The input data sent to the endpoint. + user (UserModel): The authenticated user. + bypass_filter (bool): If True, disables access filtering (default False). + + Returns: + dict: The embeddings response, following OpenAI API compatibility. + """ + if BYPASS_MODEL_ACCESS_CONTROL: + bypass_filter = True + + # Attach extra metadata from request.state if present + if hasattr(request.state, 'metadata'): + if 'metadata' not in form_data: + form_data['metadata'] = request.state.metadata + else: + form_data['metadata'] = { + **form_data['metadata'], + **request.state.metadata, + } + + # If "direct" flag present, use only that model + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): + models = { + request.state.model['id']: request.state.model, + } + else: + models = request.app.state.MODELS + + model_id = form_data.get('model') + if model_id not in models: + raise Exception('Model not found') + model = models[model_id] + + # Access filtering + if not getattr(request.state, 'direct', False): + if not bypass_filter and user.role == 'user': + await check_model_access(user, model) + + # Ollama backend — use /api/embed which supports batch input natively + if model.get('owned_by') == 'ollama': + ollama_payload = convert_embed_payload_openai_to_ollama(form_data) + response = await ollama_embed( + request=request, + form_data=GenerateEmbedForm(**ollama_payload), + user=user, + ) + return convert_embedding_response_ollama_to_openai(response) + + # Default: OpenAI or compatible backend + return await openai_embeddings( + request=request, + form_data=form_data, + user=user, + ) diff --git a/backend/open_webui/utils/files.py b/backend/open_webui/utils/files.py new file mode 100644 index 0000000000000000000000000000000000000000..8149987fe4a5e92b8618525dd8770571ee078ed6 --- /dev/null +++ b/backend/open_webui/utils/files.py @@ -0,0 +1,213 @@ +from open_webui.routers.images import ( + get_image_data, + upload_image, +) + +from fastapi import ( + APIRouter, + Depends, + HTTPException, + Request, + UploadFile, +) +from typing import Optional +from pathlib import Path + +from open_webui.storage.provider import Storage + +from open_webui.models.chats import Chats +from open_webui.models.files import Files +from open_webui.routers.files import upload_file_handler +from open_webui.retrieval.web.utils import validate_url + +import asyncio +import mimetypes +import base64 +import io +import re + +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK +from open_webui.utils.session_pool import get_session + +BASE64_IMAGE_URL_PREFIX = re.compile(r'data:image/\w+;base64,', re.IGNORECASE) +MARKDOWN_IMAGE_URL_PATTERN = re.compile(r'!\[(.*?)\]\((.+?)\)', re.IGNORECASE) + +# Extension-based MIME fallback, only used when ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK is True. +_IMAGE_MIME_FALLBACK = { + '.webp': 'image/webp', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.bmp': 'image/bmp', + '.tiff': 'image/tiff', + '.tif': 'image/tiff', + '.ico': 'image/x-icon', + '.heic': 'image/heic', + '.heif': 'image/heif', + '.avif': 'image/avif', +} + + +async def get_image_base64_from_url(url: str) -> Optional[str]: + try: + if url.startswith('http'): + # Validate URL to prevent SSRF attacks against local/private networks + validate_url(url) + # Download the image from the URL + session = await get_session() + async with session.get(url, ssl=AIOHTTP_CLIENT_SESSION_SSL) as response: + response.raise_for_status() + image_data = await response.read() + encoded_string = base64.b64encode(image_data).decode('utf-8') + content_type = response.headers.get('Content-Type', 'image/png') + return f'data:{content_type};base64,{encoded_string}' + else: + file = await Files.get_file_by_id(url) + + if not file: + return None + + file_path = await asyncio.to_thread(Storage.get_file, file.path) + file_path = Path(file_path) + + if file_path.is_file(): + with open(file_path, 'rb') as image_file: + encoded_string = base64.b64encode(image_file.read()).decode('utf-8') + content_type = mimetypes.guess_type(file_path.name)[0] or (file.meta or {}).get('content_type') + if not content_type and ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK: + content_type = _IMAGE_MIME_FALLBACK.get(file_path.suffix.lower()) + if not content_type: + return None + return f'data:{content_type};base64,{encoded_string}' + else: + return None + + except Exception as e: + return None + + +async def get_image_url_from_base64(request, base64_image_string, metadata, user): + if BASE64_IMAGE_URL_PREFIX.match(base64_image_string): + image_url = '' + # Extract base64 image data from the line + image_data, content_type = await get_image_data(base64_image_string) + if image_data is not None: + _, image_url = await upload_image( + request, + image_data, + content_type, + metadata, + user, + ) + + return image_url + return None + + +async def convert_markdown_base64_images(request, content: str, metadata, user): + MIN_REPLACEMENT_URL_LENGTH = 1024 + result_parts = [] + last_end = 0 + + for match in MARKDOWN_IMAGE_URL_PATTERN.finditer(content): + result_parts.append(content[last_end : match.start()]) + base64_string = match.group(2) + if len(base64_string) > MIN_REPLACEMENT_URL_LENGTH: + url = await get_image_url_from_base64(request, base64_string, metadata, user) + if url: + result_parts.append(f'![{match.group(1)}]({url})') + else: + result_parts.append(match.group(0)) + else: + result_parts.append(match.group(0)) + last_end = match.end() + + result_parts.append(content[last_end:]) + return ''.join(result_parts) + + +def load_b64_audio_data(b64_str): + try: + if ',' in b64_str: + header, b64_data = b64_str.split(',', 1) + else: + b64_data = b64_str + header = 'data:audio/wav;base64' + audio_data = base64.b64decode(b64_data) + content_type = header.split(';')[0].split(':')[1] if ';' in header else 'audio/wav' + return audio_data, content_type + except Exception as e: + print(f'Error decoding base64 audio data: {e}') + return None, None + + +async def upload_audio(request, audio_data, content_type, metadata, user): + audio_format = mimetypes.guess_extension(content_type) + file = UploadFile( + file=io.BytesIO(audio_data), + filename=f'generated-{audio_format}', # will be converted to a unique ID on upload_file + headers={ + 'content-type': content_type, + }, + ) + file_item = await upload_file_handler( + request, + file=file, + metadata=metadata, + process=False, + user=user, + ) + url = request.app.url_path_for('get_file_content_by_id', id=file_item.id) + return url + + +async def get_audio_url_from_base64(request, base64_audio_string, metadata, user): + if 'data:audio/wav;base64' in base64_audio_string: + audio_url = '' + # Extract base64 audio data from the line + audio_data, content_type = load_b64_audio_data(base64_audio_string) + if audio_data is not None: + audio_url = await upload_audio( + request, + audio_data, + content_type, + metadata, + user, + ) + return audio_url + return None + + +async def get_file_url_from_base64(request, base64_file_string, metadata, user): + if BASE64_IMAGE_URL_PREFIX.match(base64_file_string): + return await get_image_url_from_base64(request, base64_file_string, metadata, user) + elif 'data:audio/wav;base64' in base64_file_string: + return await get_audio_url_from_base64(request, base64_file_string, metadata, user) + return None + + +async def get_image_base64_from_file_id(id: str) -> Optional[str]: + file = await Files.get_file_by_id(id) + if not file: + return None + + try: + file_path = await asyncio.to_thread(Storage.get_file, file.path) + file_path = Path(file_path) + + # Check if the file already exists in the cache + if file_path.is_file(): + with open(file_path, 'rb') as image_file: + encoded_string = base64.b64encode(image_file.read()).decode('utf-8') + content_type = mimetypes.guess_type(file_path.name)[0] or (file.meta or {}).get('content_type') + if not content_type and ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK: + content_type = _IMAGE_MIME_FALLBACK.get(file_path.suffix.lower()) + if not content_type: + return None + return f'data:{content_type};base64,{encoded_string}' + else: + return None + except Exception as e: + return None diff --git a/backend/open_webui/utils/filter.py b/backend/open_webui/utils/filter.py new file mode 100644 index 0000000000000000000000000000000000000000..07edf9afa7ca9026624282c984123c4eda243beb --- /dev/null +++ b/backend/open_webui/utils/filter.py @@ -0,0 +1,134 @@ +import inspect +import logging + +from open_webui.utils.plugin import ( + load_function_module_by_id, + get_function_module_from_cache, +) +from open_webui.models.functions import Functions + +log = logging.getLogger(__name__) + + +async def get_function_module(request, function_id, load_from_db=True): + """ + Get the function module by its ID. + """ + function_module, _, _ = await get_function_module_from_cache(request, function_id, load_from_db=load_from_db) + return function_module + + +async def get_sorted_filter_ids(request, model: dict, enabled_filter_ids: list = None): + async def get_priority(function_id): + try: + function_module = await get_function_module(request, function_id) + if function_module and hasattr(function_module, 'Valves'): + valves_db = await Functions.get_function_valves_by_id(function_id) + valves = function_module.Valves(**(valves_db if valves_db else {})) + return getattr(valves, 'priority', 0) + except Exception: + pass + return 0 + + filter_ids = [function.id for function in await Functions.get_global_filter_functions()] + if 'info' in model and 'meta' in model['info']: + filter_ids.extend(model['info']['meta'].get('filterIds', [])) + filter_ids = list(set(filter_ids)) + active_filter_ids = {function.id for function in await Functions.get_functions_by_type('filter', active_only=True)} + + async def get_active_status(filter_id): + function_module = await get_function_module(request, filter_id) + + if getattr(function_module, 'toggle', None): + return filter_id in (enabled_filter_ids or set()) + + return True + + # Pre-compute active status for each filter (async functions can't be used in set comprehensions) + resolved_active = {} + for filter_id in active_filter_ids: + resolved_active[filter_id] = await get_active_status(filter_id) + active_filter_ids = {fid for fid, is_active in resolved_active.items() if is_active} + + filter_ids = [fid for fid in filter_ids if fid in active_filter_ids] + + # Pre-compute priorities (async functions can't be used in sort keys) + priorities = {} + for fid in filter_ids: + priorities[fid] = await get_priority(fid) + filter_ids.sort(key=lambda fid: (priorities.get(fid, 0), fid)) + + return filter_ids + + +# Grant these filters the discernment to pass what serves +# and refuse what harms, for every soul in the house. +async def process_filter_functions(request, filter_functions, filter_type, form_data, extra_params): + skip_files = None + + for function in filter_functions: + filter = function + filter_id = function.id + if not filter: + continue + + function_module = await get_function_module(request, filter_id, load_from_db=(filter_type != 'stream')) + # Prepare handler function + handler = getattr(function_module, filter_type, None) + if not handler: + continue + + # Check if the function has a file_handler variable + if filter_type == 'inlet' and hasattr(function_module, 'file_handler'): + skip_files = function_module.file_handler + + # Apply valves to the function + if hasattr(function_module, 'valves') and hasattr(function_module, 'Valves'): + valves = await Functions.get_function_valves_by_id(filter_id) + function_module.valves = function_module.Valves(**(valves if valves else {})) + + try: + # Prepare parameters + sig = inspect.signature(handler) + + params = {'body': form_data} + if filter_type == 'stream': + params = {'event': form_data} + + params = params | { + k: v + for k, v in { + **extra_params, + '__id__': filter_id, + }.items() + if k in sig.parameters + } + + # Handle user parameters + if '__user__' in sig.parameters: + if hasattr(function_module, 'UserValves'): + try: + params['__user__']['valves'] = function_module.UserValves( + **await Functions.get_user_valves_by_id_and_user_id(filter_id, params['__user__']['id']) + ) + except Exception as e: + log.exception(f'Failed to get user values: {e}') + + # Execute handler + if inspect.iscoroutinefunction(handler): + form_data = await handler(**params) + else: + form_data = handler(**params) + + except Exception as e: + log.debug(f'Error in {filter_type} handler {filter_id}: {e}') + raise e + + # Handle file cleanup for inlet + if skip_files: + if 'files' in form_data.get('metadata', {}): + del form_data['metadata']['files'] + if 'files' in form_data: + del form_data['files'] + + return form_data, {} diff --git a/backend/open_webui/utils/groups.py b/backend/open_webui/utils/groups.py new file mode 100644 index 0000000000000000000000000000000000000000..50099b2ee7909bc96637cef9aead4220a5c96da7 --- /dev/null +++ b/backend/open_webui/utils/groups.py @@ -0,0 +1,23 @@ +import logging +from open_webui.models.groups import Groups + +log = logging.getLogger(__name__) + + +async def apply_default_group_assignment( + default_group_id: str, + user_id: str, + db=None, +) -> None: + """ + Apply default group assignment to a user if default_group_id is provided. + + Args: + default_group_id: ID of the default group to add the user to + user_id: ID of the user to add to the default group + """ + if default_group_id: + try: + await Groups.add_users_to_group(default_group_id, [user_id], db=db) + except Exception as e: + log.error(f'Failed to add user {user_id} to default group {default_group_id}: {e}') diff --git a/backend/open_webui/utils/headers.py b/backend/open_webui/utils/headers.py new file mode 100644 index 0000000000000000000000000000000000000000..0baee5edb903536164126242727ff39407305bea --- /dev/null +++ b/backend/open_webui/utils/headers.py @@ -0,0 +1,18 @@ +from urllib.parse import quote + +from open_webui.env import ( + FORWARD_USER_INFO_HEADER_USER_NAME, + FORWARD_USER_INFO_HEADER_USER_ID, + FORWARD_USER_INFO_HEADER_USER_EMAIL, + FORWARD_USER_INFO_HEADER_USER_ROLE, +) + + +def include_user_info_headers(headers, user): + return { + **headers, + FORWARD_USER_INFO_HEADER_USER_NAME: quote(user.name, safe=' '), + FORWARD_USER_INFO_HEADER_USER_ID: user.id, + FORWARD_USER_INFO_HEADER_USER_EMAIL: user.email, + FORWARD_USER_INFO_HEADER_USER_ROLE: user.role, + } diff --git a/backend/open_webui/utils/images/comfyui.py b/backend/open_webui/utils/images/comfyui.py new file mode 100644 index 0000000000000000000000000000000000000000..9172f1c32504df883d3859bd2882efc7bceb114b --- /dev/null +++ b/backend/open_webui/utils/images/comfyui.py @@ -0,0 +1,256 @@ +import json +import logging +import random +import urllib.parse +from typing import Optional + +import aiohttp +from pydantic import BaseModel + +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL +from open_webui.utils.session_pool import get_session + +log = logging.getLogger(__name__) + +default_headers = {'User-Agent': 'Mozilla/5.0'} + + +async def queue_prompt(prompt, client_id, base_url, api_key): + log.info('queue_prompt') + p = {'prompt': prompt, 'client_id': client_id} + log.debug(f'queue_prompt data: {p}') + try: + session = await get_session() + async with session.post( + f'{base_url}/prompt', + json=p, + headers={**default_headers, 'Authorization': f'Bearer {api_key}'}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + return await r.json() + except Exception as e: + log.exception(f'Error while queuing prompt: {e}') + raise + + +async def get_image(filename, subfolder, folder_type, base_url, api_key): + log.info('get_image') + data = {'filename': filename, 'subfolder': subfolder, 'type': folder_type} + url_values = urllib.parse.urlencode(data) + session = await get_session() + async with session.get( + f'{base_url}/view?{url_values}', + headers={**default_headers, 'Authorization': f'Bearer {api_key}'}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + return await r.read() + + +def get_image_url(filename, subfolder, folder_type, base_url): + log.info('get_image') + data = {'filename': filename, 'subfolder': subfolder, 'type': folder_type} + url_values = urllib.parse.urlencode(data) + return f'{base_url}/view?{url_values}' + + +async def get_history(prompt_id, base_url, api_key): + log.info('get_history') + session = await get_session() + async with session.get( + f'{base_url}/history/{prompt_id}', + headers={**default_headers, 'Authorization': f'Bearer {api_key}'}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + return await r.json() + + +async def _ws_get_images(ws, workflow, client_id, base_url, api_key): + """Queue a prompt and wait on *ws* for ComfyUI to finish executing it. + + Returns a dict of ``{'data': [{'url': ...}, ...]}``. + """ + prompt_id = (await queue_prompt(workflow, client_id, base_url, api_key))['prompt_id'] + output_images = [] + + async for msg in ws: + if msg.type == aiohttp.WSMsgType.TEXT: + message = json.loads(msg.data) + if message['type'] == 'executing': + data = message['data'] + if data['node'] is None and data['prompt_id'] == prompt_id: + break # Execution is done + elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR): + log.error(f'WebSocket closed unexpectedly: {msg.type}') + break + # binary messages (previews) are silently skipped + + history = (await get_history(prompt_id, base_url, api_key))[prompt_id] + for node_id in history['outputs']: + node_output = history['outputs'][node_id] + if node_id in workflow and workflow[node_id].get('class_type') in [ + 'SaveImage', + 'PreviewImage', + ]: + if 'images' in node_output: + for image in node_output['images']: + url = get_image_url(image['filename'], image['subfolder'], image['type'], base_url) + output_images.append({'url': url}) + return {'data': output_images} + + +async def comfyui_upload_image(image_file_item, base_url, api_key): + url = f'{base_url}/api/upload/image' + headers = {} + + if api_key: + headers['Authorization'] = f'Bearer {api_key}' + + _, (filename, file_bytes, mime_type) = image_file_item + + form = aiohttp.FormData() + form.add_field('image', file_bytes, filename=filename, content_type=mime_type) + form.add_field('type', 'input') # required by ComfyUI + + session = await get_session() + async with session.post(url, data=form, headers=headers, ssl=AIOHTTP_CLIENT_SESSION_SSL) as resp: + resp.raise_for_status() + return await resp.json() + + +class ComfyUINodeInput(BaseModel): + type: Optional[str] = None + node_ids: list[str] = [] + key: Optional[str] = 'text' + value: Optional[str] = None + + +class ComfyUIWorkflow(BaseModel): + workflow: str + nodes: list[ComfyUINodeInput] + + +class ComfyUICreateImageForm(BaseModel): + workflow: ComfyUIWorkflow + + prompt: str + negative_prompt: Optional[str] = None + width: int + height: int + n: int = 1 + + steps: Optional[int] = None + seed: Optional[int] = None + + +def _apply_workflow_nodes(workflow, nodes, model, payload): + """Mutate *workflow* dict in-place based on typed node definitions.""" + for node in nodes: + if node.type: + if node.type == 'model': + for node_id in node.node_ids: + workflow[node_id]['inputs'][node.key] = model + elif node.type == 'prompt': + for node_id in node.node_ids: + workflow[node_id]['inputs'][node.key if node.key else 'text'] = payload.prompt + elif node.type == 'negative_prompt': + for node_id in node.node_ids: + workflow[node_id]['inputs'][node.key if node.key else 'text'] = payload.negative_prompt + elif node.type == 'image': + if isinstance(payload.image, list): + for idx, node_id in enumerate(node.node_ids): + if idx < len(payload.image): + workflow[node_id]['inputs'][node.key] = payload.image[idx] + else: + for node_id in node.node_ids: + workflow[node_id]['inputs'][node.key] = payload.image + elif node.type == 'width': + for node_id in node.node_ids: + workflow[node_id]['inputs'][node.key if node.key else 'width'] = payload.width + elif node.type == 'height': + for node_id in node.node_ids: + workflow[node_id]['inputs'][node.key if node.key else 'height'] = payload.height + elif node.type == 'n': + for node_id in node.node_ids: + workflow[node_id]['inputs'][node.key if node.key else 'batch_size'] = payload.n + elif node.type == 'steps': + for node_id in node.node_ids: + workflow[node_id]['inputs'][node.key if node.key else 'steps'] = payload.steps + elif node.type == 'seed': + seed = payload.seed if payload.seed else random.randint(0, 1125899906842624) + for node_id in node.node_ids: + workflow[node_id]['inputs'][node.key] = seed + else: + for node_id in node.node_ids: + workflow[node_id]['inputs'][node.key] = node.value + + +async def comfyui_create_image(model: str, payload: ComfyUICreateImageForm, client_id, base_url, api_key): + ws_url = base_url.replace('http://', 'ws://').replace('https://', 'wss://') + workflow = json.loads(payload.workflow.workflow) + _apply_workflow_nodes(workflow, payload.workflow.nodes, model, payload) + + headers = {'Authorization': f'Bearer {api_key}'} + session = await get_session() + + try: + async with session.ws_connect( + f'{ws_url}/ws?clientId={client_id}', + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as ws: + log.info('WebSocket connection established.') + log.info('Sending workflow to WebSocket server.') + log.info(f'Workflow: {workflow}') + images = await _ws_get_images(ws, workflow, client_id, base_url, api_key) + except aiohttp.WSServerHandshakeError as e: + log.exception(f'Failed to connect to WebSocket server: {e}') + return None + except Exception as e: + log.exception(f'Error during image generation: {e}') + return None + + return images + + +class ComfyUIEditImageForm(BaseModel): + workflow: ComfyUIWorkflow + + image: str | list[str] + prompt: str + width: Optional[int] = None + height: Optional[int] = None + n: Optional[int] = None + + steps: Optional[int] = None + seed: Optional[int] = None + + +async def comfyui_edit_image(model: str, payload: ComfyUIEditImageForm, client_id, base_url, api_key): + ws_url = base_url.replace('http://', 'ws://').replace('https://', 'wss://') + workflow = json.loads(payload.workflow.workflow) + _apply_workflow_nodes(workflow, payload.workflow.nodes, model, payload) + + headers = {'Authorization': f'Bearer {api_key}'} + session = await get_session() + + try: + async with session.ws_connect( + f'{ws_url}/ws?clientId={client_id}', + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as ws: + log.info('WebSocket connection established.') + log.info('Sending workflow to WebSocket server.') + log.info(f'Workflow: {workflow}') + images = await _ws_get_images(ws, workflow, client_id, base_url, api_key) + except aiohttp.WSServerHandshakeError as e: + log.exception(f'Failed to connect to WebSocket server: {e}') + return None + except Exception as e: + log.exception(f'Error during image editing: {e}') + return None + + return images diff --git a/backend/open_webui/utils/logger.py b/backend/open_webui/utils/logger.py new file mode 100644 index 0000000000000000000000000000000000000000..fa4e77f53dba871042ad200a1a7a9459c04c2d69 --- /dev/null +++ b/backend/open_webui/utils/logger.py @@ -0,0 +1,194 @@ +import json +import logging +import sys +from typing import TYPE_CHECKING + +from loguru import logger + +from open_webui.env import ( + ENABLE_AUDIT_STDOUT, + ENABLE_AUDIT_LOGS_FILE, + AUDIT_LOGS_FILE_PATH, + AUDIT_LOG_FILE_ROTATION_SIZE, + AUDIT_LOG_LEVEL, + GLOBAL_LOG_LEVEL, + LOG_FORMAT, + AUDIT_UVICORN_LOGGER_NAMES, + ENABLE_OTEL, + ENABLE_OTEL_LOGS, + _LEVEL_MAP, +) + +if TYPE_CHECKING: + from loguru import Message, Record + + +def stdout_format(record: 'Record') -> str: + """ + Generates a formatted string for log records that are output to the console. This format includes a timestamp, log level, source location (module, function, and line), the log message, and any extra data (serialized as JSON). + + Parameters: + record (Record): A Loguru record that contains logging details including time, level, name, function, line, message, and any extra context. + Returns: + str: A formatted log string intended for stdout. + """ + if record['extra']: + record['extra']['extra_json'] = json.dumps(record['extra']) + extra_format = ' - {extra[extra_json]}' + else: + extra_format = '' + return ( + '{time:YYYY-MM-DD HH:mm:ss.SSS} | ' + '{level: <8} | ' + '{name}:{function}:{line} - ' + '{message}' + extra_format + '\n{exception}' + ) + + +def _json_sink(message: 'Message') -> None: + """Write log records as single-line JSON to stdout. + + Used as a Loguru sink when LOG_FORMAT is set to "json". + """ + record = message.record + log_entry = { + 'ts': record['time'].strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z', + 'level': _LEVEL_MAP.get(record['level'].name, record['level'].name.lower()), + 'msg': record['message'], + 'caller': f'{record["name"]}:{record["function"]}:{record["line"]}', + } + + if record['extra']: + log_entry['extra'] = record['extra'] + + if record['exception'] is not None: + log_entry['error'] = ''.join(record['exception'].format_exception()).rstrip() + + sys.stdout.write(json.dumps(log_entry, ensure_ascii=False, default=str) + '\n') + sys.stdout.flush() + + +class InterceptHandler(logging.Handler): + """ + Intercepts log records from Python's standard logging module + and redirects them to Loguru's logger. + """ + + def emit(self, record): + """ + Called by the standard logging module for each log event. + It transforms the standard `LogRecord` into a format compatible with Loguru + and passes it to Loguru's logger. + """ + try: + level = logger.level(record.levelname).name + except ValueError: + level = record.levelno + + frame, depth = sys._getframe(6), 6 + while frame and frame.f_code.co_filename == logging.__file__: + frame = frame.f_back + depth += 1 + + logger.opt(depth=depth, exception=record.exc_info).bind(**self._get_extras()).log(level, record.getMessage()) + if ENABLE_OTEL and ENABLE_OTEL_LOGS: + from open_webui.utils.telemetry.logs import otel_handler + + otel_handler.emit(record) + + def _get_extras(self): + if not ENABLE_OTEL: + return {} + + from opentelemetry import trace + + extras = {} + context = trace.get_current_span().get_span_context() + if context.is_valid: + extras['trace_id'] = trace.format_trace_id(context.trace_id) + extras['span_id'] = trace.format_span_id(context.span_id) + return extras + + +def file_format(record: 'Record'): + """ + Formats audit log records into a structured JSON string for file output. + + Parameters: + record (Record): A Loguru record containing extra audit data. + Returns: + str: A JSON-formatted string representing the audit data. + """ + + audit_data = { + 'id': record['extra'].get('id', ''), + 'timestamp': int(record['time'].timestamp()), + 'user': record['extra'].get('user', dict()), + 'audit_level': record['extra'].get('audit_level', ''), + 'verb': record['extra'].get('verb', ''), + 'request_uri': record['extra'].get('request_uri', ''), + 'response_status_code': record['extra'].get('response_status_code', 0), + 'source_ip': record['extra'].get('source_ip', ''), + 'user_agent': record['extra'].get('user_agent', ''), + 'request_object': record['extra'].get('request_object', b''), + 'response_object': record['extra'].get('response_object', b''), + 'extra': record['extra'].get('extra', {}), + } + + record['extra']['file_extra'] = json.dumps(audit_data, default=str) + return '{extra[file_extra]}\n' + + +def start_logger(): + """ + Initializes and configures Loguru's logger with distinct handlers: + + A console (stdout) handler for general log messages (excluding those marked as auditable). + An optional file handler for audit logs if audit logging is enabled. + Additionally, this function reconfigures Python’s standard logging to route through Loguru and adjusts logging levels for Uvicorn. + + Parameters: + enable_audit_logging (bool): Determines whether audit-specific log entries should be recorded to file. + """ + logger.remove() + + audit_filter = lambda record: True if ENABLE_AUDIT_STDOUT else 'auditable' not in record['extra'] + if LOG_FORMAT == 'json': + logger.add( + _json_sink, + level=GLOBAL_LOG_LEVEL, + filter=audit_filter, + ) + else: + logger.add( + sys.stdout, + level=GLOBAL_LOG_LEVEL, + format=stdout_format, + filter=audit_filter, + ) + if AUDIT_LOG_LEVEL != 'NONE' and ENABLE_AUDIT_LOGS_FILE: + try: + logger.add( + AUDIT_LOGS_FILE_PATH, + level='INFO', + rotation=AUDIT_LOG_FILE_ROTATION_SIZE, + compression='zip', + format=file_format, + filter=lambda record: record['extra'].get('auditable') is True, + ) + except Exception as e: + logger.error(f'Failed to initialize audit log file handler: {str(e)}') + + logging.basicConfig(handlers=[InterceptHandler()], level=GLOBAL_LOG_LEVEL, force=True) + + for uvicorn_logger_name in ['uvicorn', 'uvicorn.error']: + uvicorn_logger = logging.getLogger(uvicorn_logger_name) + uvicorn_logger.setLevel(GLOBAL_LOG_LEVEL) + uvicorn_logger.handlers = [] + + for uvicorn_logger_name in AUDIT_UVICORN_LOGGER_NAMES: + uvicorn_logger = logging.getLogger(uvicorn_logger_name) + uvicorn_logger.setLevel(GLOBAL_LOG_LEVEL) + uvicorn_logger.handlers = [InterceptHandler()] + + logger.info(f'GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}') diff --git a/backend/open_webui/utils/mcp/client.py b/backend/open_webui/utils/mcp/client.py new file mode 100644 index 0000000000000000000000000000000000000000..7a5aa61b8029bcc37cfbcd0f44a4b0df9c2c00b1 --- /dev/null +++ b/backend/open_webui/utils/mcp/client.py @@ -0,0 +1,180 @@ +import asyncio +import logging +from typing import Optional +from contextlib import AsyncExitStack + +log = logging.getLogger(__name__) + +import anyio + +from mcp import ClientSession +from mcp.client.auth import OAuthClientProvider, TokenStorage +from mcp.client.streamable_http import streamablehttp_client +from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken +import httpx +from open_webui.env import AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER + + +def _build_httpx_client(headers=None, timeout=None, auth=None, verify=True): + """Create an httpx AsyncClient for MCP transport. + + Falls back to AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER when the caller + (i.e. the MCP SDK) does not supply an explicit timeout. + + Note: verify must be passed at construction time because httpx + configures the SSL context during __init__. Setting client.verify = False + after construction does not affect the underlying transport's SSL context. + """ + kwargs = { + 'follow_redirects': True, + 'verify': verify, + } + if timeout is not None: + kwargs['timeout'] = timeout + elif AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER is not None: + kwargs['timeout'] = float(AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER) + if headers is not None: + kwargs['headers'] = headers + if auth is not None: + kwargs['auth'] = auth + return httpx.AsyncClient(**kwargs) + + +def create_httpx_client(headers=None, timeout=None, auth=None): + return _build_httpx_client(headers=headers, timeout=timeout, auth=auth, verify=True) + + +def create_insecure_httpx_client(headers=None, timeout=None, auth=None): + return _build_httpx_client(headers=headers, timeout=timeout, auth=auth, verify=False) + + +class MCPClient: + def __init__(self): + self.session: Optional[ClientSession] = None + self.exit_stack = None + + async def connect(self, url: str, headers: Optional[dict] = None): + async with AsyncExitStack() as exit_stack: + try: + self._streams_context = streamablehttp_client( + url, + headers=headers, + httpx_client_factory=create_httpx_client + if AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL + else create_insecure_httpx_client, + ) + + transport = await exit_stack.enter_async_context(self._streams_context) + read_stream, write_stream, _ = transport + + self._session_context = ClientSession(read_stream, write_stream) # pylint: disable=W0201 + + self.session = await exit_stack.enter_async_context(self._session_context) + with anyio.fail_after(10): + await self.session.initialize() + self.exit_stack = exit_stack.pop_all() + except Exception as e: + await asyncio.shield(self.disconnect()) + raise e + + async def list_tool_specs(self) -> Optional[dict]: + if not self.session: + raise RuntimeError('MCP client is not connected.') + + result = await self.session.list_tools() + tools = result.tools + + tool_specs = [] + for tool in tools: + name = tool.name + description = tool.description + + inputSchema = tool.inputSchema + + # TODO: handle outputSchema if needed + outputSchema = getattr(tool, 'outputSchema', None) + + tool_specs.append({'name': name, 'description': description, 'parameters': inputSchema}) + + return tool_specs + + async def call_tool(self, function_name: str, function_args: dict) -> Optional[dict]: + if not self.session: + raise RuntimeError('MCP client is not connected.') + + result = await self.session.call_tool(function_name, function_args) + if not result: + raise Exception('No result returned from MCP tool call.') + + result_dict = result.model_dump(mode='json') + result_content = result_dict.get('content', {}) + + if result.isError: + raise Exception(result_content) + else: + return result_content + + async def list_resources(self, cursor: Optional[str] = None) -> Optional[dict]: + if not self.session: + raise RuntimeError('MCP client is not connected.') + + result = await self.session.list_resources(cursor=cursor) + if not result: + raise Exception('No result returned from MCP list_resources call.') + + result_dict = result.model_dump() + resources = result_dict.get('resources', []) + + return resources + + async def read_resource(self, uri: str) -> Optional[dict]: + if not self.session: + raise RuntimeError('MCP client is not connected.') + + result = await self.session.read_resource(uri) + if not result: + raise Exception('No result returned from MCP read_resource call.') + result_dict = result.model_dump() + + return result_dict + + async def disconnect(self): + """Clean up and close the session. + + This method is idempotent — calling it multiple times or on a + client that was never connected is safe. It shields the close + operation from CancelledError and adds a timeout so a hung MCP + server cannot block the event loop indefinitely. + """ + exit_stack = self.exit_stack + if exit_stack is None: + return + + # Prevent double-close from concurrent callers + self.exit_stack = None + self.session = None + + try: + # IMPORTANT: Do NOT use asyncio.shield() or asyncio.wait_for() + # because they create a new asyncio task, which violates the MCP SDK's + # requirement that its TaskGroup be exited in the exact same task. + # ALSO do NOT use anyio.CancelScope(shield=True) or anyio.fail_after(), + # because they push a new cancel scope onto the task, violating LIFO + # order when aclose() attempts to exit the inner TaskGroup. + # We simply call aclose() directly. If the task is cancelled, the + # sockets will eventually be cleaned up by garbage collection. + await exit_stack.aclose() + except TimeoutError: + log.warning('MCPClient.disconnect() timed out after 5 s') + except RuntimeError as exc: + log.debug('MCPClient.disconnect() suppressed RuntimeError: %s', exc) + except Exception as exc: + log.debug('MCPClient.disconnect() error: %s', exc) + + async def __aenter__(self): + await self.exit_stack.__aenter__() + return self + + async def __aexit__(self, exc_type, exc_value, traceback): + await self.exit_stack.__aexit__(exc_type, exc_value, traceback) + await self.disconnect() diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py new file mode 100644 index 0000000000000000000000000000000000000000..5b1da36d3783b74573180b691d6738fc1a6382e6 --- /dev/null +++ b/backend/open_webui/utils/middleware.py @@ -0,0 +1,5120 @@ +import copy +import time +import logging +import sys +import os +import base64 +import textwrap + +import asyncio +from aiocache import cached +from typing import Any, Optional +import random +import json +import html +import inspect +import re +import ast + +from uuid import uuid4 +from concurrent.futures import ThreadPoolExecutor + + +from fastapi import Request, HTTPException +from fastapi.responses import HTMLResponse +from starlette.responses import Response, StreamingResponse, JSONResponse + + +from open_webui.utils.misc import is_string_allowed +from open_webui.models.oauth_sessions import OAuthSessions +from open_webui.models.chats import Chats +from open_webui.models.folders import Folders +from open_webui.models.users import Users +from open_webui.socket.main import ( + get_event_call, + get_event_emitter, +) +from open_webui.routers.tasks import ( + generate_queries, + generate_title, + generate_follow_ups, + generate_image_prompt, + generate_chat_tags, +) +from open_webui.routers.retrieval import ( + process_web_search, + SearchForm, +) +from open_webui.utils.tools import get_builtin_tools +from open_webui.routers.images import ( + image_generations, + CreateImageForm, + image_edits, + EditImageForm, +) +from open_webui.routers.pipelines import ( + process_pipeline_inlet_filter, + process_pipeline_outlet_filter, +) +from open_webui.routers.memories import query_memory, QueryMemoryForm + +from open_webui.utils.webhook import post_webhook +from open_webui.utils.files import ( + convert_markdown_base64_images, + get_file_url_from_base64, + get_image_base64_from_url, + get_image_url_from_base64, +) + + +from open_webui.models.users import UserModel +from open_webui.models.functions import Functions +from open_webui.models.models import Models + +from open_webui.retrieval.utils import get_sources_from_items + + +from open_webui.utils.sanitize import sanitize_code +from open_webui.utils.chat import generate_chat_completion +from open_webui.utils.task import ( + get_task_model_id, + rag_template, + tools_function_calling_generation_template, +) +from open_webui.utils.misc import ( + deep_update, + extract_urls, + get_message_list, + add_or_update_system_message, + add_or_update_user_message, + set_last_user_message_content, + get_last_user_message, + get_last_user_message_item, + get_last_assistant_message, + get_system_message, + merge_system_messages, + replace_system_message_content, + prepend_to_first_user_message_content, + convert_logit_bias_input_to_json, + get_content_from_message, + convert_output_to_messages, + strip_empty_content_blocks, +) +from open_webui.utils.tools import ( + get_tools, + get_updated_tool_function, + get_terminal_tools, +) +from open_webui.utils.access_control import has_connection_access +from open_webui.utils.plugin import load_function_module_by_id +from open_webui.utils.filter import ( + get_sorted_filter_ids, + process_filter_functions, +) +from open_webui.utils.code_interpreter import execute_code_jupyter +from open_webui.utils.payload import apply_system_prompt_to_body +from open_webui.utils.response import normalize_usage +from open_webui.utils.mcp.client import MCPClient + + +from open_webui.config import ( + CACHE_DIR, + DEFAULT_VOICE_MODE_PROMPT_TEMPLATE, + DEFAULT_TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, + DEFAULT_CODE_INTERPRETER_PROMPT, + CODE_INTERPRETER_PYODIDE_PROMPT, + CODE_INTERPRETER_BLOCKED_MODULES, +) +from open_webui.env import ( + GLOBAL_LOG_LEVEL, + ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION, + CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE, + CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES, + BYPASS_MODEL_ACCESS_CONTROL, + ENABLE_REALTIME_CHAT_SAVE, + ENABLE_QUERIES_CACHE, + RAG_SYSTEM_CONTEXT, + ENABLE_FORWARD_USER_INFO_HEADERS, + FORWARD_SESSION_INFO_HEADER_CHAT_ID, + FORWARD_SESSION_INFO_HEADER_MESSAGE_ID, + ENABLE_RESPONSES_API_STATEFUL, +) +from open_webui.utils.headers import include_user_info_headers +from open_webui.constants import TASKS + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) +log = logging.getLogger(__name__) + + +# 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. +DEFAULT_REASONING_TAGS = [ + ('', ''), + ('', ''), + ('', ''), + ('', ''), + ('', ''), + ('', ''), + ('<|begin_of_thought|>', '<|end_of_thought|>'), + ('◁think▷', '◁/think▷'), +] +DEFAULT_SOLUTION_TAGS = [('<|begin_of_solution|>', '<|end_of_solution|>')] +DEFAULT_CODE_INTERPRETER_TAGS = [('', '')] + + +def output_id(prefix: str) -> str: + """Generate OR-style ID: prefix + 24-char hex UUID.""" + return f'{prefix}_{uuid4().hex[:24]}' + + +def _split_tool_calls( + tool_calls: list[dict], +) -> list[dict]: + """Expand tool calls whose arguments contain multiple back-to-back JSON objects. + + Some models (e.g. GPT-5.4) send multiple complete JSON argument objects + under the same tool call index, producing concatenated invalid JSON like: + '{"query":"A","count":5}{"query":"B","count":5}' + + Each such tool call is split into separate entries so each gets executed + independently. Single-object arguments pass through unchanged. + """ + + def split_json_objects(raw: str) -> list[str]: + decoder = json.JSONDecoder() + results = [] + position = 0 + + while position < len(raw): + while position < len(raw) and raw[position].isspace(): + position += 1 + if position >= len(raw): + break + try: + _, end = decoder.raw_decode(raw, position) + results.append(raw[position:end].strip()) + position = end + except json.JSONDecodeError: + return [raw] + + return results or [raw] + + expanded = [] + for tool_call in tool_calls: + arguments = tool_call.get('function', {}).get('arguments', '') + split_arguments = split_json_objects(arguments) + + if len(split_arguments) <= 1: + expanded.append(tool_call) + else: + for argument in split_arguments: + cloned = copy.deepcopy(tool_call) + cloned['id'] = f'call_{uuid4().hex[:24]}' + cloned['function']['arguments'] = argument + expanded.append(cloned) + + return expanded + + +def get_citation_source_from_tool_result( + tool_name: str, tool_params: dict, tool_result: str, tool_id: str = '' +) -> list[dict]: + """ + Parse a tool's result and convert it to source dicts for citation display. + + Follows the source format conventions from get_sources_from_items: + - source: file/item info object with id, name, type + - document: list of document contents + - metadata: list of metadata objects with source, file_id, name fields + + Returns a list of sources (usually one, but query_knowledge_files may return multiple). + """ + _EXPECTS_LIST = {'search_web', 'query_knowledge_files'} + _EXPECTS_DICT = {'view_knowledge_file', 'view_file'} + + try: + try: + tool_result = json.loads(tool_result) + except (json.JSONDecodeError, TypeError): + pass # keep tool_result as-is (e.g. fetch_url returns plain text) + if isinstance(tool_result, dict) and 'error' in tool_result: + return [] + + # Validate tool_result type based on what the branch expects + if tool_name in _EXPECTS_LIST and not isinstance(tool_result, list): + return [] + elif tool_name in _EXPECTS_DICT and not isinstance(tool_result, dict): + return [] + + if tool_name == 'search_web': + # Parse JSON array: [{"title": "...", "link": "...", "snippet": "..."}] + results = tool_result + documents = [] + metadata = [] + + for result in results: + title = result.get('title', '') + link = result.get('link', '') + snippet = result.get('snippet', '') + + documents.append(f'{title}\n{snippet}') + metadata.append( + { + 'source': link, + 'name': title, + 'url': link, + } + ) + + return [ + { + 'source': {'name': 'search_web', 'id': 'search_web'}, + 'document': documents, + 'metadata': metadata, + } + ] + + elif tool_name in ('view_knowledge_file', 'view_file'): + file_data = tool_result + filename = file_data.get('filename', 'Unknown File') + file_id = file_data.get('id', '') + knowledge_name = file_data.get('knowledge_name', '') + + return [ + { + 'source': { + 'id': file_id, + 'name': filename, + 'type': 'file', + }, + 'document': [file_data.get('content', '')], + 'metadata': [ + { + 'file_id': file_id, + 'name': filename, + 'source': filename, + **({'knowledge_name': knowledge_name} if knowledge_name else {}), + } + ], + } + ] + + elif tool_name == 'fetch_url': + url = tool_params.get('url', '') + content = tool_result if isinstance(tool_result, str) else str(tool_result) + snippet = content[:500] + ('...' if len(content) > 500 else '') + + return [ + { + 'source': {'name': url or 'fetch_url', 'id': url or 'fetch_url'}, + 'document': [snippet], + 'metadata': [ + { + 'source': url, + 'name': url, + 'url': url, + } + ], + } + ] + + elif tool_name == 'query_knowledge_files': + chunks = tool_result + + # Group chunks by source for better citation display + # Each unique source becomes a separate source entry + sources_by_file = {} + + for chunk in chunks: + source_name = chunk.get('source', 'Unknown') + file_id = chunk.get('file_id', '') + note_id = chunk.get('note_id', '') + chunk_type = chunk.get('type', 'file') + content = chunk.get('content', '') + + # Use file_id or note_id as the key + key = file_id or note_id or source_name + + if key not in sources_by_file: + sources_by_file[key] = { + 'source': { + 'id': file_id or note_id, + 'name': source_name, + 'type': chunk_type, + }, + 'document': [], + 'metadata': [], + } + + sources_by_file[key]['document'].append(content) + sources_by_file[key]['metadata'].append( + { + 'file_id': file_id, + 'name': source_name, + 'source': source_name, + **({'note_id': note_id} if note_id else {}), + } + ) + + # Return all grouped sources as a list + if sources_by_file: + return list(sources_by_file.values()) + + # Empty result fallback + return [] + + else: + # Fallback for other tools + return [ + { + 'source': { + 'name': tool_name, + 'type': 'tool', + 'id': tool_id or tool_name, + }, + 'document': [str(tool_result)], + 'metadata': [{'source': tool_name, 'name': tool_name}], + } + ] + except Exception as e: + log.exception(f'Error parsing tool result for {tool_name}: {e}') + return [ + { + 'source': {'name': tool_name, 'type': 'tool'}, + 'document': [str(tool_result)], + 'metadata': [{'source': tool_name}], + } + ] + + +def split_content_and_whitespace(content): + content_stripped = content.rstrip() + original_whitespace = content[len(content_stripped) :] if len(content) > len(content_stripped) else '' + return content_stripped, original_whitespace + + +def is_opening_code_block(content): + backtick_segments = content.split('```') + # Even number of segments means the last backticks are opening a new block + return len(backtick_segments) > 1 and len(backtick_segments) % 2 == 0 + + +_OPENAI_TOOL_DISPLAY_NAMES = { + 'web_search_call': 'Web Search', + 'file_search_call': 'File Search', + 'computer_call': 'Computer Use', +} + + +def _render_openai_tool_call_handler(item: dict, done: bool) -> str: + """Render an OpenAI Responses API server-side tool item as a
      block. + + Handles web_search_call, file_search_call, and computer_call items whose + schemas are defined in the openai-python SDK (generated from OpenAPI spec). + """ + item_type = item.get('type', '') + call_id = item.get('id', '') + display_name = _OPENAI_TOOL_DISPLAY_NAMES.get(item_type, item_type) + + # Build a short summary of what the tool did + summary = '' + if item_type == 'web_search_call': + action = item.get('action', {}) + if isinstance(action, dict): + atype = action.get('type', '') + if atype == 'search': + queries = action.get('queries') or [] + query = action.get('query', '') + summary = ( + f'Search: {", ".join(str(q) for q in queries)}' + if queries + else (f'Search: {query}' if query else '') + ) + elif atype == 'open_page': + summary = f'Open page: {action.get("url", "")}' if action.get('url') else '' + elif atype == 'find_in_page': + summary = f'Find in page: {action.get("pattern", "")}' if action.get('pattern') else '' + elif item_type == 'file_search_call': + queries = item.get('queries', []) + if queries: + summary = f'Queries: {", ".join(str(q) for q in queries)}' + elif item_type == 'computer_call': + action = item.get('action') + actions = item.get('actions') + if isinstance(action, dict): + summary = f'Action: {action.get("type", "unknown")}' + elif isinstance(actions, list) and actions: + summary = f'Actions: {", ".join(a.get("type", "?") for a in actions if isinstance(a, dict))}' + + escaped_name = html.escape(display_name) + if done: + return f'
      \nTool Executed\n{html.escape(summary)}\n
      \n' + return f'
      \nExecuting...\n
      \n' + + +def serialize_output(output: list) -> str: + """ + Convert OR-aligned output items to HTML for display. + For LLM consumption, use convert_output_to_messages() instead. + """ + parts: list[str] = [] + + # First pass: collect function_call_output items by call_id for lookup + tool_outputs = {} + for item in output: + if item.get('type') == 'function_call_output': + tool_outputs[item.get('call_id')] = item + + # Second pass: render items in order + for idx, item in enumerate(output): + item_type = item.get('type', '') + + if item_type == 'message': + for content_part in item.get('content', []): + if 'text' in content_part: + text = content_part.get('text', '').strip() + if text: + parts.append(text) + + elif item_type == 'function_call': + call_id = item.get('call_id', '') + name = item.get('name', '') + arguments = item.get('arguments', '') + + result_item = tool_outputs.get(call_id) + if result_item: + result_parts: list[str] = [] + for result_output in result_item.get('output', []): + if 'text' in result_output: + output_text = result_output.get('text', '') + result_parts.append(str(output_text) if not isinstance(output_text, str) else output_text) + result_text = ''.join(result_parts) + files = result_item.get('files') + embeds = result_item.get('embeds', '') + + parts.append( + f'
      \nTool Executed\n{html.escape(json.dumps(result_text, ensure_ascii=False))}\n
      ' + ) + else: + parts.append( + f'
      \nExecuting...\n
      ' + ) + + elif item_type == 'function_call_output': + # Already handled inline with function_call above + pass + + elif item_type in _OPENAI_TOOL_DISPLAY_NAMES: + status = item.get('status', 'in_progress') + done = status in ('completed', 'failed', 'incomplete') or idx != len(output) - 1 + parts.append(_render_openai_tool_call_handler(item, done).rstrip('\n')) + + elif item_type == 'reasoning': + reasoning_parts: list[str] = [] + # Check for 'summary' (new structure) or 'content' (legacy/fallback) + source_list = item.get('summary', []) or item.get('content', []) + for content_part in source_list: + if 'text' in content_part: + reasoning_parts.append(content_part.get('text', '')) + elif 'summary' in content_part: # Handle potential nested logic if any + pass + + reasoning_content = ''.join(reasoning_parts).strip() + + duration = item.get('duration') + status = item.get('status', 'in_progress') + + # Infer completion: if this reasoning item is NOT the last item, + # render as done (a subsequent item means reasoning is complete) + is_last_item = idx == len(output) - 1 + + display = html.escape( + '\n'.join( + (f'> {line}' if not line.startswith('>') else line) for line in reasoning_content.splitlines() + ) + ) + + if status == 'completed' or duration is not None or not is_last_item: + parts.append( + f'
      \nThought for {duration or 0} seconds\n{display}\n
      ' + ) + else: + parts.append( + f'
      \nThinking…\n{display}\n
      ' + ) + + elif item_type == 'open_webui:code_interpreter': + # Code interpreter needs to inspect/mutate prior accumulated content + # to strip trailing unclosed code fences — materialize only here. + content = '\n'.join(parts) + content_stripped, original_whitespace = split_content_and_whitespace(content) + if is_opening_code_block(content_stripped): + content = content_stripped.rstrip('`').rstrip() + original_whitespace + else: + content = content_stripped + original_whitespace + + # Re-split back into parts list after mutation + parts = [content] if content else [] + + # Render the code_interpreter item as a
      block + # so the frontend Collapsible renders "Analyzing..."/"Analyzed". + code = item.get('code', '').strip() + lang = item.get('lang', 'python') + status = item.get('status', 'in_progress') + duration = item.get('duration') + is_last_item = idx == len(output) - 1 + + # Build inner content: code block + display = '' + if code: + display = f'```{lang}\n{code}\n```' + + # Build output attribute as HTML-escaped JSON for CodeBlock.svelte + ci_output = item.get('output') + output_attr = '' + if ci_output: + if isinstance(ci_output, dict): + output_json = json.dumps(ci_output, ensure_ascii=False) + else: + output_json = json.dumps({'result': str(ci_output)}, ensure_ascii=False) + output_attr = f' output="{html.escape(output_json)}"' + + if status == 'completed' or duration is not None or not is_last_item: + parts.append( + f'
      \nAnalyzed\n{display}\n
      ' + ) + else: + parts.append( + f'
      \nAnalyzing…\n{display}\n
      ' + ) + + return '\n'.join(parts).strip() + + +def deep_merge(target, source): + """ + Merge source into target recursively (returning new structure). + - Dicts: Recursive merge. + - Strings: Concatenation. + - Others: Overwrite. + """ + if isinstance(target, dict) and isinstance(source, dict): + new_target = target.copy() + for k, v in source.items(): + if k in new_target: + new_target[k] = deep_merge(new_target[k], v) + else: + new_target[k] = v + return new_target + elif isinstance(target, str) and isinstance(source, str): + return target + source + else: + return source + + +def handle_responses_streaming_event( + data: dict, + current_output: list, +) -> tuple[list, dict | None]: + """ + Handle Responses API streaming events in a pure functional way. + + Args: + data: The event data + current_output: List of output items (treated as immutable) + + Returns: + tuple[list, dict | None]: (new_output, metadata) + - new_output: The updated output list. + - metadata: Metadata to emit (e.g. usage), {} if update occurred, None if skip. + """ + # Default: no change + # Note: treating current_output as immutable, but avoiding full deepcopy for perf. + # We will shallow copy only if we need to modify the list structure or items. + + event_type = data.get('type', '') + + if event_type == 'response.output_item.added': + item = data.get('item', {}) + if item: + new_output = list(current_output) + new_output.append(item) + return new_output, None + return current_output, None + + elif event_type == 'response.content_part.added': + part = data.get('part', {}) + output_index = data.get('output_index', len(current_output) - 1) + + if current_output and 0 <= output_index < len(current_output): + new_output = list(current_output) + # Copy the item to mutate it + item = new_output[output_index].copy() + new_output[output_index] = item + + if 'content' not in item: + item['content'] = [] + else: + # Copy content list + item['content'] = list(item['content']) + + if item.get('type') == 'reasoning': + # Reasoning items should not have content parts + pass + else: + item['content'].append(part) + return new_output, None + return current_output, None + + elif event_type == 'response.reasoning_summary_part.added': + part = data.get('part', {}) + output_index = data.get('output_index', len(current_output) - 1) + + if current_output and 0 <= output_index < len(current_output): + new_output = list(current_output) + item = new_output[output_index].copy() + new_output[output_index] = item + + if 'summary' not in item: + item['summary'] = [] + else: + item['summary'] = list(item['summary']) + + item['summary'].append(part) + return new_output, None + return current_output, None + + elif event_type.startswith('response.') and event_type.endswith('.delta'): + # Generic Delta Handling + parts = event_type.split('.') + if len(parts) >= 3: + delta_type = parts[1] + delta = data.get('delta', '') + + output_index = data.get('output_index', len(current_output) - 1) + + if current_output and 0 <= output_index < len(current_output): + new_output = list(current_output) + item = new_output[output_index].copy() + new_output[output_index] = item + item_type = item.get('type', '') + + # Determine target field and object based on delta_type and item_type + if delta_type == 'function_call_arguments': + key = 'arguments' + if item_type == 'function_call': + # Function call args are usually strings + item[key] = item.get(key, '') + str(delta) + else: + # Generic handling, refined by item type below + pass + + if item_type == 'message': + # Message items: "text"/"output_text" -> "text" + # "reasoning_text" -> Skipped (should use reasoning item) + if delta_type in ['text', 'output_text']: + key = 'text' + elif delta_type in ['reasoning_text', 'reasoning_summary_text']: + # Skip reasoning updates for message items + return new_output, None + else: + key = delta_type + + content_index = data.get('content_index', 0) + if 'content' not in item: + item['content'] = [] + else: + item['content'] = list(item['content']) + content_list = item['content'] + + while len(content_list) <= content_index: + content_list.append({'type': 'text', 'text': ''}) + + # Copy the part to mutate it + part = content_list[content_index].copy() + content_list[content_index] = part + + current_val = part.get(key) + if current_val is None: + # Initialize based on delta type + current_val = {} if isinstance(delta, dict) else '' + + part[key] = deep_merge(current_val, delta) + + elif item_type == 'reasoning': + # Reasoning items: "reasoning_text"/"reasoning_summary_text" -> "text" + # "text"/"output_text" -> Skipped (should use message item) + if delta_type == 'reasoning_summary_text': + # Summary updates -> item['summary'] + key = 'text' + summary_index = data.get('summary_index', 0) + if 'summary' not in item: + item['summary'] = [] + else: + item['summary'] = list(item['summary']) + summary_list = item['summary'] + + while len(summary_list) <= summary_index: + summary_list.append({'type': 'summary_text', 'text': ''}) + + part = summary_list[summary_index].copy() + summary_list[summary_index] = part + + target_val = part.get(key, '') + part[key] = deep_merge(target_val, delta) + + elif delta_type == 'reasoning_text': + # Reasoning body updates -> item['content'] + key = 'text' + content_index = data.get('content_index', 0) + if 'content' not in item: + item['content'] = [] + else: + item['content'] = list(item['content']) + content_list = item['content'] + + while len(content_list) <= content_index: + # Reasoning content parts default to text + content_list.append({'type': 'text', 'text': ''}) + + part = content_list[content_index].copy() + content_list[content_index] = part + + target_val = part.get(key, '') + part[key] = deep_merge(target_val, delta) + + elif delta_type in ['text', 'output_text']: + return new_output, None + else: + # Fallback just in case other deltas target reasoning? + pass + + else: + # Fallback for other item types + if delta_type in ['text', 'output_text']: + key = 'text' + else: + key = delta_type + + current_val = item.get(key) + if current_val is None: + current_val = {} if isinstance(delta, dict) else '' + item[key] = deep_merge(current_val, delta) + + return new_output, None + + elif event_type.startswith('response.') and event_type.endswith('.done'): + # Delta Events: response.content_part.done, response.text.done, etc. + parts = event_type.split('.') + if len(parts) >= 3: + type_name = parts[1] + + # 1. Handle specific Delta "done" signals + if type_name == 'content_part': + # "Signaling that no further changes will occur to a content part" + # If payloads contains the full part, we could update it. + # Usually purely signaling in standard implementation, but we check payload. + part = data.get('part') + output_index = data.get('output_index', len(current_output) - 1) + + if part and current_output and 0 <= output_index < len(current_output): + new_output = list(current_output) + item = new_output[output_index].copy() + new_output[output_index] = item + + if 'content' in item: + item['content'] = list(item['content']) + content_index = data.get('content_index', len(item['content']) - 1) + if 0 <= content_index < len(item['content']): + item['content'][content_index] = part + return new_output, {} + return current_output, None + + elif type_name == 'reasoning_summary_part': + part = data.get('part') + output_index = data.get('output_index', len(current_output) - 1) + + if part and current_output and 0 <= output_index < len(current_output): + new_output = list(current_output) + item = new_output[output_index].copy() + new_output[output_index] = item + + if 'summary' in item: + item['summary'] = list(item['summary']) + summary_index = data.get('summary_index', len(item['summary']) - 1) + if 0 <= summary_index < len(item['summary']): + item['summary'][summary_index] = part + return new_output, {} + return current_output, None + + # 2. Skip Output Item done (handled specifically below) + if type_name == 'output_item': + pass + + # 3. Generic Field Done (text.done, audio.done) + elif type_name not in ['completed', 'failed']: + output_index = data.get('output_index', len(current_output) - 1) + if current_output and 0 <= output_index < len(current_output): + key = ( + 'text' + if type_name + in [ + 'text', + 'output_text', + 'reasoning_text', + 'reasoning_summary_text', + ] + else type_name + ) + if type_name == 'function_call_arguments': + key = 'arguments' + + if key in data: + final_value = data[key] + new_output = list(current_output) + item = new_output[output_index].copy() + new_output[output_index] = item + item_type = item.get('type', '') + + if type_name == 'function_call_arguments': + if item_type == 'function_call': + item['arguments'] = final_value + elif item_type == 'message': + content_index = data.get('content_index', 0) + if 'content' in item: + item['content'] = list(item['content']) + if len(item['content']) > content_index: + part = item['content'][content_index].copy() + item['content'][content_index] = part + part[key] = final_value + elif item_type == 'reasoning': + item['status'] = 'completed' + else: + item[key] = final_value + + return new_output, {} + + return current_output, None + + elif event_type == 'response.output_item.done': + # Delta Event: Output item complete + item = data.get('item') + output_index = data.get('output_index', len(current_output) - 1) + + new_output = list(current_output) + if item and 0 <= output_index < len(current_output): + new_output[output_index] = item + elif item: + new_output.append(item) + return new_output, {} + + elif event_type == 'response.completed': + # State Machine Event: Completed + response_data = data.get('response', {}) + final_output = response_data.get('output') + + new_output = final_output if final_output is not None else current_output + + # Ensure reasoning items are marked as completed in the final output + if new_output: + for item in new_output: + if item.get('type') == 'reasoning' and item.get('status') != 'completed': + item['status'] = 'completed' + + return new_output, { + 'usage': response_data.get('usage'), + 'done': True, + 'response_id': response_data.get('id'), + } + + elif event_type == 'response.in_progress': + # State Machine Event: In Progress + # We could extract metadata if needed, but for now just acknowledge iteration + return current_output, None + + elif event_type == 'response.failed': + # State Machine Event: Failed + error = data.get('response', {}).get('error', {}) + return current_output, {'error': error} + + else: + return current_output, None + + +def get_source_context(sources: list, source_ids: dict = None, include_content: bool = True) -> str: + """ + Build tag context string from citation sources. + """ + context_string = '' + if source_ids is None: + source_ids = {} + for source in sources: + for doc, meta in zip(source.get('document', []), source.get('metadata', [])): + source_id = meta.get('source') or source.get('source', {}).get('id') or 'N/A' + if source_id not in source_ids: + source_ids[source_id] = len(source_ids) + 1 + src_name = source.get('source', {}).get('name') + src_type = source.get('source', {}).get('type') + src_rid = source.get('source', {}).get('id') + body = doc if include_content else '' + context_string += ( + f'{body}\n' + ) + return context_string + + +def apply_source_context_to_messages( + request: Request, + messages: list, + sources: list, + user_message: str, + include_content: bool = True, +) -> list: + """ + Build source context from citation sources and apply to messages. + Uses RAG template to format context for model consumption. + + When include_content is False, emit tags with id/name but no + document body — useful when the content is already present elsewhere + (e.g. in a tool result message) and only citation markers are needed. + """ + if not sources or not user_message: + return messages + + context = get_source_context(sources, include_content=include_content) + + context = context.strip() + if not context: + return messages + + if RAG_SYSTEM_CONTEXT: + return add_or_update_system_message( + rag_template(request.app.state.config.RAG_TEMPLATE, context, user_message), + messages, + append=True, + ) + else: + return add_or_update_user_message( + rag_template(request.app.state.config.RAG_TEMPLATE, context, user_message), + messages, + append=False, + ) + + +async def process_tool_result( + request, + tool_function_name, + tool_result, + tool_type, + direct_tool=False, + metadata=None, + user=None, +): + tool_result_embeds = [] + EXTERNAL_TOOL_TYPES = ('external', 'action', 'terminal') + + # Support (HTMLResponse, result_context) tuples: the optional second + # element lets tool authors provide the LLM with actionable context + # about the generated embed instead of the generic fallback message. + result_context = None + if isinstance(tool_result, tuple) and len(tool_result) == 2 and isinstance(tool_result[0], HTMLResponse): + tool_result, result_context = tool_result + + if isinstance(tool_result, HTMLResponse): + content_disposition = tool_result.headers.get('Content-Disposition', '') + if 'inline' in content_disposition: + content = tool_result.body.decode('utf-8', 'replace') + tool_result_embeds.append(content) + + if 200 <= tool_result.status_code < 300: + if result_context is not None and isinstance(result_context, (str, dict, list)): + tool_result = result_context + else: + tool_result = { + 'status': 'success', + 'code': 'ui_component', + 'message': f'{tool_function_name}: Embedded UI result is active and visible to the user.', + } + elif 400 <= tool_result.status_code < 500: + tool_result = { + 'status': 'error', + 'code': 'ui_component', + 'message': f'{tool_function_name}: Client error {tool_result.status_code} from embedded UI result.', + } + elif 500 <= tool_result.status_code < 600: + tool_result = { + 'status': 'error', + 'code': 'ui_component', + 'message': f'{tool_function_name}: Server error {tool_result.status_code} from embedded UI result.', + } + else: + tool_result = { + 'status': 'error', + 'code': 'ui_component', + 'message': f'{tool_function_name}: Unexpected status code {tool_result.status_code} from embedded UI result.', + } + else: + tool_result = tool_result.body.decode('utf-8', 'replace') + + elif (tool_type in EXTERNAL_TOOL_TYPES and isinstance(tool_result, tuple)) or ( + direct_tool and isinstance(tool_result, list) and len(tool_result) == 2 + ): + tool_result, tool_response_headers = tool_result + + try: + if not isinstance(tool_response_headers, dict): + tool_response_headers = dict(tool_response_headers) + except Exception as e: + tool_response_headers = {} + log.debug(e) + + if tool_response_headers and isinstance(tool_response_headers, dict): + content_disposition = tool_response_headers.get( + 'Content-Disposition', + tool_response_headers.get('content-disposition', ''), + ) + + if 'inline' in content_disposition: + content_type = tool_response_headers.get( + 'Content-Type', + tool_response_headers.get('content-type', ''), + ) + location = tool_response_headers.get( + 'Location', + tool_response_headers.get('location', ''), + ) + + if 'text/html' in content_type: + # Support (html_content, result_context) nested tuple + result_context = None + html_content = tool_result + if isinstance(tool_result, (tuple, list)) and len(tool_result) == 2: + html_content, result_context = tool_result + + # Display as iframe embed + tool_result_embeds.append(html_content) + if result_context is not None and isinstance(result_context, (str, dict, list)): + tool_result = result_context + else: + tool_result = { + 'status': 'success', + 'code': 'ui_component', + 'message': f'{tool_function_name}: Embedded UI result is active and visible to the user.', + } + elif location: + # Support (html_content, result_context) nested tuple for location embeds + result_context = None + if isinstance(tool_result, (tuple, list)) and len(tool_result) == 2: + _, result_context = tool_result + + tool_result_embeds.append(location) + if result_context is not None and isinstance(result_context, (str, dict, list)): + tool_result = result_context + else: + tool_result = { + 'status': 'success', + 'code': 'ui_component', + 'message': f'{tool_function_name}: Embedded UI result is active and visible to the user.', + } + + tool_result_files = [] + + # Detect base64 image data URIs from tool results (e.g. binary image + # responses from execute_tool_server). Move the data URI to + # tool_result_files and replace tool_result with a text summary. + if isinstance(tool_result, str) and tool_result.startswith('data:image/'): + tool_result_files.append({'type': 'image', 'url': tool_result}) + tool_result = f'{tool_function_name}: Image file read successfully.' + + if isinstance(tool_result, list): + if tool_type == 'mcp': # MCP + tool_response = [] + for item in tool_result: + if isinstance(item, dict): + if item.get('type') == 'text': + text = item.get('text', '') + if isinstance(text, str): + try: + text = json.loads(text) + except json.JSONDecodeError: + pass + tool_response.append(text) + elif item.get('type') in ['image', 'audio']: + file_url = await get_file_url_from_base64( + request, + f'data:{item.get("mimeType")};base64,{item.get("data", item.get("blob", ""))}', + { + 'chat_id': metadata.get('chat_id', None), + 'message_id': metadata.get('message_id', None), + 'session_id': metadata.get('session_id', None), + 'result': item, + }, + user, + ) + + tool_result_files.append( + { + 'type': item.get('type', 'data'), + 'url': file_url, + } + ) + elif item.get('type') == 'resource': + resource = item.get('resource', {}) + text = resource.get('text', '') + if isinstance(text, str) and text: + try: + text = json.loads(text) + except json.JSONDecodeError: + pass + tool_response.append(text) + tool_result = tool_response[0] if len(tool_response) == 1 else tool_response + else: # OpenAPI + for item in tool_result: + if isinstance(item, str) and item.startswith('data:'): + tool_result_files.append( + { + 'type': 'data', + 'content': item, + } + ) + tool_result.remove(item) + + if isinstance(tool_result, list): + tool_result = {'results': tool_result} + + if isinstance(tool_result, dict) or isinstance(tool_result, list): + tool_result = json.dumps(tool_result, indent=2, ensure_ascii=False) + + # Safety: ensure tool_result is always a string (or None) to prevent + # downstream TypeError when concatenating (e.g. if an upstream callable + # returned a tuple that was not unpacked by the branches above). + if tool_result is not None and not isinstance(tool_result, str): + if isinstance(tool_result, tuple): + # execute_tool_server returns (data, headers); unpack the data part + tool_result = json.dumps(tool_result[0], indent=2, ensure_ascii=False) if len(tool_result) > 0 else '' + else: + tool_result = str(tool_result) + + return tool_result, tool_result_files, tool_result_embeds + + +async def terminal_event_handler( + tool_function_name: str, + tool_function_params: dict, + tool_result, + event_emitter, +): + """Emit terminal:* events for Open Terminal tools. + + - display_file → emits 'terminal:display_file' to open the file preview. + - write_file / replace_file_content → emits 'terminal:write_file' to refresh. + - run_command → emits 'terminal:run_command' with cwd to refresh if relevant. + """ + if not event_emitter: + return + + if tool_function_name == 'display_file': + path = tool_function_params.get('path', '') + if not path: + return + # Only emit if the file actually exists + parsed = tool_result + if isinstance(parsed, str): + try: + parsed = json.loads(parsed) + except (json.JSONDecodeError, TypeError): + pass + if isinstance(parsed, dict) and parsed.get('exists') is False: + return + + await event_emitter( + { + 'type': f'terminal:{tool_function_name}', + 'data': {'path': path}, + } + ) + elif tool_function_name in ('write_file', 'replace_file_content'): + path = tool_function_params.get('path', '') + if not path: + return + await event_emitter( + { + 'type': f'terminal:{tool_function_name}', + 'data': {'path': path}, + } + ) + elif tool_function_name == 'run_command': + await event_emitter( + { + 'type': 'terminal:run_command', + 'data': {}, + } + ) + + +async def chat_completion_tools_handler( + request: Request, body: dict, extra_params: dict, user: UserModel, models, tools +) -> tuple[dict, dict]: + async def get_content_from_response(response) -> Optional[str]: + content = None + if hasattr(response, 'body_iterator'): + async for chunk in response.body_iterator: + data = json.loads(chunk.decode('utf-8', 'replace')) + content = data['choices'][0]['message']['content'] + + # Cleanup any remaining background tasks if necessary + if response.background is not None: + await response.background() + else: + content = response['choices'][0]['message']['content'] + return content + + def get_tools_function_calling_payload(messages, task_model_id, content): + user_message = get_last_user_message(messages) + + if user_message and messages and messages[-1]['role'] == 'user': + # Remove the last user message to avoid duplication + messages = messages[:-1] + + recent_messages = messages[-4:] if len(messages) > 4 else messages + chat_history = '\n'.join( + f'{message["role"].upper()}: """{get_content_from_message(message)}"""' for message in recent_messages + ) + + prompt = f'History:\n{chat_history}\nQuery: {user_message}' if chat_history else f'Query: {user_message}' + + return { + 'model': task_model_id, + 'messages': [ + {'role': 'system', 'content': content}, + {'role': 'user', 'content': prompt}, + ], + 'stream': False, + 'metadata': {'task': str(TASKS.FUNCTION_CALLING)}, + } + + event_caller = extra_params['__event_call__'] + event_emitter = extra_params['__event_emitter__'] + metadata = extra_params['__metadata__'] + + task_model_id = get_task_model_id( + body['model'], + request.app.state.config.TASK_MODEL, + request.app.state.config.TASK_MODEL_EXTERNAL, + models, + ) + + skip_files = False + sources = [] + + specs = [tool['spec'] for tool in tools.values()] + tools_specs = json.dumps(specs, ensure_ascii=False) + + if request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE != '': + template = request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE + else: + template = DEFAULT_TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE + + tools_function_calling_prompt = tools_function_calling_generation_template(template, tools_specs) + payload = get_tools_function_calling_payload(body['messages'], task_model_id, tools_function_calling_prompt) + + try: + response = await generate_chat_completion(request, form_data=payload, user=user) + log.debug(f'{response=}') + content = await get_content_from_response(response) + log.debug(f'{content=}') + + if not content: + return body, {} + + try: + content = content[content.find('{') : content.rfind('}') + 1] + if not content: + raise Exception('No JSON object found in the response') + + result = json.loads(content) + + async def tool_call_handler(tool_call): + nonlocal skip_files + + log.debug(f'{tool_call=}') + + tool_function_name = tool_call.get('name', None) + if tool_function_name not in tools: + return body, {} + + tool_function_params = tool_call.get('parameters', {}) + + tool = None + tool_type = '' + direct_tool = False + + try: + tool = tools[tool_function_name] + tool_type = tool.get('type', '') + direct_tool = tool.get('direct', False) + + spec = tool.get('spec', {}) + allowed_params = spec.get('parameters', {}).get('properties', {}).keys() + tool_function_params = {k: v for k, v in tool_function_params.items() if k in allowed_params} + + if tool.get('direct', False): + tool_result = await event_caller( + { + 'type': 'execute:tool', + 'data': { + 'id': str(uuid4()), + 'name': tool_function_name, + 'params': tool_function_params, + 'server': tool.get('server', {}), + 'session_id': metadata.get('session_id', None), + }, + } + ) + else: + tool_function = tool['callable'] + tool_result = await tool_function(**tool_function_params) + + except Exception as e: + tool_result = str(e) + + tool_result, tool_result_files, tool_result_embeds = await process_tool_result( + request, + tool_function_name, + tool_result, + tool_type, + direct_tool, + metadata, + user, + ) + + if event_emitter: + await terminal_event_handler( + tool_function_name, + tool_function_params, + tool_result, + event_emitter, + ) + + if tool_result_files: + await event_emitter( + { + 'type': 'files', + 'data': { + 'files': tool_result_files, + }, + } + ) + + if tool_result_embeds: + await event_emitter( + { + 'type': 'embeds', + 'data': { + 'embeds': tool_result_embeds, + }, + } + ) + + if tool_result: + tool = tools[tool_function_name] + tool_id = tool.get('tool_id', '') + + tool_name = f'{tool_id}/{tool_function_name}' if tool_id else f'{tool_function_name}' + + # Citation is enabled for this tool + sources.append( + { + 'source': { + 'name': (f'{tool_name}'), + }, + 'document': [str(tool_result)], + 'metadata': [ + { + 'source': (f'{tool_name}'), + 'parameters': tool_function_params, + } + ], + 'tool_result': True, + } + ) + + if tools[tool_function_name].get('metadata', {}).get('file_handler', False): + skip_files = True + + # check if "tool_calls" in result + if result.get('tool_calls'): + for tool_call in result.get('tool_calls'): + await tool_call_handler(tool_call) + else: + await tool_call_handler(result) + + except Exception as e: + log.debug(f'Error: {e}') + content = None + except Exception as e: + log.debug(f'Error: {e}') + content = None + + log.debug(f'tool_contexts: {sources}') + + if skip_files and 'files' in body.get('metadata', {}): + del body['metadata']['files'] + + return body, {'sources': sources} + + +async def chat_memory_handler(request: Request, form_data: dict, extra_params: dict, user): + try: + results = await query_memory( + request, + QueryMemoryForm( + **{ + 'content': get_last_user_message(form_data['messages']) or '', + 'k': 3, + } + ), + user, + ) + except Exception as e: + log.debug(e) + results = None + + user_context = '' + if results and hasattr(results, 'documents'): + if results.documents and len(results.documents) > 0: + for doc_idx, doc in enumerate(results.documents[0]): + created_at_date = 'Unknown Date' + + if results.metadatas[0][doc_idx].get('created_at'): + created_at_timestamp = results.metadatas[0][doc_idx]['created_at'] + created_at_date = time.strftime('%Y-%m-%d', time.localtime(created_at_timestamp)) + + user_context += f'{doc_idx + 1}. [{created_at_date}] {doc}\n' + + form_data['messages'] = add_or_update_system_message( + f'User Context:\n{user_context}\n', form_data['messages'], append=True + ) + + return form_data + + +async def chat_web_search_handler(request: Request, form_data: dict, extra_params: dict, user): + event_emitter = extra_params['__event_emitter__'] + await event_emitter( + { + 'type': 'status', + 'data': { + 'action': 'web_search', + 'description': 'Searching the web', + 'done': False, + }, + } + ) + + messages = form_data['messages'] + user_message = get_last_user_message(messages) + + queries = [] + try: + res = await generate_queries( + request, + { + 'model': form_data['model'], + 'messages': messages, + 'prompt': user_message, + 'type': 'web_search', + 'chat_id': extra_params.get('__chat_id__'), + }, + user, + ) + + response = res['choices'][0]['message']['content'] + + try: + bracket_start = response.rfind('{') + bracket_end = response.rfind('}') + 1 + + if bracket_start == -1 or bracket_end == -1: + raise Exception('No JSON object found in the response') + + response = response[bracket_start:bracket_end] + queries = json.loads(response) + queries = queries.get('queries', []) + except Exception as e: + queries = [response] + + if ENABLE_QUERIES_CACHE: + request.state.cached_queries = queries + + except Exception as e: + log.exception(e) + queries = [user_message or ''] + + # Check if generated queries are empty + if len(queries) == 1 and queries[0].strip() == '': + queries = [user_message or ''] + + # Check if queries are not found + if len(queries) == 0: + await event_emitter( + { + 'type': 'status', + 'data': { + 'action': 'web_search', + 'description': 'No search query generated', + 'done': True, + }, + } + ) + return form_data + + await event_emitter( + { + 'type': 'status', + 'data': { + 'action': 'web_search_queries_generated', + 'queries': queries, + 'done': False, + }, + } + ) + + try: + results = await process_web_search( + request, + SearchForm(queries=queries), + user=user, + ) + + if results: + files = form_data.get('files', []) + + if results.get('collection_names'): + for col_idx, collection_name in enumerate(results.get('collection_names')): + files.append( + { + 'collection_name': collection_name, + 'name': ', '.join(queries), + 'type': 'web_search', + 'urls': results['filenames'], + 'queries': queries, + } + ) + elif results.get('docs'): + # Invoked when bypass embedding and retrieval is set to True + docs = results['docs'] + files.append( + { + 'docs': docs, + 'name': ', '.join(queries), + 'type': 'web_search', + 'urls': results['filenames'], + 'queries': queries, + } + ) + + form_data['files'] = files + + await event_emitter( + { + 'type': 'status', + 'data': { + 'action': 'web_search', + 'description': 'Searched {{count}} sites', + 'urls': results['filenames'], + 'items': results.get('items', []), + 'done': True, + }, + } + ) + else: + await event_emitter( + { + 'type': 'status', + 'data': { + 'action': 'web_search', + 'description': 'No search results found', + 'done': True, + 'error': True, + }, + } + ) + + except Exception as e: + log.exception(e) + await event_emitter( + { + 'type': 'status', + 'data': { + 'action': 'web_search', + 'description': 'An error occurred while searching the web', + 'queries': queries, + 'done': True, + 'error': True, + }, + } + ) + + return form_data + + +def get_images_from_messages(message_list): + images = [] + + for message in reversed(message_list): + message_images = [] + for file in message.get('files', []): + if file.get('type') == 'image': + message_images.append(file.get('url')) + elif file.get('content_type', '').startswith('image/'): + message_images.append(file.get('url')) + + if message_images: + images.append(message_images) + + return images + + +async def get_image_urls(delta_images, request, metadata, user) -> list[str]: + if not isinstance(delta_images, list): + return [] + + image_urls = [] + for img in delta_images: + if not isinstance(img, dict) or img.get('type') != 'image_url': + continue + + url = img.get('image_url', {}).get('url') + if not url: + continue + + if url.startswith('data:image/png;base64'): + url = await get_image_url_from_base64(request, url, metadata, user) + + image_urls.append(url) + + return image_urls + + +async def add_file_context(messages: list, chat_id: str, user) -> list: + """ + Add file URLs to messages for native function calling. + """ + if not chat_id or chat_id.startswith('local:'): + return messages + + chat = await Chats.get_chat_by_id_and_user_id(chat_id, user.id) + if not chat: + return messages + + history = chat.chat.get('history', {}) + stored_messages = get_message_list(history.get('messages', {}), history.get('currentId')) + + def format_file_tag(file): + attrs = f'type="{file.get("type", "file")}" url="{file["url"]}"' + if file.get('content_type'): + attrs += f' content_type="{file["content_type"]}"' + if file.get('name'): + attrs += f' name="{file["name"]}"' + return f'' + + # Pair only user-role messages from both lists to avoid misalignment. + # After process_messages_with_output(), assistant messages with tool calls + # are expanded into multiple messages (assistant + tool results), making + # the payload message list longer than the stored message list. A naive + # positional zip() would pair user messages with wrong stored messages, + # causing later images to lose their file context (see #21878). + user_messages = [m for m in messages if m.get('role') == 'user'] + stored_user_messages = [m for m in stored_messages if m.get('role') == 'user'] + + for message, stored_message in zip(user_messages, stored_user_messages): + files_with_urls = [ + file + for file in stored_message.get('files', []) + if file.get('url') and not file.get('url').startswith('data:') + ] + if not files_with_urls: + continue + + file_tags = [format_file_tag(file) for file in files_with_urls] + file_context = '\n' + '\n'.join(file_tags) + '\n\n\n' + + content = message.get('content', '') + if isinstance(content, list): + message['content'] = [{'type': 'text', 'text': file_context}] + content + else: + message['content'] = file_context + content + + return messages + + +async def chat_image_generation_handler(request: Request, form_data: dict, extra_params: dict, user): + metadata = extra_params.get('__metadata__', {}) + chat_id = metadata.get('chat_id', None) + __event_emitter__ = extra_params.get('__event_emitter__', None) + + if not chat_id or not isinstance(chat_id, str) or not __event_emitter__: + return form_data + + if chat_id.startswith('local:'): + message_list = form_data.get('messages', []) + else: + chat = await Chats.get_chat_by_id_and_user_id(chat_id, user.id) + await __event_emitter__( + { + 'type': 'status', + 'data': {'description': 'Creating image', 'done': False}, + } + ) + + messages_map = chat.chat.get('history', {}).get('messages', {}) + message_id = chat.chat.get('history', {}).get('currentId') + message_list = get_message_list(messages_map, message_id) + + user_message = get_last_user_message(message_list) + + prompt = user_message + message_images = get_images_from_messages(message_list) + + # Limit to first 2 sets of images + # We may want to change this in the future to allow more images + input_images = [] + for idx, images in enumerate(message_images): + if idx >= 2: + break + for image in images: + input_images.append(image) + + system_message_content = '' + + if len(input_images) > 0 and request.app.state.config.ENABLE_IMAGE_EDIT: + # Edit image(s) + try: + images = await image_edits( + request=request, + form_data=EditImageForm(**{'prompt': prompt, 'image': input_images}), + metadata={ + 'chat_id': metadata.get('chat_id', None), + 'message_id': metadata.get('message_id', None), + }, + user=user, + ) + + await __event_emitter__( + { + 'type': 'status', + 'data': {'description': 'Image created', 'done': True}, + } + ) + + await __event_emitter__( + { + 'type': 'files', + 'data': { + 'files': [ + { + 'type': 'image', + 'url': image['url'], + } + for image in images + ] + }, + } + ) + + system_message_content = 'The requested image has been edited and created and is now being shown to the user. Let them know that it has been generated.' + except Exception as e: + log.debug(e) + + error_message = '' + if isinstance(e, HTTPException): + if e.detail and isinstance(e.detail, dict): + error_message = e.detail.get('message', str(e.detail)) + else: + error_message = str(e.detail) + + await __event_emitter__( + { + 'type': 'status', + 'data': { + 'description': f'An error occurred while generating an image', + 'done': True, + }, + } + ) + + system_message_content = f'Image generation was attempted but failed. The system is currently unable to generate the image. Tell the user that the following error occurred: {error_message}' + + else: + # Create image(s) + if request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION: + try: + res = await generate_image_prompt( + request, + { + 'model': form_data['model'], + 'messages': form_data['messages'], + 'chat_id': metadata.get('chat_id'), + }, + user, + ) + + response = res['choices'][0]['message']['content'] + + try: + bracket_start = response.rfind('{') + bracket_end = response.rfind('}') + 1 + + if bracket_start == -1 or bracket_end == -1: + raise Exception('No JSON object found in the response') + + response = response[bracket_start:bracket_end] + response = json.loads(response) + prompt = response.get('prompt', []) + except Exception as e: + prompt = user_message + + except Exception as e: + log.exception(e) + prompt = user_message + + try: + images = await image_generations( + request=request, + form_data=CreateImageForm(**{'prompt': prompt}), + metadata={ + 'chat_id': metadata.get('chat_id', None), + 'message_id': metadata.get('message_id', None), + }, + user=user, + ) + + await __event_emitter__( + { + 'type': 'status', + 'data': {'description': 'Image created', 'done': True}, + } + ) + + await __event_emitter__( + { + 'type': 'files', + 'data': { + 'files': [ + { + 'type': 'image', + 'url': image['url'], + } + for image in images + ] + }, + } + ) + + system_message_content = 'The requested image has been created by the system successfully and is now being shown to the user. Let the user know that the image they requested has been generated and is now shown in the chat.' + except Exception as e: + log.debug(e) + + error_message = '' + if isinstance(e, HTTPException): + if e.detail and isinstance(e.detail, dict): + error_message = e.detail.get('message', str(e.detail)) + else: + error_message = str(e.detail) + + await __event_emitter__( + { + 'type': 'status', + 'data': { + 'description': f'An error occurred while generating an image', + 'done': True, + }, + } + ) + + system_message_content = f'Image generation was attempted but failed because of an error. The system is currently unable to generate the image. Tell the user that the following error occurred: {error_message}' + + if system_message_content: + form_data['messages'] = add_or_update_system_message(system_message_content, form_data['messages']) + + return form_data + + +async def chat_completion_files_handler( + request: Request, body: dict, extra_params: dict, user: UserModel +) -> tuple[dict, dict[str, list]]: + __event_emitter__ = extra_params['__event_emitter__'] + sources = [] + + if files := body.get('metadata', {}).get('files', None): + # Check if all files are in full context mode + all_full_context = all(item.get('context') == 'full' for item in files) + + queries = [] + if not all_full_context: + try: + queries_response = await generate_queries( + request, + { + 'model': body['model'], + 'messages': body['messages'], + 'type': 'retrieval', + 'chat_id': body.get('metadata', {}).get('chat_id'), + }, + user, + ) + queries_response = queries_response['choices'][0]['message']['content'] + + try: + bracket_start = queries_response.rfind('{') + bracket_end = queries_response.rfind('}') + 1 + + if bracket_start == -1 or bracket_end == -1: + raise Exception('No JSON object found in the response') + + queries_response = queries_response[bracket_start:bracket_end] + queries_response = json.loads(queries_response) + except Exception as e: + queries_response = {'queries': [queries_response]} + + queries = queries_response.get('queries', []) + except Exception: + pass + + await __event_emitter__( + { + 'type': 'status', + 'data': { + 'action': 'queries_generated', + 'queries': queries, + 'done': False, + }, + } + ) + + if len(queries) == 0: + queries = [get_last_user_message(body['messages']) or ''] + + try: + # Directly await async get_sources_from_items (no thread needed - fully async now) + sources = await get_sources_from_items( + request=request, + items=files, + queries=queries, + embedding_function=lambda query, prefix: request.app.state.EMBEDDING_FUNCTION( + query, prefix=prefix, user=user + ), + k=request.app.state.config.TOP_K, + reranking_function=( + (lambda query, documents: request.app.state.RERANKING_FUNCTION(query, documents, user=user)) + if request.app.state.RERANKING_FUNCTION + else None + ), + k_reranker=request.app.state.config.TOP_K_RERANKER, + r=request.app.state.config.RELEVANCE_THRESHOLD, + hybrid_bm25_weight=request.app.state.config.HYBRID_BM25_WEIGHT, + hybrid_search=request.app.state.config.ENABLE_RAG_HYBRID_SEARCH, + full_context=all_full_context or request.app.state.config.RAG_FULL_CONTEXT, + user=user, + ) + except Exception as e: + log.exception(e) + + log.debug(f'rag_contexts:sources: {sources}') + + unique_ids = set() + for source in sources or []: + if not source or len(source.keys()) == 0: + continue + + documents = source.get('document') or [] + metadatas = source.get('metadata') or [] + src_info = source.get('source') or {} + + for index, _ in enumerate(documents): + metadata = metadatas[index] if index < len(metadatas) else None + _id = (metadata or {}).get('source') or (src_info or {}).get('id') or 'N/A' + unique_ids.add(_id) + + sources_count = len(unique_ids) + await __event_emitter__( + { + 'type': 'status', + 'data': { + 'action': 'sources_retrieved', + 'count': sources_count, + 'done': True, + }, + } + ) + + return body, {'sources': sources} + + +def apply_params_to_form_data(form_data, model): + params = form_data.pop('params', {}) + custom_params = params.pop('custom_params', {}) + + open_webui_params = { + 'stream_response': bool, + 'stream_delta_chunk_size': int, + 'function_calling': str, + 'reasoning_tags': list, + 'system': str, + } + + for key in list(params.keys()): + if key in open_webui_params: + del params[key] + + if custom_params: + # Attempt to parse custom_params if they are strings + for key, value in custom_params.items(): + if isinstance(value, str): + try: + # Attempt to parse the string as JSON + custom_params[key] = json.loads(value) + except json.JSONDecodeError: + # If it fails, keep the original string + pass + + # If custom_params are provided, merge them into params + params = deep_update(params, custom_params) + + if model.get('owned_by') == 'ollama': + # Ollama specific parameters + form_data['options'] = params + else: + if isinstance(params, dict): + for key, value in params.items(): + if value is not None: + form_data[key] = value + + if 'logit_bias' in params and params['logit_bias'] is not None: + try: + logit_bias = convert_logit_bias_input_to_json(params['logit_bias']) + + if logit_bias: + form_data['logit_bias'] = json.loads(logit_bias) + except Exception as e: + log.exception(f'Error parsing logit_bias: {e}') + + return form_data + + +async def convert_url_images_to_base64(form_data): + messages = form_data.get('messages', []) + + for message in messages: + content = message.get('content') + if not isinstance(content, list): + continue + + new_content = [] + + for item in content: + if not isinstance(item, dict) or item.get('type') != 'image_url': + new_content.append(item) + continue + + image_url = item.get('image_url', {}).get('url', '') + if image_url.startswith('data:image/'): + new_content.append(item) + continue + + try: + base64_data = await get_image_base64_from_url(image_url) + if base64_data: + new_content.append( + { + 'type': 'image_url', + 'image_url': {'url': base64_data}, + } + ) + else: + new_content.append(item) + except Exception as e: + log.debug(f'Error converting image URL to base64: {e}') + new_content.append(item) + + message['content'] = new_content + + return form_data + + +async def load_messages_from_db(chat_id: str, message_id: str) -> Optional[list[dict]]: + """ + Load the message chain from DB up to message_id, + keeping only LLM-relevant fields (role, content, output). + """ + messages_map = await Chats.get_messages_map_by_chat_id(chat_id) + if not messages_map: + return None + + db_messages = get_message_list(messages_map, message_id) + if not db_messages: + return None + + return [{k: v for k, v in msg.items() if k in ('role', 'content', 'output', 'files')} for msg in db_messages] + + +def process_messages_with_output(messages: list[dict]) -> list[dict]: + """ + Process messages with OR-aligned output items for LLM consumption. + + For assistant messages with 'output' field, produces properly formatted + OpenAI-style messages (tool_calls + tool results). Strips 'output' before LLM. + """ + processed = [] + + for message in messages: + if message.get('role') == 'assistant' and message.get('output'): + # Use output items for clean OpenAI-format messages + output_messages = convert_output_to_messages(message['output'], raw=True) + if output_messages: + processed.extend(output_messages) + continue + + # Strip 'output' field before adding (LLM shouldn't see it) + clean_message = {k: v for k, v in message.items() if k != 'output'} + processed.append(clean_message) + + return processed + + +SKILL_MENTION_RE = re.compile(r'<\$([^|>]+)\|?[^>]*>') + + +def _get_text_parts(message: dict) -> list[str]: + """Return all text segments from a message's content.""" + content = message.get('content') + if isinstance(content, str): + return [content] + if isinstance(content, list): + return [p.get('text', '') for p in content if isinstance(p, dict) and p.get('type') == 'text'] + return [] + + +def extract_skill_ids_from_messages(messages: list[dict]) -> set[str]: + """Extract skill IDs from <$skillId|label> mention tags in messages.""" + ids: set[str] = set() + for message in messages: + for text in _get_text_parts(message): + ids.update(m.group(1) for m in SKILL_MENTION_RE.finditer(text)) + return ids + + +def strip_skill_mentions(messages: list[dict]) -> None: + """Strip <$skillId|label> mention tags from message content in-place.""" + strip_re = re.compile(r'<\$[^>]+>') + for message in messages: + content = message.get('content') + if isinstance(content, str) and strip_re.search(content): + message['content'] = strip_re.sub('', content).strip() + elif isinstance(content, list): + for part in content: + if isinstance(part, dict) and part.get('type') == 'text': + text = part.get('text', '') + if strip_re.search(text): + part['text'] = strip_re.sub('', text).strip() + + +async def process_chat_payload(request, form_data, user, metadata, model): + # Pipeline Inlet -> Filter Inlet -> Chat Memory -> Chat Web Search -> Chat Image Generation + # -> Chat Code Interpreter (Form Data Update) -> (Default) Chat Tools Function Calling + # -> Chat Files + + # Arena model resolution — pick the sub-model now so all downstream + # processing (knowledge, capabilities, tools, params) uses its settings + # instead of the empty arena wrapper. + if model.get('owned_by') == 'arena': + arena_model_ids = model.get('info', {}).get('meta', {}).get('model_ids') + arena_filter_mode = model.get('info', {}).get('meta', {}).get('filter_mode') + if arena_model_ids and arena_filter_mode == 'exclude': + arena_model_ids = [ + available_model['id'] + for available_model in request.app.state.MODELS.values() + if available_model.get('owned_by') != 'arena' and available_model['id'] not in arena_model_ids + ] + + if isinstance(arena_model_ids, list) and arena_model_ids: + selected_model_id = random.choice(arena_model_ids) + else: + arena_model_ids = [ + available_model['id'] + for available_model in request.app.state.MODELS.values() + if available_model.get('owned_by') != 'arena' + ] + selected_model_id = random.choice(arena_model_ids) + + selected_model = request.app.state.MODELS.get(selected_model_id) + if selected_model: + model = selected_model + form_data['model'] = selected_model_id + metadata['selected_model_id'] = selected_model_id + + form_data = apply_params_to_form_data(form_data, model) + log.debug(f'form_data: {form_data}') + + # Load messages from DB when available — DB preserves structured 'output' items + # which the frontend strips, causing tool calls to be merged into content. + chat_id = metadata.get('chat_id') + user_message_id = metadata.get('user_message_id') + + if chat_id and user_message_id and not chat_id.startswith('local:'): + db_messages = await load_messages_from_db(chat_id, user_message_id) + if db_messages: + system_message = get_system_message(form_data.get('messages', [])) + form_data['messages'] = [system_message, *db_messages] if system_message else db_messages + + # Inject image files into content as image_url parts (mirrors frontend logic) + for message in form_data['messages']: + image_files = [ + f + for f in message.get('files', []) + if f.get('type') == 'image' or (f.get('content_type') or '').startswith('image/') + ] + if message.get('role') == 'user' and image_files: + text_content = message.get('content', '') + if isinstance(text_content, str): + message['content'] = [ + {'type': 'text', 'text': text_content}, + *[ + { + 'type': 'image_url', + 'image_url': {'url': f['url']}, + } + for f in image_files + if f.get('url') + ], + ] + # Strip files field — it's been incorporated into content + message.pop('files', None) + + # Process messages with OR-aligned output items for clean LLM messages + form_data['messages'] = process_messages_with_output(form_data.get('messages', [])) + + system_message = get_system_message(form_data.get('messages', [])) + if system_message: # Chat Controls/User Settings + try: + form_data = apply_system_prompt_to_body( + system_message.get('content'), form_data, metadata, user, replace=True + ) # Required to handle system prompt variables + except Exception: + pass + + form_data = await convert_url_images_to_base64(form_data) + + event_emitter = await get_event_emitter(metadata) + event_caller = await get_event_call(metadata) + + extra_params = { + '__event_emitter__': event_emitter, + '__event_call__': event_caller, + '__user__': user.model_dump() if isinstance(user, UserModel) else {}, + '__metadata__': metadata, + '__oauth_token__': await get_system_oauth_token(request, user), + '__request__': request, + '__model__': model, + '__chat_id__': metadata.get('chat_id'), + '__message_id__': metadata.get('message_id'), + } + # Initialize events to store additional event to be sent to the client + # Initialize contexts and citation + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): + models = { + request.state.model['id']: request.state.model, + } + else: + models = request.app.state.MODELS + + task_model_id = get_task_model_id( + form_data['model'], + request.app.state.config.TASK_MODEL, + request.app.state.config.TASK_MODEL_EXTERNAL, + models, + ) + + events = [] + sources = [] + + # Folder "Project" handling + # Check if the request has chat_id and is inside of a folder + # Uses lightweight column query — only fetches folder_id, not the full chat JSON blob + chat_id = metadata.get('chat_id', None) + folder_id = None + if chat_id and user: + folder_id = await Chats.get_chat_folder_id(chat_id, user.id) + + # Fallback: use folder_id from metadata (temporary chats have no DB record) + if not folder_id: + folder_id = metadata.get('folder_id', None) + + if folder_id and user: + folder = await Folders.get_folder_by_id_and_user_id(folder_id, user.id) + + if folder and folder.data: + if 'system_prompt' in folder.data: + form_data = apply_system_prompt_to_body(folder.data['system_prompt'], form_data, metadata, user) + if 'files' in folder.data: + if metadata.get('params', {}).get('function_calling') != 'native': + form_data['files'] = [ + *folder.data['files'], + *form_data.get('files', []), + ] + else: + # Native FC: skip RAG injection, builtin tools + # will read folder knowledge from metadata. + metadata['folder_knowledge'] = folder.data['files'] + + # Model "Knowledge" handling + user_message = get_last_user_message(form_data['messages']) + model_knowledge = model.get('info', {}).get('meta', {}).get('knowledge', False) + + if model_knowledge and metadata.get('params', {}).get('function_calling') != 'native': + await event_emitter( + { + 'type': 'status', + 'data': { + 'action': 'knowledge_search', + 'query': user_message, + 'done': False, + }, + } + ) + + knowledge_files = [] + for item in model_knowledge: + if item.get('collection_name'): + knowledge_files.append( + { + 'id': item.get('collection_name'), + 'name': item.get('name'), + 'legacy': True, + } + ) + elif item.get('collection_names'): + knowledge_files.append( + { + 'name': item.get('name'), + 'type': 'collection', + 'collection_names': item.get('collection_names'), + 'legacy': True, + } + ) + else: + knowledge_files.append(item) + + files = form_data.get('files', []) + files.extend(knowledge_files) + form_data['files'] = files + + variables = form_data.pop('variables', None) + + # Process the form_data through the pipeline + try: + form_data = await process_pipeline_inlet_filter(request, form_data, user, models) + except Exception as e: + raise e + + try: + filter_ids = await get_sorted_filter_ids(request, model, metadata.get('filter_ids', [])) + filter_functions = await Functions.get_functions_by_ids(filter_ids) + + form_data, flags = await process_filter_functions( + request=request, + filter_functions=filter_functions, + filter_type='inlet', + form_data=form_data, + extra_params=extra_params, + ) + except Exception as e: + raise Exception(f'{e}') + + features = form_data.pop('features', None) or {} + extra_params['__features__'] = features + if features: + if 'voice' in features and features['voice']: + if request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE != None: + if request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE != '': + template = request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE + else: + template = DEFAULT_VOICE_MODE_PROMPT_TEMPLATE + + form_data['messages'] = add_or_update_system_message( + template, + form_data['messages'], + ) + + if 'memory' in features and features['memory']: + # Skip forced memory injection when native FC is enabled - model can use memory tools + if metadata.get('params', {}).get('function_calling') != 'native': + form_data = await chat_memory_handler(request, form_data, extra_params, user) + + if 'web_search' in features and features['web_search']: + # Skip forced RAG web search when native FC is enabled - model can use web_search tool + if metadata.get('params', {}).get('function_calling') != 'native': + form_data = await chat_web_search_handler(request, form_data, extra_params, user) + + if 'image_generation' in features and features['image_generation']: + # Skip forced image generation when native FC is enabled - model can use generate_image tool + if metadata.get('params', {}).get('function_calling') != 'native': + form_data = await chat_image_generation_handler(request, form_data, extra_params, user) + + if 'code_interpreter' in features and features['code_interpreter']: + engine = getattr(request.app.state.config, 'CODE_INTERPRETER_ENGINE', 'pyodide') + + # Skip XML-tag prompt injection when native FC is enabled — + # execute_code will be injected as a builtin tool instead + if metadata.get('params', {}).get('function_calling') != 'native': + prompt = ( + request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE + if request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE != '' + else DEFAULT_CODE_INTERPRETER_PROMPT + ) + + # Append filesystem awareness only for pyodide engine + if engine != 'jupyter': + prompt += CODE_INTERPRETER_PYODIDE_PROMPT + + form_data['messages'] = add_or_update_user_message( + prompt, + 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. + if engine != 'jupyter': + form_data['messages'] = add_or_update_system_message( + CODE_INTERPRETER_PYODIDE_PROMPT, + form_data['messages'], + append=True, + ) + + tool_ids = form_data.pop('tool_ids', None) + terminal_id = form_data.pop('terminal_id', None) + files = form_data.pop('files', None) + form_data.pop('folder_id', None) + + # Caller-provided OpenAI-style tools take precedence over server-side + # tool resolution (tool_ids, MCP servers, builtin tools). + payload_tools = form_data.get('tools', None) + + # Skills — extract IDs from message content (<$skillId|label> tags) so + # persisted chats work without relying on the frontend to send skill_ids. + user_skill_ids = set(form_data.pop('skill_ids', None) or []) + user_skill_ids |= extract_skill_ids_from_messages(form_data.get('messages', [])) + model_skill_ids = set(model.get('info', {}).get('meta', {}).get('skillIds', [])) + + all_skill_ids = user_skill_ids | model_skill_ids + available_skills = [] + if all_skill_ids: + from open_webui.models.skills import Skills as SkillsModel + + accessible_skill_ids = {s.id for s in await SkillsModel.get_skills_by_user_id(user.id, 'read')} + available_skills = [] + for sid in all_skill_ids: + if sid in accessible_skill_ids: + s = await SkillsModel.get_skill_by_id(sid) + if s and s.is_active: + available_skills.append(s) + + skill_descriptions = '' + for skill in available_skills: + if skill.id in user_skill_ids: + # User-selected: inject full content + form_data['messages'] = add_or_update_system_message( + f'\n{skill.content}\n', + form_data['messages'], + append=True, + ) + else: + # Model-attached: name+description only + skill_descriptions += f'\n{skill.id}\n{skill.name}\n{skill.description or ""}\n\n' + + if skill_descriptions: + form_data['messages'] = add_or_update_system_message( + f'\n{skill_descriptions}', + form_data['messages'], + append=True, + ) + + # Strip <$skillId|label> mention tags so the model doesn't see raw markup. + strip_skill_mentions(form_data.get('messages', [])) + + prompt = get_last_user_message(form_data['messages']) + # TODO: re-enable URL extraction from prompt + # urls = [] + # if prompt and len(prompt or "") < 500 and (not files or len(files) == 0): + # urls = extract_urls(prompt) + + if files: + if not files: + files = [] + + for file_item in files: + if file_item.get('type', 'file') == 'folder': + # Get folder files + folder_id = file_item.get('id', None) + if folder_id: + folder = await Folders.get_folder_by_id_and_user_id(folder_id, user.id) + if folder and folder.data and 'files' in folder.data: + files = [f for f in files if f.get('id', None) != folder_id] + files = [*files, *folder.data['files']] + + # files = [*files, *[{"type": "url", "url": url, "name": url} for url in urls]] + # Remove duplicate files based on their content + files = list({json.dumps(f, sort_keys=True): f for f in files}.values()) + + metadata = { + **metadata, + 'model_id': form_data.get('model'), + 'tool_ids': tool_ids, + 'terminal_id': terminal_id, + 'files': files, + } + form_data['metadata'] = metadata + + # When the caller provides an explicit OpenAI-style `tools` array in the + # request body, skip all server-side tool resolution and pass the caller's + # tools through to the model unchanged. + if not payload_tools: + # Server side tools + tool_ids = metadata.get('tool_ids', None) + # Client side tools + direct_tool_servers = metadata.get('tool_servers', None) + + log.debug(f'{tool_ids=}') + log.debug(f'{direct_tool_servers=}') + + tools_dict = {} + + mcp_clients = {} + mcp_tools_dict = {} + + if tool_ids: + for tool_id in tool_ids: + if tool_id.startswith('server:mcp:'): + try: + server_id = tool_id[len('server:mcp:') :] + + mcp_server_connection = None + for server_connection in request.app.state.config.TOOL_SERVER_CONNECTIONS: + if ( + server_connection.get('type', '') == 'mcp' + and server_connection.get('info', {}).get('id') == server_id + ): + mcp_server_connection = server_connection + break + + if not mcp_server_connection: + log.error(f'MCP server with id {server_id} not found') + continue + + # Check access control for MCP server + if not await has_connection_access(user, mcp_server_connection): + log.warning(f'Access denied to MCP server {server_id} for user {user.id}') + continue + + auth_type = mcp_server_connection.get('auth_type', '') + headers = {} + if auth_type == 'bearer': + headers['Authorization'] = f'Bearer {mcp_server_connection.get("key", "")}' + elif auth_type == 'none': + # No authentication + pass + elif auth_type == 'session': + headers['Authorization'] = f'Bearer {request.state.token.credentials}' + elif auth_type == 'system_oauth': + oauth_token = extra_params.get('__oauth_token__', None) + if oauth_token: + headers['Authorization'] = f'Bearer {oauth_token.get("access_token", "")}' + elif auth_type in ('oauth_2.1', 'oauth_2.1_static'): + try: + splits = server_id.split(':') + server_id = splits[-1] if len(splits) > 1 else server_id + + oauth_token = await request.app.state.oauth_client_manager.get_oauth_token( + user.id, f'mcp:{server_id}' + ) + + if oauth_token: + headers['Authorization'] = f'Bearer {oauth_token.get("access_token", "")}' + except Exception as e: + log.error(f'Error getting OAuth token: {e}') + oauth_token = None + + connection_headers = mcp_server_connection.get('headers', None) + if connection_headers and isinstance(connection_headers, dict): + for key, value in connection_headers.items(): + headers[key] = value + + # Add user info headers if enabled + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) + if metadata and metadata.get('chat_id'): + headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = metadata.get('chat_id') + if metadata and metadata.get('message_id'): + headers[FORWARD_SESSION_INFO_HEADER_MESSAGE_ID] = metadata.get('message_id') + + mcp_clients[server_id] = MCPClient() + await mcp_clients[server_id].connect( + url=mcp_server_connection.get('url', ''), + headers=headers if headers else None, + ) + + function_name_filter_list = mcp_server_connection.get('config', {}).get( + 'function_name_filter_list', '' + ) + + if isinstance(function_name_filter_list, str): + function_name_filter_list = function_name_filter_list.split(',') + + tool_specs = await mcp_clients[server_id].list_tool_specs() + for tool_spec in tool_specs: + + async def make_tool_function(client, function_name): + async def tool_function(**kwargs): + return await client.call_tool( + function_name, + function_args=kwargs, + ) + + return tool_function + + if function_name_filter_list: + if not is_string_allowed(tool_spec['name'], function_name_filter_list): + # Skip this function + continue + + tool_function = await make_tool_function(mcp_clients[server_id], tool_spec['name']) + + mcp_tools_dict[f'{server_id}_{tool_spec["name"]}'] = { + 'spec': { + **tool_spec, + 'name': f'{server_id}_{tool_spec["name"]}', + }, + 'callable': tool_function, + 'type': 'mcp', + 'client': mcp_clients[server_id], + 'direct': False, + } + except Exception as e: + log.debug(e) + if event_emitter: + await event_emitter( + { + 'type': 'chat:message:error', + 'data': {'error': {'content': f"Failed to connect to MCP server '{server_id}'"}}, + } + ) + continue + + tools_dict = await get_tools( + request, + tool_ids, + user, + { + **extra_params, + '__model__': models[task_model_id], + '__messages__': form_data['messages'], + '__files__': metadata.get('files', []), + }, + ) + + if mcp_tools_dict: + tools_dict = {**tools_dict, **mcp_tools_dict} + + # Resolve terminal tools if terminal_id is set (outside tool_ids check + # so system terminals work even when no other tools are selected) + terminal_capability = (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get('terminal', True) + if terminal_id and terminal_capability: + try: + terminal_result = await get_terminal_tools( + request, + terminal_id, + user, + extra_params, + ) + if isinstance(terminal_result, tuple): + terminal_tools, system_prompt = terminal_result + else: + terminal_tools = terminal_result + system_prompt = None + if terminal_tools: + tools_dict = {**tools_dict, **terminal_tools} + if system_prompt: + form_data['messages'] = add_or_update_system_message( + system_prompt, + form_data['messages'], + append=True, + ) + except Exception as e: + log.exception(e) + + if direct_tool_servers: + for tool_server in direct_tool_servers: + system_prompt = tool_server.pop('system_prompt', None) + if system_prompt: + form_data['messages'] = add_or_update_system_message( + system_prompt, + form_data['messages'], + append=True, + ) + + tool_specs = tool_server.pop('specs', []) + + for tool in tool_specs: + tools_dict[tool['name']] = { + 'spec': tool, + 'direct': True, + 'server': tool_server, + } + + if mcp_clients: + metadata['mcp_clients'] = mcp_clients + + # Inject builtin tools for native function calling based on enabled features and model capability + # Check if builtin_tools capability is enabled for this model (defaults to True if not specified) + builtin_tools_enabled = (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get( + 'builtin_tools', True + ) + if metadata.get('params', {}).get('function_calling') == 'native' and builtin_tools_enabled: + # Add file context to user messages + chat_id = metadata.get('chat_id') + form_data['messages'] = await add_file_context(form_data.get('messages', []), chat_id, user) + builtin_tools = await get_builtin_tools( + request, + { + **extra_params, + '__event_emitter__': event_emitter, + '__skill_ids__': [s.id for s in available_skills if s.id not in user_skill_ids], + }, + features, + model, + ) + for name, tool_dict in builtin_tools.items(): + if name not in tools_dict: + tools_dict[name] = tool_dict + + if tools_dict: + # Always store resolved tools in metadata so downstream consumers + # (e.g. pipe functions) can access all tools including MCP and builtins. + metadata['tools'] = tools_dict + + if metadata.get('params', {}).get('function_calling') == 'native': + # If the function calling is native, then call the tools function calling handler + form_data['tools'] = [ + {'type': 'function', 'function': tool.get('spec', {})} for tool in tools_dict.values() + ] + else: + # If the function calling is not native, then call the tools function calling handler + try: + form_data, flags = await chat_completion_tools_handler( + request, form_data, extra_params, user, models, tools_dict + ) + sources.extend(flags.get('sources', [])) + except Exception as e: + log.exception(e) + + # Check if file context extraction is enabled for this model (default True) + file_context_enabled = (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get('file_context', True) + + if file_context_enabled: + try: + form_data, flags = await chat_completion_files_handler(request, form_data, extra_params, user) + sources.extend(flags.get('sources', [])) + except Exception as e: + log.exception(e) + + # Save the pre-RAG message state so the native tool call loop can + # restore to the true original (before file-source injection) rather + # than a snapshot that already has the RAG template baked in. + system_message = get_system_message(form_data['messages']) + metadata['system_prompt'] = get_content_from_message(system_message) if system_message else None + metadata['user_prompt'] = get_last_user_message(form_data['messages']) + metadata['sources'] = sources[:] if sources else [] + + # If context is not empty, insert it into the messages + if sources and prompt: + form_data['messages'] = apply_source_context_to_messages(request, form_data['messages'], sources, prompt) + + # If there are citations, add them to the data_items + sources = [ + source + for source in sources + if source.get('source', {}).get('name', '') or source.get('source', {}).get('id', '') + ] + + if len(sources) > 0: + events.append({'sources': sources}) + + if model_knowledge: + await event_emitter( + { + 'type': 'status', + 'data': { + 'action': 'knowledge_search', + 'query': user_message, + 'done': True, + 'hidden': True, + }, + } + ) + + # Strip empty text content blocks from multimodal messages + # to prevent errors from providers like Gemini and Claude + form_data['messages'] = strip_empty_content_blocks(form_data.get('messages', [])) + + # Merge any duplicate system messages into a single message at position 0 + # to prevent template parsing errors with strict chat templates (e.g. Qwen) + form_data['messages'] = merge_system_messages(form_data.get('messages', [])) + + return form_data, metadata, events + + +async def get_event_emitter_and_caller(metadata): + event_emitter = None + event_caller = None + + # event_emitter only needs user_id + chat_id + message_id. + # It broadcasts to user:{user_id} room AND persists to DB, + # so it works for backend-initiated calls (automations, API). + if metadata.get('chat_id') and metadata.get('message_id'): + event_emitter = await get_event_emitter(metadata) + + # event_caller needs session_id — it calls back to a specific + # websocket session (used by direct tools, pyodide code interpreter). + if metadata.get('session_id') and metadata.get('chat_id') and metadata.get('message_id'): + event_caller = await get_event_call(metadata) + + return event_emitter, event_caller + + +async def build_chat_response_context(request, form_data, user, model, metadata, tasks, events): + event_emitter, event_caller = await get_event_emitter_and_caller(metadata) + return { + 'request': request, + 'form_data': form_data, + 'user': user, + 'model': model, + 'metadata': metadata, + 'tasks': tasks, + 'events': events, + 'event_emitter': event_emitter, + 'event_caller': event_caller, + } + + +def get_response_data(response): + if isinstance(response, list) and len(response) == 1: + # If the response is a single-item list, unwrap it #17213 + response = response[0] + + if isinstance(response, JSONResponse): + if isinstance(response.body, bytes): + try: + response_data = json.loads(response.body.decode('utf-8', 'replace')) + except json.JSONDecodeError: + response_data = {'error': {'detail': 'Invalid JSON response'}} + else: + response_data = response + elif isinstance(response, dict): + response_data = response + else: + response_data = None + + return response, response_data + + +def merge_events_into_response(response_data, events): + if events and isinstance(events, list): + extra_response = {} + for event in events: + if isinstance(event, dict): + extra_response.update(event) + else: + extra_response[event] = True + + return { + **extra_response, + **response_data, + } + return response_data + + +def build_response_object(response, response_data): + if isinstance(response, dict): + return response_data + if isinstance(response, JSONResponse): + return JSONResponse( + content=response_data, + headers=response.headers, + status_code=response.status_code, + ) + return response + + +async def get_system_oauth_token(request, user): + """Get the system OAuth token for a user. + + Primary path: use the oauth_session_id cookie (browser requests). + Fallback: look up the user's most recent OAuth session from the DB + (covers automations, API calls, and other cookie-less contexts). + """ + oauth_token = None + try: + oauth_session_id = request.cookies.get('oauth_session_id', None) + if oauth_session_id: + oauth_token = await request.app.state.oauth_manager.get_oauth_token( + user.id, + oauth_session_id, + ) + + # Fallback: no cookie (automation, API key, etc.) — use most recent session + if oauth_token is None: + from open_webui.models.oauth_sessions import OAuthSessions + + sessions = await OAuthSessions.get_sessions_by_user_id(user.id) + if sessions: + best = max(sessions, key=lambda s: s.updated_at) + oauth_token = await request.app.state.oauth_manager.get_oauth_token( + user.id, + best.id, + ) + except Exception as e: + log.error(f'Error getting OAuth token: {e}') + return oauth_token + + +async def background_tasks_handler(ctx): + request = ctx['request'] + form_data = ctx['form_data'] + user = ctx['user'] + metadata = ctx['metadata'] + tasks = ctx['tasks'] + event_emitter = ctx['event_emitter'] + + message = None + messages = [] + + if 'chat_id' in metadata and not metadata['chat_id'].startswith('local:'): + messages_map = await Chats.get_messages_map_by_chat_id(metadata['chat_id']) + message = messages_map.get(metadata['message_id']) if messages_map else None + + message_list = get_message_list(messages_map, metadata['message_id']) + + # Remove details tags and files from the messages. + # as get_message_list creates a new list, it does not affect + # the original messages outside of this handler + + messages = [] + for message in message_list: + content = message.get('content', '') + if isinstance(content, list): + for item in content: + if item.get('type') == 'text': + content = item['text'] + break + + if isinstance(content, str): + content = re.sub( + r']*>.*?<\/details>|!\[.*?\]\(.*?\)', + '', + content, + flags=re.S | re.I, + ).strip() + + messages.append( + { + **message, + 'role': message.get('role', 'assistant'), # Safe fallback for missing role + 'content': content, + } + ) + else: + # Local temp chat, get the model and message from the form_data + message = get_last_user_message_item(form_data.get('messages', [])) + messages = form_data.get('messages', []) + if message: + message['model'] = form_data.get('model') + + if message and 'model' in message: + if tasks and messages: + if TASKS.FOLLOW_UP_GENERATION in tasks and tasks[TASKS.FOLLOW_UP_GENERATION]: + res = await generate_follow_ups( + request, + { + 'model': message['model'], + 'messages': messages, + 'message_id': metadata['message_id'], + 'chat_id': metadata['chat_id'], + }, + user, + ) + + if res and isinstance(res, dict): + if len(res.get('choices', [])) == 1: + response_message = res.get('choices', [])[0].get('message', {}) + + follow_ups_string = response_message.get('content') or response_message.get( + 'reasoning_content', '' + ) + else: + follow_ups_string = '' + + follow_ups_string = follow_ups_string[ + follow_ups_string.find('{') : follow_ups_string.rfind('}') + 1 + ] + + try: + follow_ups = json.loads(follow_ups_string).get('follow_ups', []) + await event_emitter( + { + 'type': 'chat:message:follow_ups', + 'data': { + 'follow_ups': follow_ups, + }, + } + ) + + if not metadata.get('chat_id', '').startswith('local:'): + await Chats.upsert_message_to_chat_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], + { + 'followUps': follow_ups, + }, + ) + + except Exception as e: + pass + + if not metadata.get('chat_id', '').startswith('local:'): # Only update titles and tags for non-temp chats + if TASKS.TITLE_GENERATION in tasks: + user_message = get_last_user_message(messages) + if user_message and len(user_message) > 100: + user_message = user_message[:100] + '...' + + title = None + if tasks[TASKS.TITLE_GENERATION]: + res = await generate_title( + request, + { + 'model': message['model'], + 'messages': messages, + 'chat_id': metadata['chat_id'], + }, + user, + ) + + if res and isinstance(res, dict): + if len(res.get('choices', [])) == 1: + response_message = res.get('choices', [])[0].get('message', {}) + + title_string = ( + response_message.get('content') + or response_message.get( + 'reasoning_content', + ) + or message.get('content', user_message) + ) + else: + title_string = '' + + title_string = title_string[title_string.find('{') : title_string.rfind('}') + 1] + + try: + title = json.loads(title_string).get('title', user_message) + except Exception as e: + title = '' + + if not title: + title = messages[0].get('content', user_message) + + await Chats.update_chat_title_by_id(metadata['chat_id'], title) + + await event_emitter( + { + 'type': 'chat:title', + 'data': title, + } + ) + + if title == None and len(messages) == 2 and (not messages_map or len(messages_map) <= 2): + title = messages[0].get('content', user_message) + + await Chats.update_chat_title_by_id(metadata['chat_id'], title) + + await event_emitter( + { + 'type': 'chat:title', + 'data': message.get('content', user_message), + } + ) + + if TASKS.TAGS_GENERATION in tasks and tasks[TASKS.TAGS_GENERATION]: + res = await generate_chat_tags( + request, + { + 'model': message['model'], + 'messages': messages, + 'chat_id': metadata['chat_id'], + }, + user, + ) + + if res and isinstance(res, dict): + if len(res.get('choices', [])) == 1: + response_message = res.get('choices', [])[0].get('message', {}) + + tags_string = response_message.get('content') or response_message.get( + 'reasoning_content', '' + ) + else: + tags_string = '' + + tags_string = tags_string[tags_string.find('{') : tags_string.rfind('}') + 1] + + try: + tags = json.loads(tags_string).get('tags', []) + await Chats.update_chat_tags_by_id(metadata['chat_id'], tags, user) + + await event_emitter( + { + 'type': 'chat:tags', + 'data': tags, + } + ) + except Exception as e: + pass + + +async def outlet_filter_handler(ctx): + """Run outlet filters inline after chat completion. + + Replaces the separate POST /api/chat/completed round-trip. + Persists outlet-modified content to DB and emits a chat:outlet event + so the frontend can sync its in-memory state. + + For temp chats (local: prefix), messages are built from form_data + plus the assistant response message stored in ctx['assistant_message'], + since temp chats have no DB-persisted history. + """ + request = ctx['request'] + user = ctx['user'] + model = ctx['model'] + metadata = ctx['metadata'] + event_emitter = ctx.get('event_emitter') + event_caller = ctx.get('event_caller') + + chat_id = metadata.get('chat_id', '') + message_id = metadata.get('message_id') + + if not chat_id or not message_id: + return + + is_temp_chat = chat_id.startswith('local:') + + try: + messages_map = None + + if is_temp_chat: + # Temp chats have no DB record — build message list from + # the in-memory form_data plus the assistant response. + form_messages = ctx.get('form_data', {}).get('messages', []) + assistant_message = ctx.get('assistant_message', {}) + + message_list = [ + { + 'role': m.get('role'), + 'content': m.get('content', ''), + } + for m in form_messages + ] + + # Append the full assistant message (content, output, usage, etc.) + if assistant_message: + message_list.append( + { + 'id': message_id, + 'role': 'assistant', + **assistant_message, + } + ) + else: + messages_map = await Chats.get_messages_map_by_chat_id(chat_id) + if not messages_map: + return + + message_list = get_message_list(messages_map, message_id) + if not message_list: + return + + model_id = model.get('id') if isinstance(model, dict) else model + + outlet_data = { + 'model': model_id, + 'messages': [ + { + 'id': m.get('id'), + 'role': m.get('role'), + 'content': m.get('content', ''), + 'info': m.get('info'), + 'timestamp': m.get('timestamp'), + **({'output': m['output']} if m.get('output') else {}), + **({'usage': m['usage']} if m.get('usage') else {}), + **({'sources': m['sources']} if m.get('sources') else {}), + } + for m in message_list + ], + 'filter_ids': metadata.get('filter_ids', []), + 'chat_id': chat_id, + 'session_id': metadata.get('session_id'), + 'id': message_id, + } + + # Pipeline outlet filters + models = request.app.state.MODELS + try: + outlet_data = await process_pipeline_outlet_filter(request, outlet_data, user, models) + except Exception as e: + log.debug(f'Pipeline outlet filter error: {e}') + + # Function outlet filters + extra_params = { + '__event_emitter__': event_emitter, + '__event_call__': event_caller, + '__user__': user.model_dump() if isinstance(user, UserModel) else {}, + '__metadata__': metadata, + '__request__': request, + '__model__': model, + } + + filter_ids = await get_sorted_filter_ids(request, model, metadata.get('filter_ids', [])) + filter_functions = await Functions.get_functions_by_ids(filter_ids) + + outlet_result, _ = await process_filter_functions( + request=request, + filter_functions=filter_functions, + filter_type='outlet', + form_data=outlet_data, + extra_params=extra_params, + ) + + # Persist outlet-modified content and notify frontend + # (skip DB persistence for temp chats — they have no DB record) + if outlet_result and outlet_result.get('messages'): + if not is_temp_chat and messages_map: + for message in outlet_result['messages']: + outlet_message_id = message.get('id') + if outlet_message_id and outlet_message_id in messages_map: + original_message = messages_map[outlet_message_id] + if original_message.get('content') != message.get('content'): + await Chats.upsert_message_to_chat_by_id_and_message_id( + chat_id, + outlet_message_id, + { + 'content': message['content'], + 'originalContent': original_message.get('content'), + }, + ) + + if event_emitter: + await event_emitter( + { + 'type': 'chat:outlet', + 'data': {'messages': outlet_result['messages']}, + } + ) + except Exception as e: + log.debug(f'Error running outlet filters: {e}') + + +async def non_streaming_chat_response_handler(response, ctx): + request = ctx['request'] + + user = ctx['user'] + metadata = ctx['metadata'] + events = ctx['events'] + + event_emitter = ctx['event_emitter'] + + response, response_data = get_response_data(response) + if response_data is None: + return response + + if event_emitter: + try: + if 'error' in response_data: + error = response_data.get('error') + + if isinstance(error, dict): + error = error.get('detail', error) + else: + error = str(error) + + log.error('Provider returned error (non-streaming): %s', error) + + await Chats.upsert_message_to_chat_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], + { + 'error': {'content': error}, + }, + ) + if isinstance(error, str) or isinstance(error, dict): + await event_emitter( + { + 'type': 'chat:message:error', + 'data': {'error': {'content': error}}, + } + ) + + if 'selected_model_id' in response_data: + await Chats.upsert_message_to_chat_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], + { + 'selectedModelId': response_data['selected_model_id'], + }, + ) + + choices = response_data.get('choices', []) + if choices and choices[0].get('message', {}).get('content'): + content = response_data['choices'][0]['message']['content'] + + if content: + await event_emitter( + { + 'type': 'chat:completion', + 'data': response_data, + } + ) + + title = await Chats.get_chat_title_by_id(metadata['chat_id']) + + # Use output from backend if provided (OR-compliant backends), + # otherwise generate from response content + response_output = response_data.get('output') + if not response_output: + response_output = [ + { + 'type': 'message', + 'id': output_id('msg'), + 'status': 'completed', + 'role': 'assistant', + 'content': [{'type': 'output_text', 'text': content}], + } + ] + + await event_emitter( + { + 'type': 'chat:completion', + 'data': { + 'done': True, + 'content': content, + 'output': response_output, + 'title': title, + }, + } + ) + + # Save message in the database + usage = normalize_usage(response_data.get('usage', {}) or {}) + + await Chats.upsert_message_to_chat_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], + { + 'done': True, + 'role': 'assistant', + 'content': content, + 'output': response_output, + **({'usage': usage} if usage else {}), + }, + ) + + # Send a webhook notification if the user is not active + if request.app.state.config.ENABLE_USER_WEBHOOKS and not await Users.is_user_active(user.id): + webhook_url = await Users.get_user_webhook_url_by_id(user.id) + if webhook_url: + await post_webhook( + request.app.state.WEBUI_NAME, + webhook_url, + f'{content}\n\n{title} - {request.app.state.config.WEBUI_URL}/c/{metadata["chat_id"]}', + { + 'action': 'chat', + 'message': content, + 'title': title, + 'url': f'{request.app.state.config.WEBUI_URL}/c/{metadata["chat_id"]}', + }, + ) + + await background_tasks_handler(ctx) + ctx['assistant_message'] = { + 'content': content, + 'output': response_output, + **({'usage': usage} if usage else {}), + } + await outlet_filter_handler(ctx) + + response = build_response_object(response, merge_events_into_response(response_data, events)) + except Exception as e: + log.debug(f'Error occurred while processing request: {e}') + pass + + return response + + if isinstance(response, dict): + response = merge_events_into_response(response_data, events) + + return response + + +async def streaming_chat_response_handler(response, ctx): + request = ctx['request'] + + form_data = ctx['form_data'] + + user = ctx['user'] + model = ctx['model'] + + metadata = ctx['metadata'] + events = ctx['events'] + + event_emitter = ctx['event_emitter'] + event_caller = ctx['event_caller'] + + extra_params = { + '__event_emitter__': event_emitter, + '__event_call__': event_caller, + '__user__': user.model_dump() if isinstance(user, UserModel) else {}, + '__metadata__': metadata, + '__oauth_token__': await get_system_oauth_token(request, user), + '__request__': request, + '__model__': model, + } + + filter_functions = [ + await Functions.get_function_by_id(filter_id) + for filter_id in await get_sorted_filter_ids(request, model, metadata.get('filter_ids', [])) + ] + + # Standard streaming response handler + # event_caller is optional — only needed for direct (client-side) tools + # and pyodide code interpreter. Server-side tools work without it. + if event_emitter: + task_id = str(uuid4()) # Create a unique task ID. + model_id = form_data.get('model', '') + + # Handle as a background task + async def response_handler(response, events): + def tag_output_handler(content_type, tags, output): + """ + Detect special tags (reasoning, solution, code_interpreter) in streaming + content and create corresponding OR-aligned output items directly. + Operates on output items instead of content_blocks. + + Uses the text from the output items themselves for tag detection, + eliminating state divergence between accumulated content and items. + """ + end_flag = False + + def extract_attributes(tag_content): + """Extract attributes from a tag if they exist.""" + attributes = {} + if not tag_content: + return attributes + matches = re.findall(r'(\w+)\s*=\s*"([^"]+)"', tag_content) + for key, value in matches: + attributes[key] = value + return attributes + + def get_last_text(out): + """Get text from last message item, or empty string.""" + if out and out[-1].get('type') == 'message': + parts = out[-1].get('content', []) + if parts and parts[-1].get('type') == 'output_text': + return parts[-1].get('text', '') + return '' + + def set_last_text(out, text): + """Set text on last message item's output_text.""" + if out and out[-1].get('type') == 'message': + parts = out[-1].get('content', []) + if parts and parts[-1].get('type') == 'output_text': + parts[-1]['text'] = text + + # Map content_type to output item type + output_type_map = { + 'reasoning': 'reasoning', + 'solution': 'message', # solution tags just produce text + 'code_interpreter': 'open_webui:code_interpreter', + } + output_item_type = output_type_map.get(content_type, content_type) + + last_type = output[-1].get('type', '') if output else '' + + if last_type == 'message': + # Use the output item's own text for tag detection + item_text = get_last_text(output) + for start_tag, end_tag in tags: + start_tag_pattern = rf'{re.escape(start_tag)}' + if start_tag.startswith('<') and start_tag.endswith('>'): + start_tag_pattern = rf'<{re.escape(start_tag[1:-1])}(\s.*?)?>' + + match = re.search(start_tag_pattern, item_text) + if match: + try: + attr_content = match.group(1) if match.group(1) else '' + except Exception: + attr_content = '' + + attributes = extract_attributes(attr_content) + + before_tag = item_text[: match.start()] + after_tag = item_text[match.end() :] + + # Keep only text before the tag in the message + set_last_text(output, before_tag) + + if not before_tag.strip(): + # Remove empty message item + if output and output[-1].get('type') == 'message': + output.pop() + + # Append the new output item + if output_item_type == 'reasoning': + output.append( + { + 'type': 'reasoning', + 'id': output_id('r'), + 'status': 'in_progress', + 'start_tag': start_tag, + 'end_tag': end_tag, + 'attributes': attributes, + 'content': [], + 'summary': None, + 'started_at': time.time(), + } + ) + elif output_item_type == 'open_webui:code_interpreter': + output.append( + { + 'type': 'open_webui:code_interpreter', + 'id': output_id('ci'), + 'status': 'in_progress', + 'start_tag': start_tag, + 'end_tag': end_tag, + 'attributes': attributes, + 'lang': attributes.get('lang', 'python'), + 'code': '', + 'output': None, + 'started_at': time.time(), + } + ) + else: + # solution or other text-producing tag + output.append( + { + 'type': 'message', + 'id': output_id('msg'), + 'status': 'in_progress', + 'role': 'assistant', + 'content': [{'type': 'output_text', 'text': ''}], + '_tag_type': content_type, + 'start_tag': start_tag, + 'end_tag': end_tag, + 'attributes': attributes, + 'started_at': time.time(), + } + ) + + if after_tag: + # Set the after_tag content on the new item + if output_item_type == 'reasoning': + output[-1]['content'] = [{'type': 'output_text', 'text': after_tag}] + elif output_item_type == 'open_webui:code_interpreter': + output[-1]['code'] = after_tag + else: + set_last_text(output, after_tag) + + _, recursive_end = tag_output_handler(content_type, tags, output) + if recursive_end: + end_flag = True + + break + + elif ( + (last_type == 'reasoning' and content_type == 'reasoning') + or (last_type == 'open_webui:code_interpreter' and content_type == 'code_interpreter') + or (last_type == 'message' and output[-1].get('_tag_type') == content_type) + ): + item = output[-1] + start_tag = item.get('start_tag', '') + end_tag = item.get('end_tag', '') + + end_tag_pattern = rf'{re.escape(end_tag)}' + + # Get the block content from the item itself + if last_type == 'reasoning': + parts = item.get('content', []) + block_content = '' + if parts and parts[-1].get('type') == 'output_text': + block_content = parts[-1].get('text', '') + elif last_type == 'open_webui:code_interpreter': + block_content = item.get('code', '') + else: + block_content = get_last_text(output) + + if re.search(end_tag_pattern, block_content): + end_flag = True + + # Strip start and end tags from content + start_tag_pattern = rf'{re.escape(start_tag)}' + if start_tag.startswith('<') and start_tag.endswith('>'): + start_tag_pattern = rf'<{re.escape(start_tag[1:-1])}(\s.*?)?>' + block_content = re.sub(start_tag_pattern, '', block_content).strip() + + end_tag_regex = re.compile(end_tag_pattern, re.DOTALL) + split_content = end_tag_regex.split(block_content, maxsplit=1) + + block_content = split_content[0].strip() if split_content else '' + leftover_content = split_content[1].strip() if len(split_content) > 1 else '' + + if block_content: + # Update the item with final content + if last_type == 'reasoning': + item['content'] = [{'type': 'output_text', 'text': block_content}] + item['ended_at'] = time.time() + item['duration'] = int(item['ended_at'] - item['started_at']) + item['status'] = 'completed' + elif last_type == 'open_webui:code_interpreter': + item['code'] = block_content + item['ended_at'] = time.time() + item['duration'] = int(item['ended_at'] - item['started_at']) + else: + set_last_text(output, block_content) + item['ended_at'] = time.time() + + # Reset by appending a new message item for leftover + output.append( + { + 'type': 'message', + 'id': output_id('msg'), + 'status': 'in_progress', + 'role': 'assistant', + 'content': [ + { + 'type': 'output_text', + 'text': leftover_content, + } + ], + } + ) + else: + # Remove the block if content is empty + output.pop() + output.append( + { + 'type': 'message', + 'id': output_id('msg'), + 'status': 'in_progress', + 'role': 'assistant', + 'content': [ + { + 'type': 'output_text', + 'text': leftover_content, + } + ], + } + ) + + return output, end_flag + + message = await Chats.get_message_by_id_and_message_id(metadata['chat_id'], metadata['message_id']) + + tool_calls = [] + + last_assistant_message = None + try: + if form_data['messages'][-1]['role'] == 'assistant': + last_assistant_message = get_last_assistant_message(form_data['messages']) + except Exception as e: + pass + + content = ( + message.get('content', '') if message else last_assistant_message if last_assistant_message else '' + ) + + # Initialize output: use existing from message if continuing, else create new + existing_output = message.get('output') if message else None + if existing_output: + output = existing_output + else: + # Only create an initial message item if there is content to initialize with + if content: + output = [ + { + 'type': 'message', + 'id': output_id('msg'), + 'status': 'in_progress', + 'role': 'assistant', + 'content': [{'type': 'output_text', 'text': content}], + } + ] + else: + output = [] + + usage = None + prior_output = [] + last_response_id = None + + def full_output(): + return prior_output + output if prior_output else output + + reasoning_tags_param = metadata.get('params', {}).get('reasoning_tags') + DETECT_REASONING_TAGS = reasoning_tags_param is not False + DETECT_CODE_INTERPRETER = metadata.get('features', {}).get('code_interpreter', False) + + reasoning_tags = [] + if DETECT_REASONING_TAGS: + if isinstance(reasoning_tags_param, list) and len(reasoning_tags_param) == 2: + reasoning_tags = [(reasoning_tags_param[0], reasoning_tags_param[1])] + else: + reasoning_tags = DEFAULT_REASONING_TAGS + + try: + for event in events: + await event_emitter( + { + 'type': 'chat:completion', + 'data': event, + } + ) + + # Save message in the database + await Chats.upsert_message_to_chat_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], + { + **event, + }, + ) + + async def stream_body_handler(response, form_data): + nonlocal content + nonlocal usage + nonlocal output + nonlocal prior_output + nonlocal last_response_id + + response_tool_calls = [] + + delta_count = 0 + delta_chunk_size = max( + CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE, + int(metadata.get('params', {}).get('stream_delta_chunk_size') or 1), + ) + last_delta_data = None + + async def flush_pending_delta_data(threshold: int = 0): + nonlocal delta_count + nonlocal last_delta_data + + if delta_count >= threshold and last_delta_data: + await event_emitter( + { + 'type': 'chat:completion', + 'data': last_delta_data, + } + ) + delta_count = 0 + last_delta_data = None + + async for line in response.body_iterator: + line = line.decode('utf-8', 'replace') if isinstance(line, bytes) else line + data = line + + # Skip empty lines + if not data.strip(): + continue + + # "data:" is the prefix for each event + if not data.startswith('data:'): + continue + + # Remove the prefix + data = data[len('data:') :].strip() + + try: + data = json.loads(data) + + data, _ = await process_filter_functions( + request=request, + filter_functions=filter_functions, + filter_type='stream', + form_data=data, + extra_params={'__body__': form_data, **extra_params}, + ) + + if data: + if 'event' in data and not getattr(request.state, 'direct', False): + await event_emitter(data.get('event', {})) + + if 'selected_model_id' in data: + model_id = data['selected_model_id'] + await Chats.upsert_message_to_chat_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], + { + 'selectedModelId': model_id, + }, + ) + await event_emitter( + { + 'type': 'chat:completion', + 'data': data, + } + ) + # Check for Responses API events (type field starts with "response.") + elif data.get('type', '').startswith('response.'): + output, response_metadata = handle_responses_streaming_event(data, output) + + # Emit citation sources from finalized output items + # (mirrors Chat Completions annotation handling at delta level) + if data.get('type') == 'response.output_item.done': + item = data.get('item', {}) + if item.get('type') == 'message': + for part in item.get('content', []): + for annotation in part.get('annotations', []): + if annotation.get('type') == 'url_citation': + # Handle both flat (Responses API) and nested (Chat Completions) formats + url_citation = annotation.get('url_citation', annotation) + + url = url_citation.get('url', '') + title = url_citation.get('title', url) + + if url: + await event_emitter( + { + 'type': 'source', + 'data': { + 'source': { + 'name': title, + 'url': url, + }, + 'document': [title], + 'metadata': [ + { + 'source': url, + 'name': title, + } + ], + }, + } + ) + + processed_data = { + 'output': full_output(), + 'content': serialize_output(full_output()), + } + + # print(data) + # print(processed_data) + + # 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. + if response_metadata: + if ENABLE_RESPONSES_API_STATEFUL: + response_id = response_metadata.pop('response_id', None) + if response_id: + last_response_id = response_id + processed_data.update(response_metadata) + processed_data.pop('done', None) + + await event_emitter( + { + 'type': 'chat:completion', + 'data': processed_data, + } + ) + continue + else: + choices = data.get('choices', []) + + # Normalize usage data to standard format + raw_usage = data.get('usage', {}) or {} + raw_usage.update(data.get('timings', {})) # llama.cpp + if raw_usage: + usage = normalize_usage(raw_usage) + await event_emitter( + { + 'type': 'chat:completion', + 'data': { + 'usage': usage, + }, + } + ) + + if not choices: + error = data.get('error', {}) + if error: + log.error('Provider returned error (streaming): %s', error) + try: + await Chats.upsert_message_to_chat_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], + { + 'error': {'content': error}, + }, + ) + except Exception: + pass + await event_emitter( + { + 'type': 'chat:completion', + 'data': { + 'error': error, + }, + } + ) + continue + + delta = choices[0].get('delta', {}) + + # Handle delta annotations + annotations = delta.get('annotations') + if annotations: + for annotation in annotations: + if ( + annotation.get('type') == 'url_citation' + and 'url_citation' in annotation + ): + url_citation = annotation['url_citation'] + + url = url_citation.get('url', '') + title = url_citation.get('title', url) + + await event_emitter( + { + 'type': 'source', + 'data': { + 'source': { + 'name': title, + 'url': url, + }, + 'document': [title], + 'metadata': [ + { + 'source': url, + 'name': title, + } + ], + }, + } + ) + + delta_tool_calls = delta.get('tool_calls', None) + if delta_tool_calls: + for delta_tool_call in delta_tool_calls: + tool_call_index = delta_tool_call.get('index') + + if tool_call_index is not None: + # Check if the tool call already exists + current_response_tool_call = None + for response_tool_call in response_tool_calls: + if response_tool_call.get('index') == tool_call_index: + current_response_tool_call = response_tool_call + break + + if current_response_tool_call is None: + # Add the new tool call + delta_tool_call.setdefault('function', {}) + delta_tool_call['function'].setdefault('name', '') + delta_tool_call['function'].setdefault('arguments', '') + response_tool_calls.append(delta_tool_call) + else: + # Update the existing tool call + delta_name = delta_tool_call.get('function', {}).get('name') + delta_arguments = delta_tool_call.get('function', {}).get( + 'arguments' + ) + + if delta_name: + current_response_tool_call['function']['name'] = delta_name + + if delta_arguments: + current_response_tool_call['function']['arguments'] += ( + delta_arguments + ) + + # Emit pending tool calls in real-time + if response_tool_calls: + # Flush any pending text first + await flush_pending_delta_data() + + # Build pending function_call output items for display + pending_fc_items = [] + for tc in response_tool_calls: + call_id = tc.get('id', '') + func = tc.get('function', {}) + pending_fc_items.append( + { + 'type': 'function_call', + 'id': call_id or output_id('fc'), + 'call_id': call_id, + 'name': func.get('name', ''), + 'arguments': func.get('arguments', '{}'), + 'status': 'in_progress', + } + ) + + await event_emitter( + { + 'type': 'chat:completion', + 'data': { + 'content': serialize_output(full_output() + pending_fc_items), + }, + } + ) + + image_urls = await get_image_urls(delta.get('images', []), request, metadata, user) + if image_urls: + image_file_list = [{'type': 'image', 'url': url} for url in image_urls] + message_files = await Chats.add_message_files_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], + image_file_list, + ) + if message_files is None: + message_files = image_file_list + + await event_emitter( + { + 'type': 'files', + 'data': {'files': message_files}, + } + ) + + value = delta.get('content') + + reasoning_content = ( + delta.get('reasoning_content') + or delta.get('reasoning') + or delta.get('thinking') + ) + if reasoning_content: + if not output or output[-1].get('type') != 'reasoning': + reasoning_item = { + 'type': 'reasoning', + 'id': output_id('r'), + 'status': 'in_progress', + 'start_tag': '', + 'end_tag': '', + 'attributes': {'type': 'reasoning_content'}, + 'content': [], + 'summary': None, + 'started_at': time.time(), + } + output.append(reasoning_item) + else: + reasoning_item = output[-1] + + # Append to reasoning content + parts = reasoning_item.get('content', []) + if parts and parts[-1].get('type') == 'output_text': + parts[-1]['text'] += reasoning_content + else: + reasoning_item['content'] = [ + { + 'type': 'output_text', + 'text': reasoning_content, + } + ] + + data = {'content': serialize_output(full_output())} + + if value: + if ( + output + and output[-1].get('type') == 'reasoning' + and output[-1].get('attributes', {}).get('type') == 'reasoning_content' + ): + reasoning_item = output[-1] + reasoning_item['ended_at'] = time.time() + reasoning_item['duration'] = int( + reasoning_item['ended_at'] - reasoning_item['started_at'] + ) + reasoning_item['status'] = 'completed' + + output.append( + { + 'type': 'message', + 'id': output_id('msg'), + 'status': 'in_progress', + 'role': 'assistant', + 'content': [ + { + 'type': 'output_text', + 'text': '', + } + ], + } + ) + + if ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION: + value = await convert_markdown_base64_images( + request, + value, + { + 'chat_id': metadata.get('chat_id', None), + 'message_id': metadata.get('message_id', None), + }, + user, + ) + + content = f'{content}{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. + last_item = output[-1] if output else None + last_item_type = last_item.get('type', '') if last_item else '' + inside_tag_block = ( + last_item is not None + and last_item.get('status') == 'in_progress' + and last_item.get('attributes', {}).get('type') != 'reasoning_content' + and ( + last_item_type == 'reasoning' + or last_item_type == 'open_webui:code_interpreter' + or ( + last_item_type == 'message' + and last_item.get('_tag_type') is not None + ) + ) + ) + + if inside_tag_block: + # Append to the existing tag-based item + if last_item_type == 'open_webui:code_interpreter': + last_item['code'] = last_item.get('code', '') + value + elif last_item_type == 'reasoning': + parts = last_item.get('content', []) + if parts and parts[-1].get('type') == 'output_text': + parts[-1]['text'] += value + else: + last_item['content'] = [ + { + 'type': 'output_text', + 'text': value, + } + ] + else: + # solution or other _tag_type message + msg_parts = last_item.get('content', []) + if msg_parts and msg_parts[-1].get('type') == 'output_text': + msg_parts[-1]['text'] += value + else: + last_item['content'] = [ + { + 'type': 'output_text', + 'text': value, + } + ] + else: + if not output or output[-1].get('type') != 'message': + output.append( + { + 'type': 'message', + 'id': output_id('msg'), + 'status': 'in_progress', + 'role': 'assistant', + 'content': [ + { + 'type': 'output_text', + 'text': '', + } + ], + } + ) + + # Append value to last message item's text + msg_parts = output[-1].get('content', []) + if msg_parts and msg_parts[-1].get('type') == 'output_text': + msg_parts[-1]['text'] += value + else: + output[-1]['content'] = [ + { + 'type': 'output_text', + 'text': value, + } + ] + + if DETECT_REASONING_TAGS: + output, _ = tag_output_handler( + 'reasoning', + reasoning_tags, + output, + ) + + output, _ = tag_output_handler( + 'solution', + DEFAULT_SOLUTION_TAGS, + output, + ) + + if DETECT_CODE_INTERPRETER: + output, end = tag_output_handler( + 'code_interpreter', + DEFAULT_CODE_INTERPRETER_TAGS, + output, + ) + + if end: + break + + if ENABLE_REALTIME_CHAT_SAVE: + # Save message in the database + await Chats.upsert_message_to_chat_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], + { + 'content': serialize_output(full_output()), + 'output': full_output(), + }, + ) + else: + data = { + 'content': serialize_output(full_output()), + } + + if delta: + delta_count += 1 + last_delta_data = data + if delta_count >= delta_chunk_size: + await flush_pending_delta_data(delta_chunk_size) + else: + await event_emitter( + { + 'type': 'chat:completion', + 'data': data, + } + ) + except (asyncio.CancelledError, KeyboardInterrupt): + raise + except Exception as e: + done = 'data: [DONE]' in line + if done: + pass + else: + log.debug(f'Error: {e}') + continue + await flush_pending_delta_data() + + if output: + # Clean up the last message item + if output[-1].get('type') == 'message': + parts = output[-1].get('content', []) + if parts and parts[-1].get('type') == 'output_text': + parts[-1]['text'] = parts[-1]['text'].strip() + + if not parts[-1]['text']: + output.pop() + + if not output: + output.append( + { + 'type': 'message', + 'id': output_id('msg'), + 'status': 'in_progress', + 'role': 'assistant', + 'content': [{'type': 'output_text', 'text': ''}], + } + ) + + if output[-1].get('type') == 'reasoning': + reasoning_item = output[-1] + if reasoning_item.get('ended_at') is None: + reasoning_item['ended_at'] = time.time() + reasoning_item['duration'] = int( + reasoning_item['ended_at'] - reasoning_item['started_at'] + ) + reasoning_item['status'] = 'completed' + + if response_tool_calls: + tool_calls.append(_split_tool_calls(response_tool_calls)) + + # Responses API path: extract function_call items from output + if not response_tool_calls and output: + # Collect call_ids that already have results, + # including those from prior_output so we don't + # re-process tool calls from a previous turn. + handled_call_ids = { + item.get('call_id') + for item in (prior_output + output) + if item.get('type') == 'function_call_output' + } + responses_api_tool_calls = [] + for item in output: + if item.get('type') == 'function_call' and item.get('call_id') not in handled_call_ids: + arguments = item.get('arguments', '{}') + responses_api_tool_calls.append( + { + 'id': item.get('call_id', ''), + 'index': len(responses_api_tool_calls), + 'function': { + 'name': item.get('name', ''), + 'arguments': ( + arguments if isinstance(arguments, str) else json.dumps(arguments) + ), + }, + } + ) + if responses_api_tool_calls: + tool_calls.append(_split_tool_calls(responses_api_tool_calls)) + + try: + await stream_body_handler(response, form_data) + finally: + if response.background: + await response.background() + + tool_call_retries = 0 + tool_call_sources = [] # Track citation sources from tool results + all_tool_call_sources = [] # Accumulated sources across all iterations + user_message = get_last_user_message(form_data['messages']) + + # Check if citations are enabled for this model + citations_enabled = (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get( + 'citations', True + ) + + # Use the pre-RAG system content captured before the + # initial file-source injection in process_chat_payload. + # This ensures restore truly undoes the RAG template. + original_system_content = metadata.get('system_prompt') + if original_system_content is None: + original_system_message = get_system_message(form_data['messages']) + original_system_content = ( + get_content_from_message(original_system_message) if original_system_message else None + ) + + while len(tool_calls) > 0 and tool_call_retries < CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES: + tool_call_retries += 1 + + response_tool_calls = tool_calls.pop(0) + + # Append function_call items for each tool call + # (Responses API already has them from streaming, so skip duplicates) + existing_call_ids = {item.get('call_id') for item in output if item.get('type') == 'function_call'} + for tc in response_tool_calls: + call_id = tc.get('id', '') + if call_id not in existing_call_ids: + func = tc.get('function', {}) + output.append( + { + 'type': 'function_call', + 'id': call_id or output_id('fc'), + 'call_id': call_id, + 'name': func.get('name', ''), + 'arguments': func.get('arguments', '{}'), + 'status': 'in_progress', + } + ) + + await event_emitter( + { + 'type': 'chat:completion', + 'data': { + 'content': serialize_output(full_output()), + 'output': full_output(), + }, + } + ) + + tools = metadata.get('tools', {}) + + results = [] + + for tool_call in response_tool_calls: + tool_call_id = tool_call.get('id', '') + tool_function_name = tool_call.get('function', {}).get('name', '') + tool_args = tool_call.get('function', {}).get('arguments', '{}') + + tool_function_params = {} + if tool_args and tool_args.strip(): + try: + # json.loads cannot be used because some models do not produce valid JSON + tool_function_params = ast.literal_eval(tool_args) + except Exception as e: + log.debug(e) + # Fallback to JSON parsing + try: + tool_function_params = json.loads(tool_args) + except Exception as e: + log.error(f'Error parsing tool call arguments: {tool_args}') + results.append( + { + 'tool_call_id': tool_call_id, + 'content': f'Error: Tool call arguments could not be parsed. The model generated malformed or incomplete JSON for `{tool_function_name}`. Please try again.', + } + ) + continue + + # Ensure arguments are valid JSON for downstream LLM integrations + log.debug(f'Parsed args from {tool_args} to {tool_function_params}') + tool_call.setdefault('function', {})['arguments'] = json.dumps(tool_function_params) + + tool_result = None + tool = None + tool_type = None + direct_tool = False + + if tool_function_name in tools: + tool = tools[tool_function_name] + spec = tool.get('spec', {}) + + tool_type = tool.get('type', '') + direct_tool = tool.get('direct', False) + + try: + allowed_params = spec.get('parameters', {}).get('properties', {}).keys() + + tool_function_params = { + k: v for k, v in tool_function_params.items() if k in allowed_params + } + + if direct_tool: + tool_result = await event_caller( + { + 'type': 'execute:tool', + 'data': { + 'id': str(uuid4()), + 'name': tool_function_name, + 'params': tool_function_params, + 'server': tool.get('server', {}), + 'session_id': metadata.get('session_id', None), + }, + } + ) + + else: + tool_function = await get_updated_tool_function( + function=tool['callable'], + extra_params={ + '__messages__': form_data.get('messages', []), + '__files__': metadata.get('files', []), + }, + ) + + tool_result = await tool_function(**tool_function_params) + + except Exception as e: + tool_result = str(e) + + tool_result, tool_result_files, tool_result_embeds = await process_tool_result( + request, + tool_function_name, + tool_result, + tool_type, + direct_tool, + metadata, + user, + ) + + await terminal_event_handler( + tool_function_name, + tool_function_params, + tool_result, + event_emitter, + ) + + # Extract citation sources from tool results + if ( + citations_enabled + and tool_function_name + in [ + 'search_web', + 'fetch_url', + 'view_file', + 'view_knowledge_file', + 'query_knowledge_files', + ] + and tool_result + ): + try: + citation_sources = get_citation_source_from_tool_result( + tool_name=tool_function_name, + tool_params=tool_function_params, + tool_result=tool_result, + tool_id=tool.get('tool_id', '') if tool else '', + ) + tool_call_sources.extend(citation_sources) + except Exception as e: + log.exception(f'Error extracting citation source: {e}') + + results.append( + { + 'tool_call_id': tool_call_id, + 'content': str(tool_result) if tool_result else '', + **({'files': tool_result_files} if tool_result_files else {}), + **({'embeds': tool_result_embeds} if tool_result_embeds else {}), + } + ) + + # Update function_call statuses and append function_call_output items + for tc in response_tool_calls: + call_id = tc.get('id', '') + # Mark function_call as completed + for item in output: + if item.get('type') == 'function_call' and item.get('call_id') == call_id: + item['status'] = 'completed' + # Update arguments with parsed/sanitized version + item['arguments'] = tc.get('function', {}).get('arguments', '{}') + break + + for result in results: + output_parts = [{'type': 'input_text', 'text': result.get('content', '')}] + + # Separate image data URIs (for LLM via input_image) from + # other files (for frontend display via files attribute). + display_files = [] + for file_item in result.get('files', []): + if file_item.get('type') == 'image' and file_item.get('url', '').startswith('data:'): + # LLM-only: add as input_image part (invisible to serialize_output) + output_parts.append({'type': 'input_image', 'image_url': file_item['url']}) + else: + # Frontend display (MCP images, audio, etc.) + display_files.append(file_item) + + output.append( + { + 'type': 'function_call_output', + 'id': output_id('fco'), + 'call_id': result.get('tool_call_id', ''), + 'output': output_parts, + 'status': 'completed', + **({'files': display_files} if display_files else {}), + **({'embeds': result.get('embeds')} if result.get('embeds') else {}), + } + ) + + # Append a new empty message item for the next response + output.append( + { + 'type': 'message', + 'id': output_id('msg'), + 'status': 'in_progress', + 'role': 'assistant', + 'content': [{'type': 'output_text', 'text': ''}], + } + ) + + # Emit citation sources to the frontend for display + if citations_enabled: + for source in tool_call_sources: + await event_emitter({'type': 'source', 'data': source}) + + # Apply tool source context to messages for the model. + # Restoring to pre-RAG original prevents duplicating + # the RAG template across file and tool sources. + all_tool_call_sources.extend(tool_call_sources) + if all_tool_call_sources and user_message: + # Restore pre-RAG message state before re-applying + # to prevent RAG template duplication. + original_user_message = metadata.get('user_prompt') or user_message + set_last_user_message_content( + original_user_message, + form_data['messages'], + ) + replace_system_message_content( + original_system_content or '', + form_data['messages'], + ) + + # Build context: file sources with content, + # tool sources as citation markers only. + source_ids = {} + source_context = get_source_context( + metadata.get('sources', []), source_ids + ) + get_source_context( + all_tool_call_sources, + source_ids, + include_content=False, + ) + source_context = source_context.strip() + if source_context: + rag_content = rag_template( + request.app.state.config.RAG_TEMPLATE, + source_context, + user_message, + ) + if RAG_SYSTEM_CONTEXT: + form_data['messages'] = add_or_update_system_message( + rag_content, + form_data['messages'], + append=True, + ) + else: + form_data['messages'] = add_or_update_user_message( + rag_content, + form_data['messages'], + append=False, + ) + tool_call_sources.clear() + + # Strip input_image parts (large base64 data URIs) from the + # output sent to the frontend — they're only for LLM consumption + # via convert_output_to_messages. + frontend_output = [] + for item in output: + if item.get('type') == 'function_call_output': + parts = item.get('output', []) + if any(p.get('type') == 'input_image' for p in parts): + item = {**item, 'output': [p for p in parts if p.get('type') != 'input_image']} + frontend_output.append(item) + + await event_emitter( + { + 'type': 'chat:completion', + 'data': { + 'content': serialize_output(output), + 'output': frontend_output, + }, + } + ) + + try: + new_form_data = { + **form_data, + 'model': model_id, + 'stream': True, + 'metadata': metadata, + } + + if ENABLE_RESPONSES_API_STATEFUL and last_response_id: + system_message = get_system_message(form_data['messages']) + new_form_data['messages'] = ( + [system_message] if system_message else [] + ) + convert_output_to_messages(output, raw=True) + new_form_data['previous_response_id'] = last_response_id + else: + tool_messages = convert_output_to_messages(output, raw=True) + + # Chat Completions providers don't support multimodal + # tool messages. Extract images into a user message. + image_urls = [] + for message in tool_messages: + if message.get('role') == 'tool' and isinstance(message.get('content'), list): + text_parts = [] + for part in message['content']: + if part.get('type') == 'input_text': + text_parts.append(part.get('text', '')) + elif part.get('type') == 'input_image': + image_urls.append(part.get('image_url', '')) + message['content'] = ''.join(text_parts) + + new_form_data['messages'] = [ + *form_data['messages'], + *tool_messages, + ] + + if image_urls: + new_form_data['messages'].append( + { + 'role': 'user', + 'content': [ + { + 'type': 'text', + 'text': 'Here are the images from the tool results above. Please analyze them.', + }, + *[{'type': 'image_url', 'image_url': {'url': url}} for url in image_urls], + ], + } + ) + + res = await generate_chat_completion( + request, + new_form_data, + user, + bypass_system_prompt=True, + ) + + 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. + prior_output = list(output) + # Trim the trailing empty placeholder message + # so it doesn't persist as a ghost item once + # the new stream produces real content. + if ( + prior_output + and prior_output[-1].get('type') == 'message' + and prior_output[-1].get('status') == 'in_progress' + ): + msg_parts = prior_output[-1].get('content', []) + if not msg_parts or (len(msg_parts) == 1 and not msg_parts[0].get('text', '').strip()): + prior_output.pop() + output = [] + await stream_body_handler(res, new_form_data) + output[:0] = prior_output + prior_output = [] + else: + break + except Exception as e: + log.debug(e) + break + + if DETECT_CODE_INTERPRETER: + MAX_RETRIES = 5 + retries = 0 + + while output and output[-1].get('type') == 'open_webui:code_interpreter' and retries < MAX_RETRIES: + await event_emitter( + { + 'type': 'chat:completion', + 'data': { + 'content': serialize_output(output), + 'output': output, + }, + } + ) + + retries += 1 + log.debug(f'Attempt count: {retries}') + + ci_item = output[-1] + ci_output = '' + try: + if ci_item.get('attributes', {}).get('type') == 'code': + code = ci_item.get('code', '') + # Sanitize code (strips ANSI codes and markdown fences) + code = sanitize_code(code) + + if CODE_INTERPRETER_BLOCKED_MODULES: + blocking_code = textwrap.dedent( + f""" + import builtins + + BLOCKED_MODULES = {CODE_INTERPRETER_BLOCKED_MODULES} + + _real_import = builtins.__import__ + async def restricted_import(name, globals=None, locals=None, fromlist=(), level=0): + if name.split('.')[0] in BLOCKED_MODULES: + importer_name = globals.get('__name__') if globals else None + if importer_name == '__main__': + raise ImportError( + f"Direct import of module {{name}} is restricted." + ) + return _real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = restricted_import + """ + ) + code = blocking_code + '\n' + code + + if request.app.state.config.CODE_INTERPRETER_ENGINE == 'pyodide': + ci_output = await event_caller( + { + 'type': 'execute:python', + 'data': { + 'id': str(uuid4()), + 'code': code, + 'session_id': metadata.get('session_id', None), + 'files': metadata.get('files', []), + }, + } + ) + elif request.app.state.config.CODE_INTERPRETER_ENGINE == 'jupyter': + ci_output = await execute_code_jupyter( + request.app.state.config.CODE_INTERPRETER_JUPYTER_URL, + code, + ( + request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN + if request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH == 'token' + else None + ), + ( + request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD + if request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH == 'password' + else None + ), + request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT, + ) + else: + ci_output = {'stdout': 'Code interpreter engine not configured.'} + + log.debug(f'Code interpreter output: {ci_output}') + + if isinstance(ci_output, dict): + stdout = ci_output.get('stdout', '') + + if isinstance(stdout, str): + stdoutLines = stdout.split('\n') + for idx, line in enumerate(stdoutLines): + if re.match(r'data:image/\w+;base64', line): + image_url = await get_image_url_from_base64( + request, + line, + metadata, + user, + ) + if image_url: + stdoutLines[idx] = f'![Output Image]({image_url})' + + ci_output['stdout'] = '\n'.join(stdoutLines) + + result = ci_output.get('result', '') + + if isinstance(result, str): + resultLines = result.split('\n') + for idx, line in enumerate(resultLines): + if re.match(r'data:image/\w+;base64', line): + image_url = await get_image_url_from_base64( + request, + line, + metadata, + user, + ) + resultLines[idx] = f'![Output Image]({image_url})' + ci_output['result'] = '\n'.join(resultLines) + except Exception as e: + ci_output = str(e) + + ci_item['output'] = ci_output + ci_item['status'] = 'completed' + + output.append( + { + 'type': 'message', + 'id': output_id('msg'), + 'status': 'in_progress', + 'role': 'assistant', + 'content': [{'type': 'output_text', 'text': ''}], + } + ) + + await event_emitter( + { + 'type': 'chat:completion', + 'data': { + 'content': serialize_output(output), + 'output': output, + }, + } + ) + + try: + new_form_data = { + **form_data, + 'model': model_id, + 'stream': True, + 'metadata': metadata, + 'messages': [ + *form_data['messages'], + *convert_output_to_messages(output, raw=True), + ], + } + + res = await generate_chat_completion( + request, + new_form_data, + user, + bypass_system_prompt=True, + ) + + if isinstance(res, StreamingResponse): + await stream_body_handler(res, new_form_data) + else: + break + except Exception as e: + log.debug(e) + break + + # Mark all in-progress items as completed + for item in output: + if item.get('status') == 'in_progress': + item['status'] = 'completed' + + title = await Chats.get_chat_title_by_id(metadata['chat_id']) + data = { + 'done': True, + 'content': serialize_output(output), + 'output': output, + 'title': title, + **({'usage': usage} if usage else {}), + } + + if not ENABLE_REALTIME_CHAT_SAVE: + # Save message in the database + await Chats.upsert_message_to_chat_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], + { + 'done': True, + 'content': serialize_output(output), + 'output': output, + **({'usage': usage} if usage else {}), + }, + ) + elif usage: + await Chats.upsert_message_to_chat_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], + {'done': True, 'usage': usage}, + ) + else: + await Chats.upsert_message_to_chat_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], + {'done': True}, + ) + + # Send a webhook notification if the user is not active + if request.app.state.config.ENABLE_USER_WEBHOOKS and not await Users.is_user_active(user.id): + webhook_url = await Users.get_user_webhook_url_by_id(user.id) + if webhook_url: + await post_webhook( + request.app.state.WEBUI_NAME, + webhook_url, + f'{content}\n\n{title} - {request.app.state.config.WEBUI_URL}/c/{metadata["chat_id"]}', + { + 'action': 'chat', + 'message': content, + 'title': title, + 'url': f'{request.app.state.config.WEBUI_URL}/c/{metadata["chat_id"]}', + }, + ) + + await event_emitter( + { + 'type': 'chat:completion', + 'data': data, + } + ) + + await background_tasks_handler(ctx) + ctx['assistant_message'] = { + 'content': serialize_output(output), + 'output': output, + **({'usage': usage} if usage else {}), + } + await outlet_filter_handler(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. + if hasattr(response, 'body_iterator') and hasattr(response.body_iterator, 'aclose'): + try: + await asyncio.shield(response.body_iterator.aclose()) + except (asyncio.CancelledError, Exception): + pass + + async def save_cancelled_state(): + await event_emitter({'type': 'chat:tasks:cancel'}) + if not ENABLE_REALTIME_CHAT_SAVE: + await Chats.upsert_message_to_chat_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], + { + 'done': True, + 'content': serialize_output(output), + 'output': output, + }, + ) + else: + await Chats.upsert_message_to_chat_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], + {'done': True}, + ) + + try: + await asyncio.shield(save_cancelled_state()) + except (asyncio.CancelledError, Exception): + pass + raise # re-raise CancelledError for proper propagation + + if response.background is not None: + await response.background() + + return await response_handler(response, events) + + else: + # Fallback to the original response + async def stream_wrapper(original_generator, events): + def wrap_item(item): + return f'data: {item}\n\n' + + for event in events: + event, _ = await process_filter_functions( + request=request, + filter_functions=filter_functions, + filter_type='stream', + form_data=event, + extra_params=extra_params, + ) + + if event: + yield wrap_item(json.dumps(event)) + + async for data in original_generator: + data, _ = await process_filter_functions( + request=request, + filter_functions=filter_functions, + filter_type='stream', + form_data=data, + extra_params=extra_params, + ) + + if data: + yield data + + return StreamingResponse( + stream_wrapper(response.body_iterator, events), + headers=dict(response.headers), + background=response.background, + ) + + +async def process_chat_response(response, ctx): + # Non-streaming response + if not isinstance(response, StreamingResponse): + return await non_streaming_chat_response_handler(response, ctx) + + # Non standard response + if not any( + content_type in response.headers['Content-Type'] + for content_type in ['text/event-stream', 'application/x-ndjson'] + ): + return response + + # Streaming response + return await streaming_chat_response_handler(response, ctx) diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py new file mode 100644 index 0000000000000000000000000000000000000000..dec5dce94c7fac1169d9d3ab46b7b7e03df3dbb1 --- /dev/null +++ b/backend/open_webui/utils/misc.py @@ -0,0 +1,1026 @@ +import hashlib +import re +import threading +import time +import uuid +import logging +from datetime import timedelta +from pathlib import Path +from typing import Callable, Optional, Sequence, Union +import json +import aiohttp +import mimeparse + + +import collections.abc +from open_webui.env import CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE + +log = logging.getLogger(__name__) + + +def deep_update(d, u): + for k, v in u.items(): + if isinstance(v, collections.abc.Mapping): + d[k] = deep_update(d.get(k, {}), v) + else: + d[k] = v + return d + + +def get_allow_block_lists(filter_list): + allow_list = [] + block_list = [] + + if filter_list: + for d in filter_list: + if d.startswith('!'): + # Domains starting with "!" → blocked + block_list.append(d[1:].strip()) + else: + # Domains starting without "!" → allowed + allow_list.append(d.strip()) + + return allow_list, block_list + + +def is_string_allowed(string: Union[str, Sequence[str]], filter_list: Optional[list[str]] = None) -> bool: + """ + Checks if a string is allowed based on the provided filter list. + :param string: The string or sequence of strings to check (e.g., domain or hostname). + :param filter_list: List of allowed/blocked strings. Strings starting with "!" are blocked. + :return: True if the string or sequence of strings is allowed, False otherwise. + """ + if not filter_list: + return True + + allow_list, block_list = get_allow_block_lists(filter_list) + strings = [string] if isinstance(string, str) else list(string) + + # If allow list is non-empty, require domain to match one of them + if allow_list: + if not any(s.endswith(allowed) for s in strings for allowed in allow_list): + return False + + # Block list always removes matches + if any(s.endswith(blocked) for s in strings for blocked in block_list): + return False + + return True + + +def get_message_list(messages_map, message_id): + """ + Reconstructs a list of messages in order up to the specified message_id. + + :param message_id: ID of the message to reconstruct the chain + :param messages: Message history dict containing all messages + :return: List of ordered messages starting from the root to the given message + """ + + # Handle case where messages is None + if not messages_map: + return [] # Return empty list instead of None to prevent iteration errors + + # Find the message by its id + current_message = messages_map.get(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 + message_list = [] + visited_message_ids = set() + + while current_message: + message_id = current_message.get('id') + if message_id in visited_message_ids: + # Cycle detected, break to prevent infinite loop + break + + if message_id is not None: + visited_message_ids.add(message_id) + + message_list.append(current_message) + parent_id = current_message.get('parentId') # Use .get() for safety + current_message = messages_map.get(parent_id) if parent_id else None + + message_list.reverse() + return message_list + + +def get_messages_content(messages: list[dict]) -> str: + return '\n'.join([f'{message["role"].upper()}: {get_content_from_message(message)}' for message in messages]) + + +def get_last_user_message_item(messages: list[dict]) -> Optional[dict]: + for message in reversed(messages): + if message['role'] == 'user': + return message + return None + + +def get_content_from_message(message: dict) -> Optional[str]: + if isinstance(message.get('content'), list): + for item in message['content']: + if item['type'] == 'text': + return item['text'] + else: + return message.get('content') + return None + + +def convert_output_to_messages(output: list, raw: bool = False) -> list[dict]: + """ + Convert OR-aligned output items to OpenAI Chat Completion-format messages. + + This reconstructs the full conversation from the stored Responses API-native + output items, including assistant messages with tool_calls arrays and tool + role messages. + + Args: + output: List of OR-aligned output items (Responses API format). + raw: If True, include reasoning blocks (with original tags) and code + interpreter blocks for LLM re-processing follow-ups. + """ + if not output or not isinstance(output, list): + return [] + + messages = [] + pending_tool_calls = [] + pending_content = [] + + def flush_pending(): + nonlocal pending_content, pending_tool_calls + if pending_content or pending_tool_calls: + messages.append( + { + 'role': 'assistant', + 'content': '\n'.join(pending_content) if pending_content else '', + **({'tool_calls': pending_tool_calls} if pending_tool_calls else {}), + } + ) + pending_content = [] + pending_tool_calls = [] + + for item in output: + item_type = item.get('type', '') + + if item_type == 'message': + # Extract text from output_text content parts + content_parts = item.get('content', []) + text = '' + for part in content_parts: + if part.get('type') == 'output_text': + text += part.get('text', '') + if text: + pending_content.append(text) + + elif item_type == 'function_call': + # Collect tool calls to batch into assistant message + arguments = item.get('arguments', '{}') + # Ensure arguments is always a JSON string + if not isinstance(arguments, str): + arguments = json.dumps(arguments) + pending_tool_calls.append( + { + 'id': item.get('call_id', ''), + 'type': 'function', + 'function': { + 'name': item.get('name', ''), + 'arguments': arguments, + }, + } + ) + + elif item_type == 'function_call_output': + # Flush any pending content/tool_calls before adding tool result + flush_pending() + + # Extract text and images from output content parts + output_parts = item.get('output', []) + content = '' + image_urls = [] + for part in output_parts: + if part.get('type') == 'input_text': + output_text = part.get('text', '') + content += str(output_text) if not isinstance(output_text, str) else output_text + elif part.get('type') == 'input_image': + url = part.get('image_url', '') + if url: + image_urls.append(url) + + if image_urls: + # Multimodal tool content with image(s) + messages.append( + { + 'role': 'tool', + 'tool_call_id': item.get('call_id', ''), + 'content': [ + {'type': 'input_text', 'text': content}, + *[{'type': 'input_image', 'image_url': url} for url in image_urls], + ], + } + ) + else: + messages.append( + { + 'role': 'tool', + 'tool_call_id': item.get('call_id', ''), + 'content': content, + } + ) + + elif item_type == 'reasoning': + if raw: + # Include reasoning with original tags for LLM re-processing + reasoning_text = '' + source_list = item.get('summary', []) or item.get('content', []) + for part in source_list: + if part.get('type') == 'output_text': + reasoning_text += part.get('text', '') + elif 'text' in part: + reasoning_text += part.get('text', '') + + if reasoning_text: + start_tag = item.get('start_tag', '') + end_tag = item.get('end_tag', '') + pending_content.append(f'{start_tag}{reasoning_text}{end_tag}') + # NOTE: Some providers (e.g. Moonshot/Kimi K2.5) require + # reasoning_content as a top-level field on assistant + # messages. This should be handled externally via a + # pipeline filter or connection-level middleware, not + # here — adding it universally breaks strict providers + # (OpenAI, Vertex AI, Azure) that reject unknown fields. + # else: skip reasoning blocks for normal LLM messages + + elif item_type == 'open_webui:code_interpreter': + # Always include code interpreter content so the LLM knows + # the code was already executed and doesn't retry. + code = item.get('code', '') + code_output = item.get('output', '') + + if code: + pending_content.append(f'\n{code}\n') + + if code_output: + if isinstance(code_output, dict): + stdout = code_output.get('stdout', '') + result = code_output.get('result', '') + output_text = stdout or result + else: + output_text = str(code_output) + if output_text: + pending_content.append(f'\n{output_text}\n') + + elif item_type.startswith('open_webui:'): + # Skip other extension types + pass + + # Flush remaining content/tool_calls + flush_pending() + + return messages + + +def get_last_user_message(messages: list[dict]) -> Optional[str]: + message = get_last_user_message_item(messages) + if message is None: + return None + return get_content_from_message(message) + + +def set_last_user_message_content(content: str, messages: list[dict]) -> list[dict]: + """ + Replace the text content of the last user message in-place. + Handles both plain-string and list-of-parts content formats. + """ + for message in reversed(messages): + if message.get('role') == 'user': + if isinstance(message.get('content'), list): + for item in message['content']: + if item.get('type') == 'text': + item['text'] = content + break + else: + message['content'] = content + break + return messages + + +def get_last_assistant_message_item(messages: list[dict]) -> Optional[dict]: + for message in reversed(messages): + if message['role'] == 'assistant': + return message + return None + + +def get_last_assistant_message(messages: list[dict]) -> Optional[str]: + for message in reversed(messages): + if message['role'] == 'assistant': + return get_content_from_message(message) + return None + + +def get_system_message(messages: list[dict]) -> Optional[dict]: + for message in messages: + if message['role'] == 'system': + return message + return None + + +def remove_system_message(messages: list[dict]) -> list[dict]: + return [message for message in messages if message['role'] != 'system'] + + +def pop_system_message(messages: list[dict]) -> tuple[Optional[dict], list[dict]]: + return get_system_message(messages), remove_system_message(messages) + + +def merge_system_messages(messages: list[dict]) -> list[dict]: + """ + Merge all system messages into one at position 0. + + Some chat templates (e.g. Qwen) require exactly one system + message at the start. Multiple pipeline stages may each + insert their own system message; this function consolidates + them. + """ + system_contents: list[str] = [] + other_messages: list[dict] = [] + + for message in messages: + if message.get('role') == 'system': + content = get_content_from_message(message) + if content: + system_contents.append(content) + else: + other_messages.append(message) + + if not system_contents: + return other_messages + + merged = {'role': 'system', 'content': '\n'.join(system_contents)} + return [merged, *other_messages] + + +def update_message_content(message: dict, content: str, append: bool = True) -> dict: + if isinstance(message['content'], list): + for item in message['content']: + if item['type'] == 'text': + if append: + item['text'] = f'{item["text"]}\n{content}' + else: + item['text'] = f'{content}\n{item["text"]}' + else: + if append: + message['content'] = f'{message["content"]}\n{content}' + else: + message['content'] = f'{content}\n{message["content"]}' + return message + + +def replace_system_message_content(content: str, messages: list[dict]) -> dict: + for message in messages: + if message['role'] == 'system': + message['content'] = content + break + return messages + + +def add_or_update_system_message(content: str, messages: list[dict], append: bool = False): + """ + Adds a new system message at the beginning of the messages list + or updates the existing system message at the beginning. + + :param msg: The message to be added or appended. + :param messages: The list of message dictionaries. + :return: The updated list of message dictionaries. + """ + + if messages and messages[0].get('role') == 'system': + messages[0] = update_message_content(messages[0], content, append) + else: + # Insert at the beginning + messages.insert(0, {'role': 'system', 'content': content}) + + return messages + + +def add_or_update_user_message(content: str, messages: list[dict], append: bool = True): + """ + Adds a new user message at the end of the messages list + or updates the existing user message at the end. + + :param msg: The message to be added or appended. + :param messages: The list of message dictionaries. + :return: The updated list of message dictionaries. + """ + + if messages and messages[-1].get('role') == 'user': + messages[-1] = update_message_content(messages[-1], content, append) + else: + # Insert at the end + messages.append({'role': 'user', 'content': content}) + + return messages + + +def prepend_to_first_user_message_content(content: str, messages: list[dict]) -> list[dict]: + for message in messages: + if message['role'] == 'user': + message = update_message_content(message, content, append=False) + break + return messages + + +def append_or_update_assistant_message(content: str, messages: list[dict]): + """ + Adds a new assistant message at the end of the messages list + or updates the existing assistant message at the end. + + :param msg: The message to be added or appended. + :param messages: The list of message dictionaries. + :return: The updated list of message dictionaries. + """ + + if messages and messages[-1].get('role') == 'assistant': + messages[-1]['content'] = f'{messages[-1]["content"]}\n{content}' + else: + # Insert at the end + messages.append({'role': 'assistant', 'content': content}) + + return messages + + +def strip_empty_content_blocks(messages: list[dict]) -> list[dict]: + """ + Remove empty text content blocks from multimodal message content arrays. + + Providers like Gemini and Claude reject messages where a text block has + an empty string. This can happen when a user sends only file/image + attachments without typing any text. + """ + for message in messages: + content = message.get('content') + if isinstance(content, list): + cleaned = [ + block + for block in content + if not (isinstance(block, dict) and block.get('type') == 'text' and not block.get('text', '').strip()) + ] + if cleaned: + message['content'] = cleaned + return messages + + +def openai_chat_message_template(model: str): + return { + 'id': f'{model}-{str(uuid.uuid4())}', + 'created': int(time.time()), + 'model': model, + 'choices': [{'index': 0, 'logprobs': None, 'finish_reason': None}], + } + + +def openai_chat_chunk_message_template( + model: str, + content: Optional[str] = None, + reasoning_content: Optional[str] = None, + tool_calls: Optional[list[dict]] = None, + usage: Optional[dict] = None, +) -> dict: + template = openai_chat_message_template(model) + template['object'] = 'chat.completion.chunk' + + template['choices'][0]['index'] = 0 + template['choices'][0]['delta'] = {} + + if content: + template['choices'][0]['delta']['content'] = content + + if reasoning_content: + template['choices'][0]['delta']['reasoning_content'] = reasoning_content + + if tool_calls: + template['choices'][0]['delta']['tool_calls'] = tool_calls + + if not content and not reasoning_content and not tool_calls: + template['choices'][0]['finish_reason'] = 'stop' + + if usage: + template['usage'] = usage + return template + + +def openai_chat_completion_message_template( + model: str, + message: Optional[str] = None, + reasoning_content: Optional[str] = None, + tool_calls: Optional[list[dict]] = None, + usage: Optional[dict] = None, +) -> dict: + template = openai_chat_message_template(model) + template['object'] = 'chat.completion' + if message is not None: + template['choices'][0]['message'] = { + 'role': 'assistant', + 'content': message, + **({'reasoning_content': reasoning_content} if reasoning_content else {}), + **({'tool_calls': tool_calls} if tool_calls else {}), + } + + template['choices'][0]['finish_reason'] = 'tool_calls' if tool_calls else 'stop' + + if usage: + template['usage'] = usage + return template + + +def get_gravatar_url(email): + # Trim leading and trailing whitespace from + # an email address and force all characters + # to lower case + address = str(email).strip().lower() + + # Create a SHA256 hash of the final string + hash_object = hashlib.sha256(address.encode()) + hash_hex = hash_object.hexdigest() + + # Grab the actual image 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. +def calculate_sha256(file_path, chunk_size): + # Compute SHA-256 hash of a file efficiently in chunks + sha256 = hashlib.sha256() + with open(file_path, 'rb') as f: + while chunk := f.read(chunk_size): + sha256.update(chunk) + return sha256.hexdigest() + + +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 + sha256_hash.update(string.encode('utf-8')) + # Get the hexadecimal representation of the hash + hashed_string = sha256_hash.hexdigest() + return hashed_string + + +def validate_email_format(email: str) -> bool: + if email.endswith('@localhost'): + return True + + return bool(re.match(r'[^@]+@[^@]+\.[^@]+', email)) + + +def sanitize_filename(file_name): + # Convert to lowercase + lower_case_file_name = file_name.lower() + + # Remove special characters using regular expression + sanitized_file_name = re.sub(r'[^\w\s]', '', lower_case_file_name) + + # Replace spaces with dashes + final_file_name = re.sub(r'\s+', '-', sanitized_file_name) + + return final_file_name + + +def sanitize_text_for_db(text: str) -> str: + """Remove null bytes and invalid UTF-8 surrogates from text for PostgreSQL storage.""" + if not isinstance(text, str): + return text + # Fast path: skip work when there are no null bytes (the common case) + if '\x00' not in text: + return text + # Remove null bytes + text = text.replace('\x00', '').replace('\u0000', '') + # Remove invalid UTF-8 surrogate characters that can cause encoding errors + # This handles cases where binary data or encoding issues introduced surrogates + try: + text = text.encode('utf-8', errors='surrogatepass').decode('utf-8', errors='ignore') + except (UnicodeEncodeError, UnicodeDecodeError): + pass + return text + + +def _strip_null_bytes_deep(obj): + """Inner recursive walk — only called when null bytes are known to be present.""" + if isinstance(obj, str): + return sanitize_text_for_db(obj) + elif isinstance(obj, dict): + return {k: _strip_null_bytes_deep(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [_strip_null_bytes_deep(v) for v in obj] + return obj + + +def sanitize_data_for_db(obj): + """Recursively sanitize all strings in a data structure for database storage. + + Performs a fast pre-check: serializes the structure once and scans for + null bytes. If none are found (the overwhelmingly common case), the + original object is returned immediately, skipping the expensive + recursive walk. + """ + if isinstance(obj, str): + return sanitize_text_for_db(obj) + # Fast path: check for null bytes in the serialized form. + # json.dumps is implemented in C and much faster than a Python-level + # recursive walk over every leaf string. + try: + if '\x00' not in json.dumps(obj, ensure_ascii=False): + return obj + except (TypeError, ValueError): + pass + return _strip_null_bytes_deep(obj) + + +def sanitize_metadata(metadata: dict) -> dict: + """ + Return a JSON-safe copy of a metadata dict for database storage. + + The middleware metadata accumulates non-serializable Python objects + (e.g. callable tool functions, MCP client instances) that cause + PostgreSQL JSON inserts to fail. This helper strips those out while + preserving the primitive data needed for file-to-chat linking. + """ + if not isinstance(metadata, dict): + return metadata + + def _sanitize(obj): + if isinstance(obj, (str, int, float, bool, type(None))): + return obj + if isinstance(obj, dict): + return {k: _sanitize(v) for k, v in obj.items() if not callable(v) and _is_serializable(v)} + if isinstance(obj, list): + return [_sanitize(v) for v in obj if not callable(v) and _is_serializable(v)] + if callable(obj): + return None + # Last resort: try to see if it's serializable + try: + json.dumps(obj) + return obj + except (TypeError, ValueError): + return None + + def _is_serializable(obj): + """Quick check whether a value can survive JSON serialization.""" + if isinstance(obj, (str, int, float, bool, type(None), dict, list)): + return True + try: + json.dumps(obj) + return True + except (TypeError, ValueError): + return False + + return _sanitize(metadata) + + +def extract_folders_after_data_docs(path): + # Convert the path to a Path object if it's not already + path = Path(path) + + # Extract parts of the path + parts = path.parts + + # Find the index of '/data/docs' in the path + try: + index_data_docs = parts.index('data') + 1 + index_docs = parts.index('docs', index_data_docs) + 1 + except ValueError: + return [] + + # Exclude the filename and accumulate folder names + tags = [] + + folders = parts[index_docs:-1] + for idx, _ in enumerate(folders): + tags.append('/'.join(folders[: idx + 1])) + + return tags + + +def parse_duration(duration: str) -> Optional[timedelta]: + if duration == '-1' or duration == '0': + return None + + # Regular expression to find number and unit pairs + pattern = r'(-?\d+(\.\d+)?)(ms|s|m|h|d|w)' + matches = re.findall(pattern, duration) + + if not matches: + raise ValueError('Invalid duration string') + + total_duration = timedelta() + + for number, _, unit in matches: + number = float(number) + if unit == 'ms': + total_duration += timedelta(milliseconds=number) + elif unit == 's': + total_duration += timedelta(seconds=number) + elif unit == 'm': + total_duration += timedelta(minutes=number) + elif unit == 'h': + total_duration += timedelta(hours=number) + elif unit == 'd': + total_duration += timedelta(days=number) + elif unit == 'w': + total_duration += timedelta(weeks=number) + + return total_duration + + +def parse_ollama_modelfile(model_text): + parameters_meta = { + 'mirostat': int, + 'mirostat_eta': float, + 'mirostat_tau': float, + 'num_ctx': int, + 'repeat_last_n': int, + 'repeat_penalty': float, + 'temperature': float, + 'seed': int, + 'tfs_z': float, + 'num_predict': int, + 'top_k': int, + 'top_p': float, + 'num_keep': int, + 'presence_penalty': float, + 'frequency_penalty': float, + 'num_batch': int, + 'num_gpu': int, + 'use_mmap': bool, + 'use_mlock': bool, + 'num_thread': int, + } + + data = {'base_model_id': None, 'params': {}} + + # Parse base model + base_model_match = re.search(r'^FROM\s+(\w+)', model_text, re.MULTILINE | re.IGNORECASE) + if base_model_match: + data['base_model_id'] = base_model_match.group(1) + + # Parse template + template_match = re.search(r'TEMPLATE\s+"""(.+?)"""', model_text, re.DOTALL | re.IGNORECASE) + if template_match: + data['params'] = {'template': template_match.group(1).strip()} + + # Parse stops + stops = re.findall(r'PARAMETER stop "(.*?)"', model_text, re.IGNORECASE) + if stops: + data['params']['stop'] = stops + + # Parse other parameters from the provided list + for param, param_type in parameters_meta.items(): + param_match = re.search(rf'PARAMETER {param} (.+)', model_text, re.IGNORECASE) + if param_match: + value = param_match.group(1) + + try: + if param_type is int: + value = int(value) + elif param_type is float: + value = float(value) + elif param_type is bool: + value = value.lower() == 'true' + except Exception as e: + log.exception(f'Failed to parse parameter {param}: {e}') + continue + + data['params'][param] = value + + # Parse adapter + adapter_match = re.search(r'ADAPTER (.+)', model_text, re.IGNORECASE) + if adapter_match: + data['params']['adapter'] = adapter_match.group(1) + + # Parse system description + system_desc_match = re.search(r'SYSTEM\s+"""(.+?)"""', model_text, re.DOTALL | re.IGNORECASE) + system_desc_match_single = re.search(r'SYSTEM\s+([^\n]+)', model_text, re.IGNORECASE) + + if system_desc_match: + data['params']['system'] = system_desc_match.group(1).strip() + elif system_desc_match_single: + data['params']['system'] = system_desc_match_single.group(1).strip() + + # Parse messages + messages = [] + message_matches = re.findall(r'MESSAGE (\w+) (.+)', model_text, re.IGNORECASE) + for role, content in message_matches: + messages.append({'role': role, 'content': content}) + + if messages: + data['params']['messages'] = messages + + return data + + +def convert_logit_bias_input_to_json(logit_bias_input) -> Optional[str]: + if not logit_bias_input: + return None + + if isinstance(logit_bias_input, dict): + return json.dumps(logit_bias_input) + + logit_bias_pairs = logit_bias_input.split(',') + logit_bias_json = {} + for pair in logit_bias_pairs: + token, bias = pair.split(':') + token = str(token.strip()) + bias = int(bias.strip()) + bias = 100 if bias > 100 else -100 if bias < -100 else bias + logit_bias_json[token] = bias + return json.dumps(logit_bias_json) + + +def freeze(value): + """ + Freeze a value to make it hashable. + """ + if isinstance(value, dict): + return frozenset((k, freeze(v)) for k, v in value.items()) + elif isinstance(value, list): + return tuple(freeze(v) for v in value) + return value + + +def throttle(interval: float = 10.0): + """ + Decorator to prevent a function from being called more than once within a specified duration. + If the function is called again within the duration, it returns None. To avoid returning + different types, the return type of the function should be Optional[T]. + + :param interval: Duration in seconds to wait before allowing the function to be called again. + """ + + def decorator(func): + last_calls = {} + lock = threading.Lock() + + async def wrapper(*args, **kwargs): + if interval is None: + return await func(*args, **kwargs) + + key = (args, freeze(kwargs)) + now = time.time() + if now - last_calls.get(key, 0) < interval: + return None + with lock: + if now - last_calls.get(key, 0) < interval: + return None + last_calls[key] = now + return await func(*args, **kwargs) + + return wrapper + + return decorator + + +def strict_match_mime_type(supported: list[str] | str, header: str) -> Optional[str]: + """ + Strictly match the mime type with the supported mime types. + + :param supported: The supported mime types. + :param header: The header to match. + :return: The matched mime type or None if no match is found. + """ + + try: + if isinstance(supported, str): + supported = supported.split(',') + + supported = [s for s in supported if s.strip() and '/' in s] + + if len(supported) == 0: + # Default to common types if none are specified + supported = ['audio/*', 'video/webm'] + + match = mimeparse.best_match(supported, header) + if not match: + return None + + _, _, match_params = mimeparse.parse_mime_type(match) + _, _, header_params = mimeparse.parse_mime_type(header) + for k, v in match_params.items(): + if header_params.get(k) != v: + return None + + return match + except Exception as e: + log.exception(f'Failed to match mime type {header}: {e}') + return None + + +def extract_urls(text: str) -> list[str]: + # Regex pattern to match URLs + url_pattern = re.compile(r'(https?://[^\s]+)', re.IGNORECASE) # Matches http and https URLs + 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. +async def cleanup_response( + response: Optional[aiohttp.ClientResponse], + session: Optional[aiohttp.ClientSession], +): + if response: + if not response.closed: + # aiohttp 3.9+ made ClientResponse.close() synchronous (returns None). + # Older versions returned a coroutine. Handle both gracefully. + result = response.close() + if result is not None: + await result + if session: + if not session.closed: + result = session.close() + if result is not None: + await result + + +async def stream_wrapper(response, session, content_handler=None): + """ + Wrap a stream to ensure cleanup happens even if streaming is interrupted. + This is more reliable than BackgroundTask which may not run if client disconnects. + """ + try: + stream = content_handler(response.content) if content_handler else response.content + async for chunk in stream: + yield chunk + finally: + await cleanup_response(response, session) + + +def stream_chunks_handler(stream: aiohttp.StreamReader): + """ + Handle stream response chunks, supporting large data chunks that exceed the original 16kb limit. + When a single line exceeds max_buffer_size, returns an empty JSON string {} and skips subsequent data + until encountering normally sized data. + + :param stream: The stream reader to handle. + :return: An async generator that yields the stream data. + """ + + max_buffer_size = CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE + if max_buffer_size is None or max_buffer_size <= 0: + return stream + + async def yield_safe_stream_chunks(): + buffer = b'' + skip_mode = False + + async for data, _ in stream.iter_chunks(): + if not data: + continue + + # In skip_mode, if buffer already exceeds the limit, clear it (it's part of an oversized line) + if skip_mode and len(buffer) > max_buffer_size: + buffer = b'' + + lines = (buffer + data).split(b'\n') + + # Process complete lines (except the last possibly incomplete fragment) + for i in range(len(lines) - 1): + line = lines[i] + + if skip_mode: + # Skip mode: check if current line is small enough to exit skip mode + if len(line) <= max_buffer_size: + skip_mode = False + yield line + else: + yield b'data: {}\n' + else: + # Normal mode: check if line exceeds limit + if len(line) > max_buffer_size: + skip_mode = True + yield b'data: {}\n' + log.info(f'Skip mode triggered, line size: {len(line)}') + else: + yield line + b'\n' + + # Save the last incomplete fragment + buffer = lines[-1] + + # Check if buffer exceeds limit + if not skip_mode and len(buffer) > max_buffer_size: + skip_mode = True + log.info(f'Skip mode triggered, buffer size: {len(buffer)}') + # Clear oversized buffer to prevent unlimited growth + buffer = b'' + + # Process remaining buffer data + if buffer and not skip_mode: + yield buffer + b'\n' + + return yield_safe_stream_chunks() diff --git a/backend/open_webui/utils/models.py b/backend/open_webui/utils/models.py new file mode 100644 index 0000000000000000000000000000000000000000..cc8c5fad3aea824cc8a2a8d7ec68115432f36bce --- /dev/null +++ b/backend/open_webui/utils/models.py @@ -0,0 +1,470 @@ +import copy +import time +import logging +import asyncio +import sys + +from aiocache import cached +from fastapi import Request + +from open_webui.socket.utils import RedisDict +from open_webui.routers import openai, ollama +from open_webui.functions import get_function_models + + +from open_webui.models.functions import Functions +from open_webui.models.models import Models +from open_webui.models.access_grants import AccessGrants +from open_webui.models.groups import Groups + + +from open_webui.utils.plugin import ( + load_function_module_by_id, + get_function_module_from_cache, +) +from open_webui.utils.access_control import has_access, has_base_model_access + + +from open_webui.config import ( + BYPASS_ADMIN_ACCESS_CONTROL, + DEFAULT_ARENA_MODEL, +) + +from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, GLOBAL_LOG_LEVEL +from open_webui.models.users import UserModel + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) +log = logging.getLogger(__name__) + + +async def fetch_ollama_models(request: Request, user: UserModel = None): + raw_ollama_models = await ollama.get_all_models(request, user=user) + return [ + { + 'id': model['model'], + 'name': model['name'], + 'object': 'model', + 'created': int(time.time()), + 'owned_by': 'ollama', + 'ollama': model, + 'connection_type': model.get('connection_type', 'local'), + 'tags': model.get('tags', []), + } + for model in raw_ollama_models['models'] + ] + + +async def fetch_openai_models(request: Request, user: UserModel = None): + openai_response = await openai.get_all_models(request, user=user) + return openai_response['data'] + + +async def get_all_base_models(request: Request, user: UserModel = None): + openai_task = ( + fetch_openai_models(request, user) + if request.app.state.config.ENABLE_OPENAI_API + else asyncio.sleep(0, result=[]) + ) + ollama_task = ( + fetch_ollama_models(request, user) + if request.app.state.config.ENABLE_OLLAMA_API + else asyncio.sleep(0, result=[]) + ) + function_task = get_function_models(request) + + openai_models, ollama_models, function_models = await asyncio.gather(openai_task, ollama_task, function_task) + + return function_models + openai_models + ollama_models + + +async def get_all_models(request, refresh: bool = False, user: UserModel = None): + if ( + request.app.state.MODELS + and request.app.state.BASE_MODELS + and (request.app.state.config.ENABLE_BASE_MODELS_CACHE and not refresh) + ): + base_models = request.app.state.BASE_MODELS + else: + base_models = await get_all_base_models(request, user=user) + request.app.state.BASE_MODELS = base_models + + # deep copy the base models to avoid modifying the original list + models = [model.copy() for model in base_models] + + # If there are no models, return an empty list + if len(models) == 0: + return [] + + # Add arena models + if request.app.state.config.ENABLE_EVALUATION_ARENA_MODELS: + arena_models = [] + if len(request.app.state.config.EVALUATION_ARENA_MODELS) > 0: + arena_models = [ + { + 'id': model['id'], + 'name': model['name'], + 'info': { + 'meta': model['meta'], + }, + 'object': 'model', + 'created': int(time.time()), + 'owned_by': 'arena', + 'arena': True, + } + for model in request.app.state.config.EVALUATION_ARENA_MODELS + ] + else: + # Add default arena model + arena_models = [ + { + 'id': DEFAULT_ARENA_MODEL['id'], + 'name': DEFAULT_ARENA_MODEL['name'], + 'info': { + 'meta': DEFAULT_ARENA_MODEL['meta'], + }, + 'object': 'model', + 'created': int(time.time()), + 'owned_by': 'arena', + 'arena': True, + } + ] + models = models + arena_models + + global_action_ids = {function.id for function in await Functions.get_global_action_functions()} + enabled_action_ids = {function.id for function in await Functions.get_functions_by_type('action', active_only=True)} + + global_filter_ids = {function.id for function in await Functions.get_global_filter_functions()} + enabled_filter_ids = {function.id for function in await Functions.get_functions_by_type('filter', active_only=True)} + + custom_models = await Models.get_all_models() + + # Single O(1) lookup: Ollama base names first, then exact IDs (exact wins). + base_model_lookup = {} + for model in models: + if model.get('owned_by') == 'ollama': + base_model_lookup.setdefault(model['id'].split(':')[0], model) + base_model_lookup[model['id']] = model + + existing_ids = {m['id'] for m in models} + + for custom_model in custom_models: + if custom_model.base_model_id is None: + # Override applied directly to a base model (shares the same ID) + model = base_model_lookup.get(custom_model.id) + + if model: + if custom_model.is_active: + model['name'] = custom_model.name + model['info'] = custom_model.model_dump() + + action_ids = [] + filter_ids = [] + + if 'info' in model: + if 'meta' in model['info']: + action_ids.extend(model['info']['meta'].get('actionIds', [])) + filter_ids.extend(model['info']['meta'].get('filterIds', [])) + + if 'params' in model['info']: + del model['info']['params'] + + model['action_ids'] = action_ids + model['filter_ids'] = filter_ids + else: + models.remove(model) + + elif custom_model.is_active: + if custom_model.id in existing_ids: + continue + + owned_by = 'openai' + connection_type = None + pipe = None + + base_model = base_model_lookup.get(custom_model.base_model_id) + if base_model is None: + base_model = base_model_lookup.get(custom_model.base_model_id.split(':')[0]) + if base_model: + owned_by = base_model.get('owned_by', 'unknown') + if 'pipe' in base_model: + pipe = base_model['pipe'] + connection_type = base_model.get('connection_type', None) + + model = { + 'id': f'{custom_model.id}', + 'name': custom_model.name, + 'object': 'model', + 'created': custom_model.created_at, + 'owned_by': owned_by, + 'connection_type': connection_type, + 'preset': True, + **({'pipe': pipe} if pipe is not None else {}), + } + + info = custom_model.model_dump() + if 'params' in info: + # Remove params to avoid exposing sensitive info + del info['params'] + + model['info'] = info + + action_ids = [] + filter_ids = [] + + if custom_model.meta: + meta = custom_model.meta.model_dump() + + if 'actionIds' in meta: + action_ids.extend(meta['actionIds']) + + if 'filterIds' in meta: + filter_ids.extend(meta['filterIds']) + + model['action_ids'] = action_ids + model['filter_ids'] = filter_ids + + models.append(model) + + # Process action_ids to get the actions + def get_action_items_from_module(function, module): + actions = [] + if hasattr(module, 'actions'): + actions = module.actions + return [ + { + 'id': f'{function.id}.{action["id"]}', + 'name': action.get('name', f'{function.name} ({action["id"]})'), + 'description': function.meta.description, + 'icon': action.get( + 'icon_url', + function.meta.manifest.get('icon_url', None) + or getattr(module, 'icon_url', None) + or getattr(module, 'icon', None), + ), + } + for action in actions + ] + else: + return [ + { + 'id': function.id, + 'name': function.name, + 'description': function.meta.description, + 'icon': function.meta.manifest.get('icon_url', None) + or getattr(module, 'icon_url', None) + or getattr(module, 'icon', None), + } + ] + + # Process filter_ids to get the filters + def get_filter_items_from_module(function, module): + return [ + { + 'id': function.id, + 'name': function.name, + 'description': function.meta.description, + 'icon': function.meta.manifest.get('icon_url', None) + or getattr(module, 'icon_url', None) + or getattr(module, 'icon', None), + 'has_user_valves': hasattr(module, 'UserValves'), + } + ] + + # Batch-prefetch all needed function records to avoid N+1 queries + all_function_ids = set() + for model in models: + all_function_ids.update(model.get('action_ids', [])) + all_function_ids.update(model.get('filter_ids', [])) + all_function_ids.update(global_action_ids) + all_function_ids.update(global_filter_ids) + + 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. + for function_id, function in functions_by_id.items(): + try: + await get_function_module_from_cache(request, function_id, function=function) + except Exception as e: + log.debug(f'Failed to load function module for {function_id}: {e}') + + # Apply global model defaults to all models + # Per-model overrides take precedence over global defaults + default_metadata = getattr(request.app.state.config, 'DEFAULT_MODEL_METADATA', None) or {} + + if default_metadata: + for model in models: + info = model.get('info') + + if info is None: + model['info'] = {'meta': copy.deepcopy(default_metadata)} + continue + + meta = info.setdefault('meta', {}) + for key, value in default_metadata.items(): + if key == 'capabilities': + # Merge capabilities: defaults as base, per-model overrides win + existing = meta.get('capabilities') or {} + meta['capabilities'] = {**value, **existing} + elif meta.get(key) is None: + meta[key] = copy.deepcopy(value) + + # Batch-fetch all function valves in one query to avoid N+1 DB hits + # inside get_action_priority (previously called per action × per model). + all_function_valves = await Functions.get_function_valves_by_ids(list(all_function_ids)) + + def get_action_priority(action_id): + try: + function_module = request.app.state.FUNCTIONS.get(action_id) + if function_module and hasattr(function_module, 'Valves'): + valves_db = all_function_valves.get(action_id) + valves = function_module.Valves(**(valves_db if valves_db else {})) + return getattr(valves, 'priority', 0) + except Exception: + pass + return 0 + + for model in models: + action_ids = [ + action_id + for action_id in set(model.pop('action_ids', [])) | global_action_ids + if action_id in enabled_action_ids + ] + action_ids.sort(key=lambda aid: (get_action_priority(aid), aid)) + + filter_ids = [ + filter_id + for filter_id in set(model.pop('filter_ids', [])) | global_filter_ids + if filter_id in enabled_filter_ids + ] + + model['actions'] = [] + for action_id in action_ids: + action_function = functions_by_id.get(action_id) + if action_function is None: + log.info(f'Action not found: {action_id}') + continue + + function_module = request.app.state.FUNCTIONS.get(action_id) + if function_module is None: + log.info(f'Failed to load action module: {action_id}') + continue + model['actions'].extend(get_action_items_from_module(action_function, function_module)) + + model['filters'] = [] + for filter_id in filter_ids: + filter_function = functions_by_id.get(filter_id) + if filter_function is None: + log.info(f'Filter not found: {filter_id}') + continue + + function_module = request.app.state.FUNCTIONS.get(filter_id) + if function_module is None: + log.info(f'Failed to load filter module: {filter_id}') + continue + if getattr(function_module, 'toggle', None): + model['filters'].extend(get_filter_items_from_module(filter_function, function_module)) + + log.debug(f'get_all_models() returned {len(models)} models') + + models_dict = {model['id']: model for model in models} + if isinstance(request.app.state.MODELS, RedisDict): + request.app.state.MODELS.set(models_dict) + else: + request.app.state.MODELS = models_dict + + return models + + +async def check_model_access(user, model, db=None): + if model.get('arena'): + meta = model.get('info', {}).get('meta', {}) + access_grants = meta.get('access_grants', []) + if not await has_access( + user.id, + permission='read', + access_grants=access_grants, + db=db, + ): + raise Exception('Model not found') + else: + model_info = await Models.get_model_by_id(model.get('id'), db=db) + if not model_info: + raise Exception('Model not found') + elif not ( + user.id == model_info.user_id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='model', + resource_id=model_info.id, + permission='read', + db=db, + ) + ): + raise Exception('Model not found') + + # Enforce access on chained base models + if not await has_base_model_access(user.id, model_info, db=db): + raise Exception('Model not found') + + +async def get_filtered_models(models, user, db=None): + # Filter out models that the user does not have access to + if ( + user.role == 'user' or (user.role == 'admin' and not BYPASS_ADMIN_ACCESS_CONTROL) + ) and not BYPASS_MODEL_ACCESS_CONTROL: + model_infos = {} + for model in models: + if model.get('arena'): + continue + info = model.get('info') + if info: + model_infos[model['id']] = info + + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} + + # Batch-fetch accessible resource IDs in a single query instead of N has_access calls + accessible_model_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type='model', + resource_ids=list(model_infos.keys()), + permission='read', + user_group_ids=user_group_ids, + db=db, + ) + + filtered_models = [] + for model in models: + if model.get('arena'): + meta = model.get('info', {}).get('meta', {}) + access_grants = meta.get('access_grants', []) + if await has_access( + user.id, + permission='read', + access_grants=access_grants, + user_group_ids=user_group_ids, + ): + filtered_models.append(model) + continue + + model_info = model_infos.get(model['id']) + if model_info: + if ( + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == model_info.get('user_id') + or model['id'] in accessible_model_ids + ): + filtered_models.append(model) + elif user.role == 'admin': + # No DB entry means no access control configured yet; + # only admins can see unconfigured models. + filtered_models.append(model) + + return filtered_models + else: + return models diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py new file mode 100644 index 0000000000000000000000000000000000000000..4a7d79d87c908379da6a7cd0bce92cd6d1e33fdb --- /dev/null +++ b/backend/open_webui/utils/oauth.py @@ -0,0 +1,1978 @@ +import base64 +import copy +import hashlib +import logging +import mimetypes +import sys +import urllib +import uuid +import json +from datetime import datetime, timedelta + +import re +import fnmatch +import time +import secrets +from cryptography.fernet import Fernet +from typing import Literal + +import aiohttp +from authlib.integrations.starlette_client import OAuth +from authlib.jose.errors import BadSignatureError +from authlib.oidc.core import UserInfo +from fastapi import ( + HTTPException, + status, +) +from starlette.responses import RedirectResponse +from typing import Optional + + +from open_webui.models.auths import Auths +from open_webui.models.oauth_sessions import OAuthSessions +from open_webui.models.users import Users + + +from open_webui.models.groups import Groups, GroupModel, GroupUpdateForm, GroupForm +from open_webui.config import ( + DEFAULT_USER_ROLE, + ENABLE_OAUTH_SIGNUP, + OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE, + OAUTH_MERGE_ACCOUNTS_BY_EMAIL, + OAUTH_PROVIDERS, + ENABLE_OAUTH_ROLE_MANAGEMENT, + ENABLE_OAUTH_GROUP_MANAGEMENT, + ENABLE_OAUTH_GROUP_CREATION, + OAUTH_GROUP_DEFAULT_SHARE, + OAUTH_BLOCKED_GROUPS, + OAUTH_GROUPS_SEPARATOR, + OAUTH_ROLES_SEPARATOR, + OAUTH_ROLES_CLAIM, + OAUTH_SUB_CLAIM, + OAUTH_GROUPS_CLAIM, + OAUTH_EMAIL_CLAIM, + OAUTH_PICTURE_CLAIM, + OAUTH_USERNAME_CLAIM, + OAUTH_ALLOWED_ROLES, + OAUTH_ADMIN_ROLES, + OAUTH_ALLOWED_DOMAINS, + OAUTH_UPDATE_PICTURE_ON_LOGIN, + OAUTH_UPDATE_NAME_ON_LOGIN, + OAUTH_UPDATE_EMAIL_ON_LOGIN, + OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID, + OAUTH_AUDIENCE, + OAUTH_AUTHORIZE_PARAMS, + WEBHOOK_URL, + JWT_EXPIRES_IN, + AppConfig, +) +from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES +from open_webui.env import ( + AIOHTTP_CLIENT_SESSION_SSL, + WEBUI_NAME, + WEBUI_AUTH_COOKIE_SAME_SITE, + WEBUI_AUTH_COOKIE_SECURE, + ENABLE_OAUTH_ID_TOKEN_COOKIE, + ENABLE_OAUTH_EMAIL_FALLBACK, + OAUTH_CLIENT_INFO_ENCRYPTION_KEY, + OAUTH_MAX_SESSIONS_PER_USER, + REDIS_KEY_PREFIX, +) +from open_webui.utils.misc import parse_duration +from open_webui.utils.auth import get_password_hash, create_token +from open_webui.utils.webhook import post_webhook +from open_webui.utils.groups import apply_default_group_assignment +from open_webui.retrieval.web.utils import validate_url + +from mcp.shared.auth import ( + OAuthClientMetadata as MCPOAuthClientMetadata, + OAuthMetadata, +) + +from authlib.oauth2.rfc6749.errors import OAuth2Error + + +class OAuthClientMetadata(MCPOAuthClientMetadata): + token_endpoint_auth_method: Literal['none', 'client_secret_basic', 'client_secret_post'] = 'client_secret_post' + pass + + +class OAuthClientInformationFull(OAuthClientMetadata): + issuer: Optional[str] = None # URL of the OAuth server that issued this client + + client_id: str + client_secret: str | None = None + client_id_issued_at: int | None = None + client_secret_expires_at: int | None = None + + server_metadata: Optional[OAuthMetadata] = None # Fetched from the OAuth server + + +from open_webui.env import GLOBAL_LOG_LEVEL + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) +log = logging.getLogger(__name__) + +auth_manager_config = AppConfig() +auth_manager_config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE +auth_manager_config.ENABLE_OAUTH_SIGNUP = ENABLE_OAUTH_SIGNUP +auth_manager_config.OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE = OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE +auth_manager_config.OAUTH_MERGE_ACCOUNTS_BY_EMAIL = OAUTH_MERGE_ACCOUNTS_BY_EMAIL +auth_manager_config.ENABLE_OAUTH_ROLE_MANAGEMENT = ENABLE_OAUTH_ROLE_MANAGEMENT +auth_manager_config.ENABLE_OAUTH_GROUP_MANAGEMENT = ENABLE_OAUTH_GROUP_MANAGEMENT +auth_manager_config.ENABLE_OAUTH_GROUP_CREATION = ENABLE_OAUTH_GROUP_CREATION +auth_manager_config.OAUTH_GROUP_DEFAULT_SHARE = OAUTH_GROUP_DEFAULT_SHARE +auth_manager_config.OAUTH_BLOCKED_GROUPS = OAUTH_BLOCKED_GROUPS +auth_manager_config.OAUTH_ROLES_CLAIM = OAUTH_ROLES_CLAIM +auth_manager_config.OAUTH_SUB_CLAIM = OAUTH_SUB_CLAIM +auth_manager_config.OAUTH_GROUPS_CLAIM = OAUTH_GROUPS_CLAIM +auth_manager_config.OAUTH_EMAIL_CLAIM = OAUTH_EMAIL_CLAIM +auth_manager_config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM +auth_manager_config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM +auth_manager_config.OAUTH_ALLOWED_ROLES = OAUTH_ALLOWED_ROLES +auth_manager_config.OAUTH_ADMIN_ROLES = OAUTH_ADMIN_ROLES +auth_manager_config.OAUTH_ALLOWED_DOMAINS = OAUTH_ALLOWED_DOMAINS +auth_manager_config.WEBHOOK_URL = WEBHOOK_URL +auth_manager_config.JWT_EXPIRES_IN = JWT_EXPIRES_IN +auth_manager_config.OAUTH_UPDATE_PICTURE_ON_LOGIN = OAUTH_UPDATE_PICTURE_ON_LOGIN +auth_manager_config.OAUTH_UPDATE_NAME_ON_LOGIN = OAUTH_UPDATE_NAME_ON_LOGIN +auth_manager_config.OAUTH_UPDATE_EMAIL_ON_LOGIN = OAUTH_UPDATE_EMAIL_ON_LOGIN +auth_manager_config.OAUTH_AUDIENCE = OAUTH_AUDIENCE + + +# Conservative default when the provider omits both expires_in and expires_at. +# Matches the value recommended by Authlib's compliance_fix documentation. +DEFAULT_TOKEN_EXPIRY_SECONDS = 3600 + + +def _normalize_token_expiry(token: dict) -> dict: + """Ensure a token dict always has a numeric ``expires_at``. + + Resolution order: + 1. If *expires_at* is already present and non-None, trust it. + 2. Else if *expires_in* is present and non-None, compute *expires_at*. + 3. Otherwise fall back to ``DEFAULT_TOKEN_EXPIRY_SECONDS`` and log a + warning so operators can identify providers that omit expiration. + + Also stamps *issued_at* for auditing. + """ + token['issued_at'] = datetime.now().timestamp() + + if token.get('expires_at') is not None: + token['expires_at'] = int(token['expires_at']) + return token + + if token.get('expires_in') is not None: + token['expires_at'] = int(datetime.now().timestamp() + token['expires_in']) + return token + + # Neither field present — conservative fallback + log.warning( + "OAuth token response missing both 'expires_in' and 'expires_at'; " + f'defaulting to {DEFAULT_TOKEN_EXPIRY_SECONDS}s from now' + ) + token['expires_at'] = int(datetime.now().timestamp() + DEFAULT_TOKEN_EXPIRY_SECONDS) + return token + + +FERNET = None + +if len(OAUTH_CLIENT_INFO_ENCRYPTION_KEY) != 44: + key_bytes = hashlib.sha256(OAUTH_CLIENT_INFO_ENCRYPTION_KEY.encode()).digest() + OAUTH_CLIENT_INFO_ENCRYPTION_KEY = base64.urlsafe_b64encode(key_bytes) +else: + OAUTH_CLIENT_INFO_ENCRYPTION_KEY = OAUTH_CLIENT_INFO_ENCRYPTION_KEY.encode() + +try: + FERNET = Fernet(OAUTH_CLIENT_INFO_ENCRYPTION_KEY) +except Exception as e: + log.error(f'Error initializing Fernet with provided key: {e}') + raise + + +def encrypt_data(data) -> str: + """Encrypt data for storage""" + try: + data_json = json.dumps(data) + encrypted = FERNET.encrypt(data_json.encode()).decode() + return encrypted + except Exception as e: + log.error(f'Error encrypting data: {e}') + raise + + +def decrypt_data(data: str): + """Decrypt data from storage""" + try: + decrypted = FERNET.decrypt(data.encode()).decode() + return json.loads(decrypted) + except Exception as e: + log.error(f'Error decrypting data: {e}') + raise + + +def _build_oauth_callback_error_message(e: Exception) -> str: + """ + Produce a user-facing callback error string with actionable context. + Keeps the message short and strips newlines for safe redirect usage. + """ + if isinstance(e, OAuth2Error): + parts = [p for p in [e.error, e.description] if p] + detail = ' - '.join(parts) + elif isinstance(e, HTTPException): + detail = e.detail if isinstance(e.detail, str) else str(e.detail) + elif isinstance(e, aiohttp.ClientResponseError): + detail = f'Upstream provider returned {e.status}: {e.message}' + elif isinstance(e, aiohttp.ClientError): + detail = str(e) + elif isinstance(e, KeyError): + missing = str(e).strip("'") + if missing.lower() == 'state': + detail = 'Missing state parameter in callback (session may have expired)' + else: + detail = f"Missing expected key '{missing}' in OAuth response" + else: + detail = str(e) + + detail = detail.replace('\n', ' ').strip() + if not detail: + detail = e.__class__.__name__ + + message = f'OAuth callback failed: {detail}' + return message[:197] + '...' if len(message) > 200 else message + + +def is_in_blocked_groups(group_name: str, groups: list) -> bool: + """ + Check if a group name matches any blocked pattern. + Supports exact matches, shell-style wildcards (*, ?), and regex patterns. + + Args: + group_name: The group name to check + groups: List of patterns to match against + + Returns: + True if the group is blocked, False otherwise + """ + if not groups: + return False + + for group_pattern in groups: + if not group_pattern: # Skip empty patterns + continue + + # Exact match + if group_name == group_pattern: + return True + + # Try as regex pattern first if it contains regex-specific characters + if any(char in group_pattern for char in ['^', '$', '[', ']', '(', ')', '{', '}', '+', '\\', '|']): + try: + # Use the original pattern as-is for regex matching + if re.search(group_pattern, group_name): + return True + except re.error: + # If regex is invalid, fall through to wildcard check + pass + + # Shell-style wildcard match (supports * and ?) + if '*' in group_pattern or '?' in group_pattern: + if fnmatch.fnmatch(group_name, group_pattern): + return True + + return False + + +def get_parsed_and_base_url(server_url) -> tuple[urllib.parse.ParseResult, str]: + parsed = urllib.parse.urlparse(server_url) + base_url = f'{parsed.scheme}://{parsed.netloc}' + return parsed, base_url + + +async def get_authorization_server_discovery_urls(server_url: str) -> list[str]: + """ + https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization + """ + + authorization_servers = [] + try: + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.post( + server_url, + json={'jsonrpc': '2.0', 'method': 'initialize', 'params': {}, 'id': 1}, + headers={'Content-Type': 'application/json'}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + if response.status == 401: + resource_metadata_urls = [] + match = re.search( + r'resource_metadata=(?:"([^"]+)"|([^\s,]+))', + response.headers.get('WWW-Authenticate', ''), + ) + if match: + resource_metadata_urls = [match.group(1) or match.group(2)] + log.debug(f'Found resource_metadata URL: {resource_metadata_urls[0]}') + else: + # Fall back to well-known resource metadata URIs (RFC 9728 §4.2) + parsed, base_url = get_parsed_and_base_url(server_url) + if parsed.path and parsed.path != '/': + path = parsed.path.rstrip('/') + resource_metadata_urls.append( + urllib.parse.urljoin(base_url, f'/.well-known/oauth-protected-resource{path}') + ) + resource_metadata_urls.append( + urllib.parse.urljoin(base_url, '/.well-known/oauth-protected-resource') + ) + log.debug(f'No resource_metadata in header, trying well-known URIs: {resource_metadata_urls}') + + # Fetch Protected Resource metadata from candidate URLs + for resource_metadata_url in resource_metadata_urls: + try: + async with session.get( + resource_metadata_url, ssl=AIOHTTP_CLIENT_SESSION_SSL + ) as resource_response: + if resource_response.status == 200: + resource_metadata = await resource_response.json() + + servers = resource_metadata.get('authorization_servers', []) + if servers: + authorization_servers = servers + log.debug(f'Discovered authorization servers: {servers}') + break + except Exception as e: + log.debug(f'Failed to fetch resource metadata from {resource_metadata_url}: {e}') + continue + except Exception as e: + log.debug(f'MCP Protected Resource discovery failed: {e}') + + discovery_urls = [] + for auth_server in authorization_servers: + auth_server = auth_server.rstrip('/') + discovery_urls.extend(_build_well_known_urls(auth_server)) + + return discovery_urls + + +def _build_well_known_urls(server_url: str) -> list[str]: + """Build RFC 8414 / OIDC Discovery well-known URLs for a server URL.""" + parsed, base_url = get_parsed_and_base_url(server_url) + urls = [] + + if parsed.path and parsed.path != '/': + path = parsed.path.rstrip('/') + urls.extend( + [ + urllib.parse.urljoin(base_url, f'/.well-known/oauth-authorization-server{path}'), + urllib.parse.urljoin(base_url, f'/.well-known/openid-configuration{path}'), + urllib.parse.urljoin(base_url, f'{path}/.well-known/openid-configuration'), + ] + ) + + urls.extend( + [ + urllib.parse.urljoin(base_url, '/.well-known/oauth-authorization-server'), + urllib.parse.urljoin(base_url, '/.well-known/openid-configuration'), + ] + ) + + return urls + + +async def get_discovery_urls(server_url) -> list[str]: + urls = await get_authorization_server_discovery_urls(server_url) + urls.extend(_build_well_known_urls(server_url)) + return urls + + +# TODO: Some OAuth providers require Initial Access Tokens (IATs) for dynamic client registration. +# This is not currently supported. +async def get_oauth_client_info_with_dynamic_client_registration( + request, + client_id: str, + oauth_server_url: str, + oauth_server_key: Optional[str] = None, +) -> OAuthClientInformationFull: + try: + oauth_server_metadata = None + oauth_server_metadata_url = None + + redirect_base_url = (str(request.app.state.config.WEBUI_URL or request.base_url)).rstrip('/') + + oauth_client_metadata = OAuthClientMetadata( + client_name='Open WebUI', + redirect_uris=[f'{redirect_base_url}/oauth/clients/{client_id}/callback'], + grant_types=['authorization_code', 'refresh_token'], + response_types=['code'], + ) + + # Attempt to fetch OAuth server metadata to get registration endpoint & scopes + discovery_urls = await get_discovery_urls(oauth_server_url) + for url in discovery_urls: + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get(url, ssl=AIOHTTP_CLIENT_SESSION_SSL) as oauth_server_metadata_response: + if oauth_server_metadata_response.status == 200: + try: + oauth_server_metadata = OAuthMetadata.model_validate( + await oauth_server_metadata_response.json() + ) + oauth_server_metadata_url = url + if ( + oauth_client_metadata.scope is None + and oauth_server_metadata.scopes_supported is not None + ): + oauth_client_metadata.scope = ' '.join(oauth_server_metadata.scopes_supported) + + if ( + oauth_server_metadata.token_endpoint_auth_methods_supported + and oauth_client_metadata.token_endpoint_auth_method + not in oauth_server_metadata.token_endpoint_auth_methods_supported + ): + # Pick the first supported method from the server + oauth_client_metadata.token_endpoint_auth_method = ( + oauth_server_metadata.token_endpoint_auth_methods_supported[0] + ) + + break + except Exception as e: + log.error(f'Error parsing OAuth metadata from {url}: {e}') + continue + + registration_url = None + if oauth_server_metadata and oauth_server_metadata.registration_endpoint: + registration_url = str(oauth_server_metadata.registration_endpoint) + else: + _, base_url = get_parsed_and_base_url(oauth_server_url) + registration_url = urllib.parse.urljoin(base_url, '/register') + + registration_data = oauth_client_metadata.model_dump( + exclude_none=True, + mode='json', + by_alias=True, + ) + + # Perform dynamic client registration and return client info + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.post( + registration_url, json=registration_data, ssl=AIOHTTP_CLIENT_SESSION_SSL + ) as oauth_client_registration_response: + try: + registration_response_json = await oauth_client_registration_response.json() + + # The mcp package requires optional unset values to be None. If an empty string is passed, it gets validated and fails. + # This replaces all empty strings with None. + registration_response_json = { + k: (None if v == '' else v) for k, v in registration_response_json.items() + } + oauth_client_info = OAuthClientInformationFull.model_validate( + { + **registration_response_json, + **{'issuer': oauth_server_metadata_url}, + **{'server_metadata': oauth_server_metadata}, + } + ) + log.info( + f'Dynamic client registration successful at {registration_url}, client_id: {oauth_client_info.client_id}' + ) + return oauth_client_info + except Exception as e: + error_text = None + try: + error_text = await oauth_client_registration_response.text() + log.error( + f'Dynamic client registration failed at {registration_url}: {oauth_client_registration_response.status} - {error_text}' + ) + except Exception as e: + pass + + log.error(f'Error parsing client registration response: {e}') + raise Exception( + f'Dynamic client registration failed: {error_text}' + if error_text + else 'Error parsing client registration response' + ) + raise Exception('Dynamic client registration failed') + except Exception as e: + log.error(f'Exception during dynamic client registration: {e}') + raise e + + +async def get_oauth_client_info_with_static_credentials( + request, + client_id: str, + oauth_server_url: str, + oauth_client_id: str, + oauth_client_secret: str, +) -> OAuthClientInformationFull: + """ + Build an OAuthClientInformationFull from user-provided static credentials. + Performs server metadata discovery to resolve authorization/token endpoints, + but skips dynamic client registration entirely. + """ + try: + oauth_server_metadata = None + oauth_server_metadata_url = None + + redirect_base_url = (str(request.app.state.config.WEBUI_URL or request.base_url)).rstrip('/') + redirect_uri = f'{redirect_base_url}/oauth/clients/{client_id}/callback' + + # Discover server metadata (authorization endpoint, token endpoint, scopes, etc.) + discovery_urls = await get_discovery_urls(oauth_server_url) + for url in discovery_urls: + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get(url, ssl=AIOHTTP_CLIENT_SESSION_SSL) as resp: + if resp.status == 200: + try: + oauth_server_metadata = OAuthMetadata.model_validate(await resp.json()) + oauth_server_metadata_url = url + break + except Exception as e: + log.error(f'Error parsing OAuth metadata from {url}: {e}') + continue + + # Let the OAuth provider apply its default scopes. + # We intentionally do NOT join all scopes_supported here — that list + # represents every scope the server *can* grant, not what the client + # should request. Requesting all of them is almost always wrong and + # can break providers like Entra ID that require resource-specific scopes. + scope = None + + # Determine token_endpoint_auth_method + token_endpoint_auth_method = 'client_secret_post' + if ( + oauth_server_metadata + and oauth_server_metadata.token_endpoint_auth_methods_supported + and token_endpoint_auth_method not in oauth_server_metadata.token_endpoint_auth_methods_supported + ): + token_endpoint_auth_method = oauth_server_metadata.token_endpoint_auth_methods_supported[0] + + oauth_client_info = OAuthClientInformationFull( + client_id=oauth_client_id, + client_secret=oauth_client_secret, + redirect_uris=[redirect_uri], + grant_types=['authorization_code', 'refresh_token'], + response_types=['code'], + scope=scope, + token_endpoint_auth_method=token_endpoint_auth_method, + issuer=oauth_server_metadata_url, + server_metadata=oauth_server_metadata, + ) + + log.info( + f'Static OAuth client info built for {oauth_client_id} using metadata from {oauth_server_metadata_url}' + ) + return oauth_client_info + except Exception as e: + log.error(f'Exception building static OAuth client info: {e}') + raise e + + +def resolve_oauth_client_info(connection: dict) -> dict: + """ + Decrypt OAuth client info from a tool server connection config. + + For oauth_2.1_static, overlays admin-provided credentials from + info.oauth_client_id and info.oauth_client_secret onto the blob. + """ + info = connection.get('info', {}) + data = decrypt_data(info.get('oauth_client_info', '')) + + if connection.get('auth_type') == 'oauth_2.1_static': + if info.get('oauth_client_id') and info.get('oauth_client_secret'): + data['client_id'] = info['oauth_client_id'] + data['client_secret'] = info['oauth_client_secret'] + + return data + + +class OAuthClientManager: + def __init__(self, app): + self.oauth = OAuth() + self.app = app + self.clients = {} + + def add_client(self, client_id, oauth_client_info: OAuthClientInformationFull): + kwargs = { + 'name': client_id, + 'client_id': oauth_client_info.client_id, + 'client_secret': oauth_client_info.client_secret, + 'client_kwargs': { + 'follow_redirects': True, + **({'scope': oauth_client_info.scope} if oauth_client_info.scope else {}), + **( + {'token_endpoint_auth_method': oauth_client_info.token_endpoint_auth_method} + if oauth_client_info.token_endpoint_auth_method + else {} + ), + }, + 'server_metadata_url': (oauth_client_info.issuer if oauth_client_info.issuer else None), + } + + # Default to S256 for OAuth 2.1 (PKCE is mandatory per RFC 9700) + kwargs['code_challenge_method'] = 'S256' + + # Only remove PKCE if metadata explicitly excludes S256 + if ( + oauth_client_info.server_metadata + and oauth_client_info.server_metadata.code_challenge_methods_supported + and isinstance( + oauth_client_info.server_metadata.code_challenge_methods_supported, + list, + ) + and 'S256' not in oauth_client_info.server_metadata.code_challenge_methods_supported + ): + del kwargs['code_challenge_method'] + + self.clients[client_id] = { + 'client': self.oauth.register(**kwargs), + 'client_info': oauth_client_info, + } + return self.clients[client_id] + + def ensure_client_from_config(self, client_id): + """ + Lazy-load an OAuth client from the current TOOL_SERVER_CONNECTIONS + config if it hasn't been registered on this node yet. + """ + if client_id in self.clients: + return self.clients[client_id]['client'] + + try: + connections = getattr(self.app.state.config, 'TOOL_SERVER_CONNECTIONS', []) + except Exception: + connections = [] + + for connection in connections or []: + if connection.get('type', 'openapi') != 'mcp': + continue + if connection.get('auth_type', 'none') not in ('oauth_2.1', 'oauth_2.1_static'): + continue + + server_id = connection.get('info', {}).get('id') + if not server_id: + continue + + expected_client_id = f'mcp:{server_id}' + if client_id != expected_client_id: + continue + + oauth_client_info = connection.get('info', {}).get('oauth_client_info', '') + if not oauth_client_info: + continue + + try: + oauth_client_info = resolve_oauth_client_info(connection) + return self.add_client(expected_client_id, OAuthClientInformationFull(**oauth_client_info))['client'] + except Exception as e: + log.error(f'Failed to lazily add OAuth client {expected_client_id} from config: {e}') + continue + + return None + + def remove_client(self, client_id): + if client_id in self.clients: + del self.clients[client_id] + log.info(f'Removed OAuth client {client_id}') + + if hasattr(self.oauth, '_clients'): + if client_id in self.oauth._clients: + self.oauth._clients.pop(client_id, None) + + if hasattr(self.oauth, '_registry'): + if client_id in self.oauth._registry: + self.oauth._registry.pop(client_id, None) + + return True + + async def _preflight_authorization_url(self, client, client_info: OAuthClientInformationFull) -> bool: + # TODO: Replace this logic with a more robust OAuth client registration validation + # Only perform preflight checks for Starlette OAuth clients + if not hasattr(client, 'create_authorization_url'): + return True + + redirect_uri = None + if client_info.redirect_uris: + redirect_uri = str(client_info.redirect_uris[0]) + + try: + auth_data = await client.create_authorization_url(redirect_uri=redirect_uri) + authorization_url = auth_data.get('url') + + if not authorization_url: + return True + except Exception as e: + log.debug( + f'Skipping OAuth preflight for client {client_info.client_id}: {e}', + ) + return True + + try: + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get( + authorization_url, + allow_redirects=False, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as resp: + if resp.status < 400: + return True + response_text = await resp.text() + + error = None + error_description = '' + + content_type = resp.headers.get('content-type', '') + if 'application/json' in content_type: + try: + payload = json.loads(response_text) + error = payload.get('error') + error_description = payload.get('error_description', '') + except Exception: + pass + else: + error_description = response_text + + error_message = f'{error or ""} {error_description or ""}'.lower() + + if any(keyword in error_message for keyword in ('invalid_client', 'invalid client', 'client id')): + log.warning( + f'OAuth client preflight detected invalid registration for {client_info.client_id}: {error} {error_description}' + ) + + return False + except Exception as e: + log.debug(f'Skipping OAuth preflight network check for client {client_info.client_id}: {e}') + + return True + + def get_client(self, client_id): + if client_id not in self.clients: + self.ensure_client_from_config(client_id) + + client = self.clients.get(client_id) + return client['client'] if client else None + + def get_client_info(self, client_id): + if client_id not in self.clients: + self.ensure_client_from_config(client_id) + + client = self.clients.get(client_id) + return client['client_info'] if client else None + + def get_server_metadata_url(self, client_id): + client = self.get_client(client_id) + if not client: + return None + + return client._server_metadata_url if hasattr(client, '_server_metadata_url') else None + + async def get_oauth_token(self, user_id: str, client_id: str, force_refresh: bool = False): + """ + Get a valid OAuth token for the user, automatically refreshing if needed. + + Args: + user_id: The user ID + client_id: The OAuth client ID (provider) + force_refresh: Force token refresh even if current token appears valid + + Returns: + dict: OAuth token data with access_token, or None if no valid token available + """ + try: + # Get the OAuth session + session = await OAuthSessions.get_session_by_provider_and_user_id(client_id, user_id) + if not session: + log.warning(f'No OAuth session found for user {user_id}, client_id {client_id}') + return None + + if ( + force_refresh + or session.expires_at is None + or datetime.now() + timedelta(minutes=5) >= datetime.fromtimestamp(session.expires_at) + ): + log.debug(f'Token refresh needed for user {user_id}, client_id {session.provider}') + refreshed_token = await self._refresh_token(session) + if refreshed_token: + return refreshed_token + else: + log.warning( + f'Token refresh failed for user {user_id}, client_id {session.provider}, deleting session {session.id}' + ) + await OAuthSessions.delete_session_by_id(session.id) + return None + return session.token + + except Exception as e: + log.error(f'Error getting OAuth token for user {user_id}: {e}') + return None + + async def _refresh_token(self, session) -> dict: + """ + Refresh an OAuth token if needed, with concurrency protection. + + Args: + session: The OAuth session object + + Returns: + dict: Refreshed token data, or None if refresh failed + """ + try: + # Perform the actual refresh + refreshed_token = await self._perform_token_refresh(session) + + if refreshed_token: + # Update the session with new token data + session = await OAuthSessions.update_session_by_id(session.id, refreshed_token) + log.info(f'Successfully refreshed token for session {session.id}') + return session.token + else: + log.error(f'Failed to refresh token for session {session.id}') + return None + + except Exception as e: + log.error(f'Error refreshing token for session {session.id}: {e}') + return None + + async def _perform_token_refresh(self, session) -> dict: + """ + Perform the actual OAuth token refresh. + + Args: + session: The OAuth session object + + Returns: + dict: New token data, or None if refresh failed + """ + client_id = session.provider + token_data = session.token + + if not token_data.get('refresh_token'): + log.warning(f'No refresh token available for session {session.id}') + return None + + try: + client = self.get_client(client_id) + if not client: + log.error(f'No OAuth client found for provider {client_id}') + return None + + token_endpoint = None + async with aiohttp.ClientSession(trust_env=True) as session_http: + async with session_http.get(self.get_server_metadata_url(client_id)) as r: + if r.status == 200: + openid_data = await r.json() + token_endpoint = openid_data.get('token_endpoint') + else: + log.error(f'Failed to fetch OpenID configuration for client_id {client_id}') + if not token_endpoint: + log.error(f'No token endpoint found for client_id {client_id}') + return None + + # Prepare refresh request + refresh_data = { + 'grant_type': 'refresh_token', + 'refresh_token': token_data['refresh_token'], + 'client_id': client.client_id, + } + if hasattr(client, 'client_secret') and client.client_secret: + refresh_data['client_secret'] = client.client_secret + + # Add scope if available in client kwargs (some providers require it on refresh) + if ( + hasattr(client, 'client_kwargs') + and client.client_kwargs.get('scope') + and getattr(self.app.state.config, 'OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE', False) + ): + refresh_data['scope'] = client.client_kwargs['scope'] + + # Make refresh request + async with aiohttp.ClientSession(trust_env=True) as session_http: + async with session_http.post( + token_endpoint, + data=refresh_data, + headers={'Content-Type': 'application/x-www-form-urlencoded'}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + if r.status == 200: + new_token_data = await r.json() + + # Merge with existing token data (preserve refresh_token if not provided) + if 'refresh_token' not in new_token_data: + new_token_data['refresh_token'] = token_data['refresh_token'] + + _normalize_token_expiry(new_token_data) + + log.debug(f'Token refresh successful for client_id {client_id}') + return new_token_data + else: + error_text = await r.text() + log.error(f'Token refresh failed for client_id {client_id}: {r.status} - {error_text}') + return None + + except Exception as e: + log.error(f'Exception during token refresh for client_id {client_id}: {e}') + return None + + async def handle_authorize(self, request, client_id: str) -> RedirectResponse: + client = self.get_client(client_id) or self.ensure_client_from_config(client_id) + if client is None: + raise HTTPException(404) + client_info = self.get_client_info(client_id) + if client_info is None: + # ensure_client_from_config registers client_info too + client_info = self.get_client_info(client_id) + if client_info is None: + raise HTTPException(404) + + redirect_uri = client_info.redirect_uris[0] if client_info.redirect_uris else None + redirect_uri_str = str(redirect_uri) if redirect_uri else None + return await client.authorize_redirect(request, redirect_uri_str) + + async def handle_callback(self, request, client_id: str, user_id: str, response): + client = self.get_client(client_id) or self.ensure_client_from_config(client_id) + if client is None: + raise HTTPException(404) + + error_message = None + try: + client_info = self.get_client_info(client_id) + + # Note: Do NOT pass client_id/client_secret explicitly here. + # The Authlib client already has these configured during add_client(). + # Passing them again causes Authlib to concatenate them (e.g., "ID1,ID1"), + # which results in 401 errors from the token endpoint. (Fix for #19823) + token = await client.authorize_access_token(request) + + # Validate that we received a proper token response + # If token exchange failed (e.g., 401), we may get an error response instead + if token and not token.get('access_token'): + error_desc = token.get('error_description', token.get('error', 'Unknown error')) + error_message = f'Token exchange failed: {error_desc}' + log.error(f'Invalid token response for client_id {client_id}: {token}') + token = None + + if token: + try: + _normalize_token_expiry(token) + + # Clean up any existing sessions for this user/client_id first + sessions = await OAuthSessions.get_sessions_by_user_id(user_id) + for session in sessions: + if session.provider == client_id: + await OAuthSessions.delete_session_by_id(session.id) + + session = await OAuthSessions.create_session( + user_id=user_id, + provider=client_id, + token=token, + ) + log.info(f'Stored OAuth session server-side for user {user_id}, client_id {client_id}') + except Exception as e: + error_message = 'Failed to store OAuth session server-side' + log.error(f'Failed to store OAuth session server-side: {e}') + else: + if not error_message: + error_message = 'Failed to obtain OAuth token' + log.warning(error_message) + except Exception as e: + error_message = _build_oauth_callback_error_message(e) + log.warning( + 'OAuth callback error for user_id=%s client_id=%s: %s', + user_id, + client_id, + error_message, + exc_info=True, + ) + + redirect_url = (str(request.app.state.config.WEBUI_URL or request.base_url)).rstrip('/') + + if error_message: + log.debug(error_message) + redirect_url = f'{redirect_url}/?error={urllib.parse.quote_plus(error_message)}' + return RedirectResponse(url=redirect_url, headers=response.headers) + + response = RedirectResponse(url=redirect_url, headers=response.headers) + return response + + +class OAuthManager: + def __init__(self, app): + self.oauth = OAuth() + self.app = app + + self._clients = {} + + for name, provider_config in OAUTH_PROVIDERS.items(): + if 'register' not in provider_config: + log.error(f'OAuth provider {name} missing register function') + continue + + client = provider_config['register'](self.oauth) + self._clients[name] = client + + def get_client(self, provider_name): + if provider_name not in self._clients: + self._clients[provider_name] = self.oauth.create_client(provider_name) + return self._clients[provider_name] + + def get_server_metadata_url(self, provider_name): + if provider_name in self._clients: + client = self._clients[provider_name] + return client._server_metadata_url if hasattr(client, '_server_metadata_url') else None + return None + + async def get_oauth_token(self, user_id: str, session_id: str, force_refresh: bool = False): + """ + Get a valid OAuth token for the user, automatically refreshing if needed. + + Args: + user_id: The user ID + provider: Optional provider name. If None, gets the most recent session. + force_refresh: Force token refresh even if current token appears valid + + Returns: + dict: OAuth token data with access_token, or None if no valid token available + """ + try: + # Get the OAuth session + session = await OAuthSessions.get_session_by_id_and_user_id(session_id, user_id) + if not session: + log.warning(f'No OAuth session found for user {user_id}, session {session_id}') + return None + + if ( + force_refresh + or session.expires_at is None + or datetime.now() + timedelta(minutes=5) >= datetime.fromtimestamp(session.expires_at) + ): + log.debug(f'Token refresh needed for user {user_id}, provider {session.provider}') + refreshed_token = await self._refresh_token(session) + if refreshed_token: + return refreshed_token + else: + log.warning( + f'Token refresh failed for user {user_id}, provider {session.provider}, deleting session {session.id}' + ) + await OAuthSessions.delete_session_by_id(session.id) + + return None + return session.token + + except Exception as e: + log.error(f'Error getting OAuth token for user {user_id}: {e}') + return None + + async def _refresh_token(self, session) -> dict: + """ + Refresh an OAuth token if needed, with concurrency protection. + + Args: + session: The OAuth session object + + Returns: + dict: Refreshed token data, or None if refresh failed + """ + try: + # Perform the actual refresh + refreshed_token = await self._perform_token_refresh(session) + + if refreshed_token: + # Update the session with new token data + session = await OAuthSessions.update_session_by_id(session.id, refreshed_token) + log.info(f'Successfully refreshed token for session {session.id}') + return session.token + else: + log.error(f'Failed to refresh token for session {session.id}') + return None + + except Exception as e: + log.error(f'Error refreshing token for session {session.id}: {e}') + return None + + async def _perform_token_refresh(self, session) -> dict: + """ + Perform the actual OAuth token refresh. + + Args: + session: The OAuth session object + + Returns: + dict: New token data, or None if refresh failed + """ + provider = session.provider + token_data = session.token + + if not token_data.get('refresh_token'): + log.warning(f'No refresh token available for session {session.id}') + return None + + try: + client = self.get_client(provider) + if not client: + log.error(f'No OAuth client found for provider {provider}') + return None + + server_metadata_url = self.get_server_metadata_url(provider) + token_endpoint = None + async with aiohttp.ClientSession(trust_env=True) as session_http: + async with session_http.get(server_metadata_url) as r: + if r.status == 200: + openid_data = await r.json() + token_endpoint = openid_data.get('token_endpoint') + else: + log.error(f'Failed to fetch OpenID configuration for provider {provider}') + if not token_endpoint: + log.error(f'No token endpoint found for provider {provider}') + return None + + # Prepare refresh request + refresh_data = { + 'grant_type': 'refresh_token', + 'refresh_token': token_data['refresh_token'], + 'client_id': client.client_id, + } + # Add client_secret if available (some providers require it) + if hasattr(client, 'client_secret') and client.client_secret: + refresh_data['client_secret'] = client.client_secret + + # Add scope if available in client kwargs (some providers require it on refresh) + if ( + hasattr(client, 'client_kwargs') + and client.client_kwargs.get('scope') + and auth_manager_config.OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE + ): + refresh_data['scope'] = client.client_kwargs['scope'] + + # Make refresh request + async with aiohttp.ClientSession(trust_env=True) as session_http: + async with session_http.post( + token_endpoint, + data=refresh_data, + headers={'Content-Type': 'application/x-www-form-urlencoded'}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + if r.status == 200: + new_token_data = await r.json() + + # Merge with existing token data (preserve refresh_token if not provided) + if 'refresh_token' not in new_token_data: + new_token_data['refresh_token'] = token_data['refresh_token'] + + _normalize_token_expiry(new_token_data) + + log.debug(f'Token refresh successful for provider {provider}') + return new_token_data + else: + error_text = await r.text() + log.error(f'Token refresh failed for provider {provider}: {r.status} - {error_text}') + return None + + except Exception as e: + log.error(f'Exception during token refresh for provider {provider}: {e}') + return None + + async def get_user_role(self, user, user_data): + user_count = await Users.get_num_users() + if user and user_count == 1: + # If the user is the only user, assign the role "admin" - actually repairs role for single user on login + log.debug('Assigning the only user the admin role') + return 'admin' + if not user and user_count == 0: + # First-user bootstrap: skip role management gating so the + # instance can be initialized. We intentionally return the + # default role here (not 'admin') — admin promotion happens + # race-safely *after* insert via get_num_users() == 1. + log.debug('First user bootstrap: using default role (admin promotion deferred to post-insert)') + return auth_manager_config.DEFAULT_USER_ROLE + + if auth_manager_config.ENABLE_OAUTH_ROLE_MANAGEMENT: + log.debug('Running OAUTH Role management') + oauth_claim = auth_manager_config.OAUTH_ROLES_CLAIM + oauth_allowed_roles = auth_manager_config.OAUTH_ALLOWED_ROLES + oauth_admin_roles = auth_manager_config.OAUTH_ADMIN_ROLES + oauth_roles = [] + # Default/fallback role if no matching roles are found + role = auth_manager_config.DEFAULT_USER_ROLE + + # Next block extracts the roles from the user data, accepting nested claims of any depth + if oauth_claim and oauth_allowed_roles and oauth_admin_roles: + claim_data = user_data + nested_claims = oauth_claim.split('.') + for nested_claim in nested_claims: + claim_data = claim_data.get(nested_claim, {}) + + # Try flat claim structure as alternative + if not claim_data: + claim_data = user_data.get(oauth_claim, {}) + + oauth_roles = [] + + if isinstance(claim_data, list): + oauth_roles = claim_data + elif isinstance(claim_data, str): + # Split by the configured separator if present + if OAUTH_ROLES_SEPARATOR and OAUTH_ROLES_SEPARATOR in claim_data: + oauth_roles = claim_data.split(OAUTH_ROLES_SEPARATOR) + else: + oauth_roles = [claim_data] + elif isinstance(claim_data, int): + oauth_roles = [str(claim_data)] + + log.debug(f'Oauth Roles claim: {oauth_claim}') + log.debug(f'User roles from oauth: {oauth_roles}') + log.debug(f'Accepted user roles: {oauth_allowed_roles}') + log.debug(f'Accepted admin roles: {oauth_admin_roles}') + + # If roles are present in the token, they must match; otherwise deny access + if oauth_roles: + matched = False + for allowed_role in oauth_allowed_roles: + if allowed_role in oauth_roles: + log.debug('Assigned user the user role') + role = 'user' + matched = True + break + for admin_role in oauth_admin_roles: + if admin_role in oauth_roles: + log.debug('Assigned user the admin role') + role = 'admin' + matched = True + break + if not matched: + log.warning( + f'OAuth role management enabled but user roles do not match any allowed/admin roles. ' + f'User roles: {oauth_roles}, allowed: {oauth_allowed_roles}, admin: {oauth_admin_roles}' + ) + raise HTTPException( + status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + else: + if not user: + # If role management is disabled, use the default role for new users + role = auth_manager_config.DEFAULT_USER_ROLE + else: + # If role management is disabled, use the existing role for existing users + role = user.role + + return role + + async def update_user_groups(self, user, user_data, default_permissions, db=None): + log.debug('Running OAUTH Group management') + oauth_claim = auth_manager_config.OAUTH_GROUPS_CLAIM + + try: + blocked_groups = json.loads(auth_manager_config.OAUTH_BLOCKED_GROUPS) + except Exception as e: + log.exception(f'Error loading OAUTH_BLOCKED_GROUPS: {e}') + blocked_groups = [] + + user_oauth_groups = [] + # Nested claim search for groups claim + if oauth_claim: + claim_data = user_data + nested_claims = oauth_claim.split('.') + for nested_claim in nested_claims: + claim_data = claim_data.get(nested_claim, {}) + + if isinstance(claim_data, list): + user_oauth_groups = claim_data + elif isinstance(claim_data, str): + # Split by the configured separator if present + if OAUTH_GROUPS_SEPARATOR in claim_data: + user_oauth_groups = claim_data.split(OAUTH_GROUPS_SEPARATOR) + else: + user_oauth_groups = [claim_data] + else: + user_oauth_groups = [] + + user_current_groups: list[GroupModel] = await Groups.get_groups_by_member_id(user.id, db=db) + all_available_groups: list[GroupModel] = await Groups.get_all_groups(db=db) + + # Create groups if they don't exist and creation is enabled + if auth_manager_config.ENABLE_OAUTH_GROUP_CREATION: + log.debug('Checking for missing groups to create...') + all_group_names = {g.name for g in all_available_groups} + groups_created = False + # Determine creator ID: Prefer admin, fallback to current user if no admin exists + admin_user = await Users.get_super_admin_user() + creator_id = admin_user.id if admin_user else user.id + log.debug(f'Using creator ID {creator_id} for potential group creation.') + + for group_name in user_oauth_groups: + if group_name not in all_group_names: + log.info(f"Group '{group_name}' not found via OAuth claim. Creating group...") + try: + new_group_form = GroupForm( + name=group_name, + description=f"Group '{group_name}' created automatically via OAuth.", + permissions=default_permissions, # Use default permissions from function args + data={'config': {'share': auth_manager_config.OAUTH_GROUP_DEFAULT_SHARE}}, + ) + # Use determined creator ID (admin or fallback to current user) + created_group = await Groups.insert_new_group(creator_id, new_group_form, db=db) + if created_group: + log.info( + f"Successfully created group '{group_name}' with ID {created_group.id} using creator ID {creator_id}" + ) + groups_created = True + # Add to local set to prevent duplicate creation attempts in this run + all_group_names.add(group_name) + else: + log.error(f"Failed to create group '{group_name}' via OAuth.") + except Exception as e: + log.error(f"Error creating group '{group_name}' via OAuth: {e}") + + # Refresh the list of all available groups if any were created + if groups_created: + all_available_groups = await Groups.get_all_groups(db=db) + log.debug('Refreshed list of all available groups after creation.') + + log.debug(f'Oauth Groups claim: {oauth_claim}') + log.debug(f'User oauth groups: {user_oauth_groups}') + log.debug(f"User's current groups: {[g.name for g in user_current_groups]}") + log.debug(f'All groups available in OpenWebUI: {[g.name for g in all_available_groups]}') + + # Remove groups that user is no longer a part of + for group_model in user_current_groups: + if ( + user_oauth_groups + and group_model.name not in user_oauth_groups + and not is_in_blocked_groups(group_model.name, blocked_groups) + ): + # Remove group from user + log.debug(f'Removing user from group {group_model.name} as it is no longer in their oauth groups') + await Groups.remove_users_from_group(group_model.id, [user.id], db=db) + + # In case a group is created, but perms are never assigned to the group by hitting "save" + group_permissions = group_model.permissions + if not group_permissions: + group_permissions = default_permissions + + await Groups.update_group_by_id( + id=group_model.id, + form_data=GroupUpdateForm( + name=group_model.name, + description=group_model.description, + permissions=group_permissions, + ), + overwrite=False, + db=db, + ) + + # Add user to new groups + for group_model in all_available_groups: + if ( + user_oauth_groups + and group_model.name in user_oauth_groups + and not any(gm.name == group_model.name for gm in user_current_groups) + and not is_in_blocked_groups(group_model.name, blocked_groups) + ): + # Add user to group + log.debug(f'Adding user to group {group_model.name} as it was found in their oauth groups') + + await Groups.add_users_to_group(group_model.id, [user.id], db=db) + + # In case a group is created, but perms are never assigned to the group by hitting "save" + group_permissions = group_model.permissions + if not group_permissions: + group_permissions = default_permissions + + await Groups.update_group_by_id( + id=group_model.id, + form_data=GroupUpdateForm( + name=group_model.name, + description=group_model.description, + permissions=group_permissions, + ), + overwrite=False, + db=db, + ) + + async def _process_picture_url(self, picture_url: str, access_token: str = None) -> str: + """Process a picture URL and return a base64 encoded data URL. + + Args: + picture_url: The URL of the picture to process + access_token: Optional OAuth access token for authenticated requests + + Returns: + A data URL containing the base64 encoded picture, or "/user.png" if processing fails + """ + if not picture_url: + return '/user.png' + + try: + validate_url(picture_url) + + get_kwargs = {} + if access_token: + get_kwargs['headers'] = { + 'Authorization': f'Bearer {access_token}', + } + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get(picture_url, **get_kwargs, ssl=AIOHTTP_CLIENT_SESSION_SSL) as resp: + if resp.ok: + picture = await resp.read() + base64_encoded_picture = base64.b64encode(picture).decode('utf-8') + guessed_mime_type = mimetypes.guess_type(picture_url)[0] + if guessed_mime_type is None: + guessed_mime_type = 'image/jpeg' + return f'data:{guessed_mime_type};base64,{base64_encoded_picture}' + else: + log.warning(f'Failed to fetch profile picture from {picture_url}') + return '/user.png' + except Exception as e: + log.error(f"Error processing profile picture '{picture_url}': {e}") + return '/user.png' + + async def handle_login(self, request, provider): + if provider not in OAUTH_PROVIDERS: + raise HTTPException(404) + # If the provider has a custom redirect URL, use that, otherwise automatically generate one + client = self.get_client(provider) + if client is None: + raise HTTPException(404) + redirect_uri = (client.server_metadata or {}).get('redirect_uri') or request.url_for( + 'oauth_login_callback', provider=provider + ) + + kwargs = {} + if auth_manager_config.OAUTH_AUDIENCE: + kwargs['audience'] = auth_manager_config.OAUTH_AUDIENCE + if OAUTH_AUTHORIZE_PARAMS: + kwargs.update(OAUTH_AUTHORIZE_PARAMS) + + return await client.authorize_redirect(request, redirect_uri, **kwargs) + + async def handle_callback(self, request, provider, response, db=None): + if provider not in OAUTH_PROVIDERS: + raise HTTPException(404) + + error_message = None + try: + client = self.get_client(provider) + + auth_params = {} + + if client: + if hasattr(client, 'client_id') and OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID: + auth_params['client_id'] = client.client_id + + try: + token = await client.authorize_access_token(request, **auth_params) + except BadSignatureError: + # The IdP likely rotated its signing keys and the cached JWKS + # is stale. Evict the cached key set so the next attempt + # fetches fresh keys from the jwks_uri. + log.warning( + 'OIDC bad_signature for provider %s — evicting cached JWKS and retrying', + provider, + ) + if hasattr(client, 'server_metadata') and isinstance(client.server_metadata, dict): + client.server_metadata.pop('jwks', None) + try: + token = await client.authorize_access_token(request, **auth_params) + except Exception as retry_exc: + detailed_error = _build_oauth_callback_error_message(retry_exc) + log.warning( + 'OAuth callback error during authorize_access_token retry for provider %s: %s', + provider, + detailed_error, + exc_info=True, + ) + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + except Exception as e: + detailed_error = _build_oauth_callback_error_message(e) + log.warning( + 'OAuth callback error during authorize_access_token for provider %s: %s', + provider, + detailed_error, + exc_info=True, + ) + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + + # Try to get userinfo from the token first, some providers include it there + user_data: UserInfo = token.get('userinfo') + # Preserve extra claims from the ID token (e.g. roles, groups for + # Microsoft Entra ID) before the userinfo endpoint possibly overwrites them. + id_token_claims = dict(user_data) if user_data else {} + if ( + (not user_data) + or (auth_manager_config.OAUTH_EMAIL_CLAIM not in user_data) + or (auth_manager_config.OAUTH_USERNAME_CLAIM not in user_data) + ): + user_data: UserInfo = await client.userinfo(token=token) + # Merge back ID token claims that the userinfo endpoint doesn't + # return. Only backfill missing keys so userinfo always wins. + if user_data and id_token_claims: + for key, value in id_token_claims.items(): + if key not in user_data: + user_data[key] = value + if provider == 'feishu' and isinstance(user_data, dict) and 'data' in user_data: + user_data = user_data['data'] + if not user_data: + log.warning(f'OAuth callback failed, user data is missing: {token}') + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + + # Extract the "sub" claim, using custom claim if configured + if auth_manager_config.OAUTH_SUB_CLAIM: + sub = user_data.get(auth_manager_config.OAUTH_SUB_CLAIM) + else: + # Fallback to the default sub claim if not configured + sub = user_data.get(OAUTH_PROVIDERS[provider].get('sub_claim', 'sub')) + if not sub: + log.warning(f'OAuth callback failed, sub is missing: {user_data}') + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + + oauth_data = {} + oauth_data[provider] = { + 'sub': sub, + } + + # Email extraction + email_claim = auth_manager_config.OAUTH_EMAIL_CLAIM + email = user_data.get(email_claim, '') + # We currently mandate that email addresses are provided + if not email: + # If the provider is GitHub,and public email is not provided, we can use the access token to fetch the user's email + if provider == 'github': + try: + access_token = token.get('access_token') + headers = {'Authorization': f'Bearer {access_token}'} + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get( + 'https://api.github.com/user/emails', + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as resp: + if resp.ok: + emails = await resp.json() + # use the primary email as the user's email + primary_email = next( + (e['email'] for e in emails if e.get('primary')), + None, + ) + if primary_email: + email = primary_email + else: + log.warning('No primary email found in GitHub response') + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + else: + log.warning('Failed to fetch GitHub email') + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + except Exception as e: + log.warning(f'Error fetching GitHub email: {e}') + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + elif ENABLE_OAUTH_EMAIL_FALLBACK: + email = f'{provider}@{sub}.local' + else: + log.warning(f'OAuth callback failed, email is missing: {user_data}') + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + + email = email.lower() + # If allowed domains are configured, check if the email domain is in the list + if ( + '*' not in auth_manager_config.OAUTH_ALLOWED_DOMAINS + and email.split('@')[-1] not in auth_manager_config.OAUTH_ALLOWED_DOMAINS + ): + log.warning(f'OAuth callback failed, e-mail domain is not in the list of allowed domains: {user_data}') + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + + # Check if the user exists + user = await Users.get_user_by_oauth_sub(provider, sub, db=db) + if not user: + # If the user does not exist, check if merging is enabled + if auth_manager_config.OAUTH_MERGE_ACCOUNTS_BY_EMAIL: + # Check if the user exists by email + user = await Users.get_user_by_email(email, db=db) + if user: + # Update the user with the new oauth sub + await Users.update_user_oauth_by_id(user.id, provider, sub, db=db) + + if user: + determined_role = await self.get_user_role(user, user_data) + if user.role != determined_role: + await Users.update_user_role_by_id(user.id, determined_role, db=db) + # Update the user object in memory as well, + # to avoid problems with the ENABLE_OAUTH_GROUP_MANAGEMENT check below + user.role = determined_role + + if auth_manager_config.OAUTH_UPDATE_NAME_ON_LOGIN: + username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM + if username_claim: + new_name = user_data.get(username_claim) + if new_name and new_name != user.name: + await Users.update_user_by_id(user.id, {'name': new_name}, db=db) + user.name = new_name + log.debug(f'Updated name for user {user.email}') + + if auth_manager_config.OAUTH_UPDATE_EMAIL_ON_LOGIN: + email_claim = auth_manager_config.OAUTH_EMAIL_CLAIM + if email_claim: + new_email = user_data.get(email_claim) + if new_email and new_email.lower() != user.email.lower(): + existing_user = await Users.get_user_by_email(new_email, db=db) + if existing_user: + log.error( + f'Cannot update email to {new_email} for user {user.id} because it is already taken.' + ) + else: + await Auths.update_email_by_id(user.id, new_email.lower(), db=db) + user.email = new_email.lower() + log.debug(f'Updated email for user {user.id}') + + # Update profile picture if enabled and different from current + if auth_manager_config.OAUTH_UPDATE_PICTURE_ON_LOGIN: + picture_claim = auth_manager_config.OAUTH_PICTURE_CLAIM + if picture_claim: + new_picture_url = user_data.get( + picture_claim, + OAUTH_PROVIDERS[provider].get('picture_url', ''), + ) + processed_picture_url = await self._process_picture_url( + new_picture_url, token.get('access_token') + ) + if processed_picture_url != user.profile_image_url: + await Users.update_user_profile_image_url_by_id(user.id, processed_picture_url, db=db) + log.debug(f'Updated profile picture for user {user.email}') + else: + # If the user does not exist, check if signups are enabled + if auth_manager_config.ENABLE_OAUTH_SIGNUP: + # Check if an existing user with the same email already exists + existing_user = await Users.get_user_by_email(email, db=db) + if existing_user: + raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) + + picture_claim = auth_manager_config.OAUTH_PICTURE_CLAIM + if picture_claim: + picture_url = user_data.get( + picture_claim, + OAUTH_PROVIDERS[provider].get('picture_url', ''), + ) + picture_url = await self._process_picture_url(picture_url, token.get('access_token')) + else: + picture_url = '/user.png' + username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM + + name = user_data.get(username_claim) + if not name: + log.warning('Username claim is missing, using email as name') + name = email + + user = await Auths.insert_new_auth( + email=email, + password=get_password_hash(str(uuid.uuid4())), # Random password, not used + name=name, + profile_image_url=picture_url, + role=await self.get_user_role(None, user_data), + oauth=oauth_data, + db=db, + ) + + if not user: + raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) + + # Atomically check if this is the only user *after* the + # insert to avoid TOCTOU race on first-user registration. + # Matches signup_handler pattern. + if await Users.get_num_users(db=db) == 1: + await Users.update_user_role_by_id(user.id, 'admin', db=db) + user = await Users.get_user_by_id(user.id, db=db) + + if auth_manager_config.WEBHOOK_URL: + await post_webhook( + WEBUI_NAME, + auth_manager_config.WEBHOOK_URL, + WEBHOOK_MESSAGES.USER_SIGNUP(user.name), + { + 'action': 'signup', + 'message': WEBHOOK_MESSAGES.USER_SIGNUP(user.name), + 'user': user.model_dump_json(exclude_none=True), + }, + ) + + await apply_default_group_assignment(request.app.state.config.DEFAULT_GROUP_ID, user.id, db=db) + + else: + raise HTTPException( + status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + jwt_token = create_token( + data={'id': user.id}, + expires_delta=parse_duration(auth_manager_config.JWT_EXPIRES_IN), + ) + if auth_manager_config.ENABLE_OAUTH_GROUP_MANAGEMENT: + await self.update_user_groups( + user=user, + user_data=user_data, + default_permissions=request.app.state.config.USER_PERMISSIONS, + db=db, + ) + + except Exception as e: + log.error(f'Error during OAuth process: {e}') + error_message = ( + e.detail + if isinstance(e, HTTPException) and e.detail + else ERROR_MESSAGES.DEFAULT('Error during OAuth process') + ) + + redirect_base_url = (str(request.app.state.config.WEBUI_URL or request.base_url)).rstrip('/') + redirect_url = f'{redirect_base_url}/auth' + + if error_message: + redirect_url = f'{redirect_url}?error={urllib.parse.quote_plus(error_message)}' + return RedirectResponse(url=redirect_url, headers=response.headers) + + response = RedirectResponse(url=redirect_url, headers=response.headers) + + # Compute cookie expiry from JWT lifetime + expires_delta = parse_duration(auth_manager_config.JWT_EXPIRES_IN) + cookie_max_age = int(expires_delta.total_seconds()) if expires_delta else None + + # Set the cookie token + # Redirect back to the frontend with the JWT token + response.set_cookie( + key='token', + value=jwt_token, + httponly=False, # Required for frontend access + samesite=WEBUI_AUTH_COOKIE_SAME_SITE, + secure=WEBUI_AUTH_COOKIE_SECURE, + **({'max_age': cookie_max_age} if cookie_max_age is not None else {}), + ) + + # Legacy cookies for compatibility with older frontend versions + if ENABLE_OAUTH_ID_TOKEN_COOKIE: + response.set_cookie( + key='oauth_id_token', + value=token.get('id_token'), + httponly=True, + samesite=WEBUI_AUTH_COOKIE_SAME_SITE, + secure=WEBUI_AUTH_COOKIE_SECURE, + **({'max_age': cookie_max_age} if cookie_max_age is not None else {}), + ) + + try: + _normalize_token_expiry(token) + + # Enforce max concurrent sessions per user/provider to prevent + # unbounded growth while allowing multi-device usage + sessions = await OAuthSessions.get_sessions_by_user_id(user.id, db=db) + provider_sessions = sorted( + [session for session in sessions if session.provider == provider], + key=lambda session: session.created_at, + reverse=True, + ) + # Keep the newest sessions up to the limit, prune the rest + if len(provider_sessions) >= OAUTH_MAX_SESSIONS_PER_USER: + for old_session in provider_sessions[OAUTH_MAX_SESSIONS_PER_USER - 1 :]: + await OAuthSessions.delete_session_by_id(old_session.id, db=db) + + session = await OAuthSessions.create_session( + user_id=user.id, + provider=provider, + token=token, + db=db, + ) + + if session: + response.set_cookie( + key='oauth_session_id', + value=session.id, + httponly=True, + samesite=WEBUI_AUTH_COOKIE_SAME_SITE, + secure=WEBUI_AUTH_COOKIE_SECURE, + **({'max_age': cookie_max_age} if cookie_max_age is not None else {}), + ) + + log.info(f'Stored OAuth session server-side for user {user.id}, provider {provider}') + else: + log.warning(f'Failed to create OAuth session for user {user.id}, provider {provider}') + except Exception as e: + log.error(f'Failed to store OAuth session server-side: {e}') + + return response + + async def handle_backchannel_logout(self, request, db=None): + """ + Handle an OIDC Back-Channel Logout request. + Validates the logout_token, identifies the user, revokes their + sessions via Redis, and deletes their OAuth sessions. + Returns a JSONResponse per the OIDC Back-Channel Logout 1.0 spec. + """ + import jwt as pyjwt + from fastapi.responses import JSONResponse + + # 1. Extract logout_token from form body + try: + form = await request.form() + logout_token = form.get('logout_token') + except Exception: + logout_token = None + + if not logout_token: + return JSONResponse( + status_code=400, + content={'error': 'invalid_request', 'error_description': 'Missing logout_token parameter'}, + ) + + # 2. Peek at unverified issuer to match against configured providers + try: + unverified_claims = pyjwt.decode(logout_token, options={'verify_signature': False}) + token_issuer = unverified_claims.get('iss') + except Exception as e: + log.warning(f'Back-channel logout: cannot decode logout_token: {e}') + return JSONResponse( + status_code=400, + content={'error': 'invalid_request', 'error_description': 'Malformed logout_token'}, + ) + + if not token_issuer: + return JSONResponse( + status_code=400, + content={'error': 'invalid_request', 'error_description': 'logout_token missing iss claim'}, + ) + + # 3. Find the configured provider whose issuer matches the token + matched_provider = None + matched_client_id = None + matched_jwks_uri = None + matched_issuer = None + + for provider_name in OAUTH_PROVIDERS: + server_metadata_url = self.get_server_metadata_url(provider_name) + if not server_metadata_url: + continue + + try: + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get(server_metadata_url, ssl=AIOHTTP_CLIENT_SESSION_SSL) as r: + if r.status != 200: + continue + oidc_config = await r.json() + + provider_issuer = oidc_config.get('issuer') + if provider_issuer and provider_issuer == token_issuer: + client = self.get_client(provider_name) + matched_provider = provider_name + matched_client_id = client.client_id if client else None + matched_jwks_uri = oidc_config.get('jwks_uri') + matched_issuer = provider_issuer + break + except Exception as e: + log.debug(f'Back-channel logout: error checking provider {provider_name}: {e}') + continue + + if not matched_provider or not matched_client_id or not matched_jwks_uri: + log.warning(f'Back-channel logout: no configured provider matches issuer {token_issuer}') + return JSONResponse( + status_code=400, + content={ + 'error': 'invalid_request', + 'error_description': 'No configured provider matches token issuer', + }, + ) + + # 4. Validate the logout_token signature and claims + try: + jwks_client = pyjwt.PyJWKClient(matched_jwks_uri) + signing_key = jwks_client.get_signing_key_from_jwt(logout_token) + + claims = pyjwt.decode( + logout_token, + signing_key.key, + algorithms=['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512'], + audience=matched_client_id, + issuer=matched_issuer, + options={ + 'require': ['iss', 'aud', 'iat', 'events'], + }, + ) + except pyjwt.InvalidTokenError as e: + log.warning(f'Back-channel logout: invalid logout_token: {e}') + return JSONResponse( + status_code=400, + content={'error': 'invalid_request', 'error_description': f'Invalid logout_token: {e}'}, + ) + except Exception as e: + log.error(f'Back-channel logout: error validating logout_token: {e}') + return JSONResponse( + status_code=400, + content={'error': 'invalid_request', 'error_description': 'Failed to validate logout_token'}, + ) + + # 5. Validate events claim per spec + events = claims.get('events', {}) + if 'http://schemas.openid.net/event/backchannel-logout' not in events: + log.warning('Back-channel logout: missing required backchannel-logout event claim') + return JSONResponse( + status_code=400, + content={'error': 'invalid_request', 'error_description': 'Missing backchannel-logout event claim'}, + ) + + # 6. Per spec, back-channel logout tokens MUST NOT contain a nonce + if 'nonce' in claims: + log.warning('Back-channel logout: logout_token contains nonce (rejected per spec)') + return JSONResponse( + status_code=400, + content={'error': 'invalid_request', 'error_description': 'logout_token must not contain nonce'}, + ) + + # 7. Extract sub and/or sid — at least one must be present + sub = claims.get('sub') + sid = claims.get('sid') + + if not sub and not sid: + log.warning('Back-channel logout: logout_token contains neither sub nor sid') + return JSONResponse( + status_code=400, + content={'error': 'invalid_request', 'error_description': 'logout_token must contain sub or sid'}, + ) + + # 8. Identify users to log out + users_to_logout = [] + if sub: + user = await Users.get_user_by_oauth_sub(matched_provider, sub, db=db) + if user: + users_to_logout.append(user) + + if not users_to_logout and sid: + log.debug(f'Back-channel logout: no user found by sub, sid-based lookup not yet supported (sid={sid})') + + if not users_to_logout: + log.debug(f'Back-channel logout: no matching user for provider={matched_provider}, sub={sub}, sid={sid}') + return JSONResponse(status_code=200, content={}) + + # 9. Revoke tokens and delete sessions + redis = request.app.state.redis + if not redis: + log.warning( + 'Back-channel logout: Redis not configured, cannot revoke JWT tokens. ' + 'OAuth sessions will be deleted but existing JWTs will remain valid until expiry.' + ) + + revoked_count = 0 + for user in users_to_logout: + sessions = await OAuthSessions.get_sessions_by_user_id(user.id, db=db) + for oauth_session in sessions: + await OAuthSessions.delete_session_by_id(oauth_session.id, db=db) + + if redis: + revocation_key = f'{REDIS_KEY_PREFIX}:auth:user:{user.id}:revoked_at' + await redis.set( + revocation_key, + str(int(time.time())), + ex=60 * 60 * 24 * 30, + ) + revoked_count += 1 + + log.info( + f'Back-channel logout: revoked sessions for user {user.id} ' + f'(email={user.email}, provider={matched_provider}, sessions_deleted={len(sessions)})' + ) + + log.info( + f'Back-channel logout: completed for {len(users_to_logout)} user(s), {revoked_count} revocation(s) set' + ) + return JSONResponse(status_code=200, content={}) diff --git a/backend/open_webui/utils/payload.py b/backend/open_webui/utils/payload.py new file mode 100644 index 0000000000000000000000000000000000000000..7cf4fe4de31356d1dd953d638c7a5dc612c60012 --- /dev/null +++ b/backend/open_webui/utils/payload.py @@ -0,0 +1,418 @@ +from open_webui.utils.task import prompt_template, prompt_variables_template +from open_webui.utils.misc import ( + deep_update, + add_or_update_system_message, + replace_system_message_content, +) + +from typing import Callable, Optional +import copy +import json + + +# What goes out cannot be taken back. Let it be shaped +# well before it leaves this place. +# inplace function: form_data is modified +def apply_system_prompt_to_body( + system: Optional[str], + form_data: dict, + metadata: Optional[dict] = None, + user=None, + replace: bool = False, +) -> dict: + if not system: + return form_data + + # Metadata (WebUI Usage) + if metadata: + variables = metadata.get('variables', {}) + if variables: + system = prompt_variables_template(system, variables) + + # Legacy (API Usage) + system = prompt_template(system, user) + + if replace: + form_data['messages'] = replace_system_message_content(system, form_data.get('messages', [])) + else: + form_data['messages'] = add_or_update_system_message(system, form_data.get('messages', [])) + + return form_data + + +# inplace function: form_data is modified +def apply_model_params_to_body(params: dict, form_data: dict, mappings: dict[str, Callable]) -> dict: + if not params: + return form_data + + for key, value in params.items(): + if value is not None: + if key in mappings: + cast_func = mappings[key] + if isinstance(cast_func, Callable): + form_data[key] = cast_func(value) + else: + form_data[key] = value + + return form_data + + +def remove_open_webui_params(params: dict) -> dict: + """ + Removes OpenWebUI specific parameters from the provided dictionary. + + Args: + params (dict): The dictionary containing parameters. + + Returns: + dict: The modified dictionary with OpenWebUI parameters removed. + """ + open_webui_params = { + 'stream_response': bool, + 'stream_delta_chunk_size': int, + 'function_calling': str, + 'reasoning_tags': list, + 'system': str, + } + + for key in list(params.keys()): + if key in open_webui_params: + del params[key] + + return params + + +# inplace function: form_data is modified +def apply_model_params_to_body_openai(params: dict, form_data: dict) -> dict: + params = remove_open_webui_params(params) + + custom_params = params.pop('custom_params', {}) + if custom_params: + # Attempt to parse custom_params if they are strings + for key, value in custom_params.items(): + if isinstance(value, str): + try: + # Attempt to parse the string as JSON + custom_params[key] = json.loads(value) + except json.JSONDecodeError: + # If it fails, keep the original string + pass + + # If there are custom parameters, we need to apply them first + params = deep_update(params, custom_params) + + mappings = { + 'temperature': float, + 'top_p': float, + 'min_p': float, + 'max_tokens': int, + 'frequency_penalty': float, + 'presence_penalty': float, + 'reasoning_effort': str, + 'seed': lambda x: x, + 'stop': lambda x: [bytes(s, 'utf-8').decode('unicode_escape') for s in x], + 'logit_bias': lambda x: x, + 'response_format': dict, + } + return apply_model_params_to_body(params, form_data, mappings) + + +def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict: + params = remove_open_webui_params(params) + + custom_params = params.pop('custom_params', {}) + if custom_params: + # Attempt to parse custom_params if they are strings + for key, value in custom_params.items(): + if isinstance(value, str): + try: + # Attempt to parse the string as JSON + custom_params[key] = json.loads(value) + except json.JSONDecodeError: + # If it fails, keep the original string + pass + + # If there are custom parameters, we need to apply them first + params = deep_update(params, custom_params) + + # Convert OpenAI parameter names to Ollama parameter names if needed. + name_differences = { + 'max_tokens': 'num_predict', + } + + 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 + params[value] = params[key] + del params[key] + + # See https://github.com/ollama/ollama/blob/main/docs/api.md#request-8 + mappings = { + 'temperature': float, + 'top_p': float, + 'seed': lambda x: x, + 'mirostat': int, + 'mirostat_eta': float, + 'mirostat_tau': float, + 'num_ctx': int, + 'num_batch': int, + 'num_keep': int, + 'num_predict': int, + 'repeat_last_n': int, + 'top_k': int, + 'min_p': float, + 'repeat_penalty': float, + 'presence_penalty': float, + 'frequency_penalty': float, + 'stop': lambda x: [bytes(s, 'utf-8').decode('unicode_escape') for s in x], + 'num_gpu': int, + 'use_mmap': bool, + 'use_mlock': bool, + 'num_thread': int, + } + + def parse_json(value: str) -> dict: + """ + Parses a JSON string into a dictionary, handling potential JSONDecodeError. + """ + try: + return json.loads(value) + except Exception as e: + return value + + ollama_root_params = { + 'format': lambda x: parse_json(x), + 'keep_alive': lambda x: parse_json(x), + 'think': lambda x: x, + } + + for key, value in ollama_root_params.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 + form_data[key] = value(param) + del params[key] + + # Unlike OpenAI, Ollama does not support params directly in the body + form_data['options'] = apply_model_params_to_body(params, (form_data.get('options', {}) or {}), mappings) + return form_data + + +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_message = {'role': message['role']} + + # Preserve Ollama-native 'thinking' field (used by reasoning models, + # may be injected by filter inlet functions). + if 'thinking' in message: + new_message['thinking'] = message['thinking'] + + content = message.get('content', []) + 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) + 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 + if tool_call_id: + new_message['tool_call_id'] = tool_call_id + + elif tool_calls: + # If tool calls are present, add them to the message + ollama_tool_calls = [] + for tool_call in tool_calls: + ollama_tool_call = { + 'index': tool_call.get('index', 0), + 'id': tool_call.get('id', None), + 'function': { + 'name': tool_call.get('function', {}).get('name', ''), + 'arguments': json.loads(tool_call.get('function', {}).get('arguments', {})), + }, + } + 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) + new_message['content'] = '' + + else: + # Otherwise, assume the content is a list of dicts, e.g., text followed by an image URL + content_text = '' + images = [] + + # Iterate through the list of content items + for item in content: + # Check if it's a text type + if item.get('type') == 'text': + content_text += item.get('text', '') + + # Check if it's an image URL type + 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 + if img_url.startswith('data:'): + img_url = img_url.split(',')[-1] + images.append(img_url) + + # Add content text (if any) + if content_text: + new_message['content'] = content_text.strip() + + # Add images (if any) + if images: + new_message['images'] = images + + # Append the new formatted message to the result + ollama_messages.append(new_message) + + return ollama_messages + + +def convert_payload_openai_to_ollama(openai_payload: dict) -> dict: + """ + Converts a payload formatted for OpenAI's API to be compatible with Ollama's API endpoint for chat completions. + + Args: + openai_payload (dict): The payload originally designed for OpenAI API usage. + + Returns: + dict: A modified payload compatible with the Ollama API. + """ + # Shallow copy metadata separately (may contain non-picklable objects) + metadata = openai_payload.get('metadata') + openai_payload = copy.deepcopy({k: v for k, v in openai_payload.items() if k != 'metadata'}) + if metadata is not None: + openai_payload['metadata'] = dict(metadata) + ollama_payload = {} + + # Mapping basic model and message details + ollama_payload['model'] = openai_payload.get('model') + ollama_payload['messages'] = convert_messages_openai_to_ollama(openai_payload.get('messages')) + ollama_payload['stream'] = openai_payload.get('stream', False) + if 'tools' in openai_payload: + ollama_payload['tools'] = openai_payload['tools'] + + if 'max_tokens' in openai_payload: + ollama_payload['num_predict'] = openai_payload['max_tokens'] + del openai_payload['max_tokens'] + + # If there are advanced parameters in the payload, format them in Ollama's options field + if openai_payload.get('options'): + ollama_payload['options'] = openai_payload['options'] + ollama_options = openai_payload['options'] + + def parse_json(value: str) -> dict: + """ + Parses a JSON string into a dictionary, handling potential JSONDecodeError. + """ + try: + return json.loads(value) + except Exception as e: + return value + + ollama_root_params = { + 'format': lambda x: parse_json(x), + 'keep_alive': lambda x: parse_json(x), + 'think': lambda x: x, + } + + # 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 + ollama_payload[key] = value(param) + del ollama_options[key] + + # Re-Mapping OpenAI's `max_tokens` -> Ollama's `num_predict` + if 'max_tokens' in ollama_options: + 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. + if 'system' in ollama_options: + ollama_payload['system'] = ollama_options['system'] + del ollama_options['system'] + + ollama_payload['options'] = ollama_options + + # If there is the "stop" parameter in the openai_payload, remap it to the ollama_payload.options + if 'stop' in openai_payload: + ollama_options = ollama_payload.get('options', {}) + ollama_options['stop'] = openai_payload.get('stop') + ollama_payload['options'] = ollama_options + + if 'metadata' in openai_payload: + ollama_payload['metadata'] = openai_payload['metadata'] + + if 'response_format' in openai_payload: + response_format = openai_payload['response_format'] + format_type = response_format.get('type', None) + + schema = response_format.get(format_type, None) + if schema: + format = schema.get('schema', None) + ollama_payload['format'] = format + + return ollama_payload + + +def convert_embedding_payload_openai_to_ollama(openai_payload: dict) -> dict: + """ + Convert an embeddings request payload from OpenAI format to Ollama format. + + Args: + openai_payload (dict): The original payload designed for OpenAI API usage. + + Returns: + dict: A payload compatible with the Ollama API embeddings endpoint. + """ + ollama_payload = {'model': openai_payload.get('model')} + input_value = openai_payload.get('input') + + # Ollama expects 'input' as a list, and 'prompt' as a single string. + if isinstance(input_value, list): + ollama_payload['input'] = input_value + ollama_payload['prompt'] = '\n'.join(str(x) for x in input_value) + else: + ollama_payload['input'] = [input_value] + ollama_payload['prompt'] = str(input_value) + + # Optionally forward other fields if present + for optional_key in ('options', 'truncate', 'keep_alive'): + if optional_key in openai_payload: + ollama_payload[optional_key] = openai_payload[optional_key] + + return ollama_payload + + +def convert_embed_payload_openai_to_ollama(openai_payload: dict) -> dict: + """ + Convert an embeddings request payload from OpenAI format to Ollama's + /api/embed format, which supports batch input natively. + + Args: + openai_payload (dict): The original payload designed for OpenAI API usage. + Expected keys: "model", "input" (str or list[str]). + + Returns: + dict: A payload compatible with the Ollama /api/embed endpoint. + """ + ollama_payload = {'model': openai_payload.get('model')} + input_value = openai_payload.get('input') + + # /api/embed accepts 'input' as a string or list of strings directly + ollama_payload['input'] = input_value + + # Optionally forward other fields if present + for optional_key in ('truncate', 'options', 'keep_alive'): + if optional_key in openai_payload: + ollama_payload[optional_key] = openai_payload[optional_key] + + return ollama_payload diff --git a/backend/open_webui/utils/pdf_generator.py b/backend/open_webui/utils/pdf_generator.py new file mode 100644 index 0000000000000000000000000000000000000000..3db4297a21d634c3db9d708e19ad9094071be261 --- /dev/null +++ b/backend/open_webui/utils/pdf_generator.py @@ -0,0 +1,142 @@ +from datetime import datetime +from io import BytesIO +from pathlib import Path +from typing import Dict, Any, List +from html import escape + +from markdown import markdown + +import site +from fpdf import FPDF + +from open_webui.env import STATIC_DIR, FONTS_DIR +from open_webui.models.chats import ChatTitleMessagesForm + + +class PDFGenerator: + """ + Description: + The `PDFGenerator` class is designed to create PDF documents from chat messages. + The process involves transforming markdown content into HTML and then into a PDF format + + Attributes: + - `form_data`: An instance of `ChatTitleMessagesForm` containing title and messages. + + """ + + def __init__(self, form_data: ChatTitleMessagesForm): + self.html_body = None + self.messages_html = None + self.form_data = form_data + + self.css = Path(STATIC_DIR / 'assets' / 'pdf-style.css').read_text() + + def format_timestamp(self, timestamp: float) -> str: + """Convert a UNIX timestamp to a formatted date string.""" + try: + date_time = datetime.fromtimestamp(timestamp) + return date_time.strftime('%Y-%m-%d, %H:%M:%S') + except (ValueError, TypeError) as e: + # Log the error if necessary + return '' + + def _build_html_message(self, message: Dict[str, Any]) -> str: + """Build HTML for a single message.""" + role = escape(message.get('role', 'user')) + content = escape(message.get('content', '')) + timestamp = message.get('timestamp') + + model = escape(message.get('model') if role == 'assistant' else '') + + date_str = escape(self.format_timestamp(timestamp) if timestamp else '') + + # extends pymdownx extension to convert markdown to html. + # - https://facelessuser.github.io/pymdown-extensions/usage_notes/ + # html_content = markdown(content, extensions=["pymdownx.extra"]) + + content = content.replace('\n', '
      ') + html_message = f""" +
      +
      +

      + {role.title()} + {model} +

      +
      {date_str}
      +
      +
      +
      + +
      + {content} +
      +
      +
      + """ + return html_message + + def _generate_html_body(self) -> str: + """Generate the full HTML body for the PDF.""" + escaped_title = escape(self.form_data.title) + return f""" + + + + + +
      +
      +

      {escaped_title}

      + {self.messages_html} +
      +
      + + + """ + + def generate_chat_pdf(self) -> bytes: + """ + Generate a PDF from chat messages. + """ + try: + global FONTS_DIR + + pdf = FPDF() + pdf.add_page() + + # When running using `pip install` the static directory is in the site packages. + if not FONTS_DIR.exists(): + FONTS_DIR = Path(site.getsitepackages()[0]) / 'static/fonts' + # When running using `pip install -e .` the static directory is in the site packages. + # This path only works if `open-webui serve` is run from the root of this project. + if not FONTS_DIR.exists(): + FONTS_DIR = Path('.') / 'backend' / 'static' / 'fonts' + + pdf.add_font('NotoSans', '', f'{FONTS_DIR}/NotoSans-Regular.ttf') + pdf.add_font('NotoSans', 'b', f'{FONTS_DIR}/NotoSans-Bold.ttf') + pdf.add_font('NotoSans', 'i', f'{FONTS_DIR}/NotoSans-Italic.ttf') + pdf.add_font('NotoSansKR', '', f'{FONTS_DIR}/NotoSansKR-Regular.ttf') + pdf.add_font('NotoSansJP', '', f'{FONTS_DIR}/NotoSansJP-Regular.ttf') + pdf.add_font('NotoSansSC', '', f'{FONTS_DIR}/NotoSansSC-Regular.ttf') + pdf.add_font('Twemoji', '', f'{FONTS_DIR}/Twemoji.ttf') + + pdf.set_font('NotoSans', size=12) + pdf.set_fallback_fonts(['NotoSansKR', 'NotoSansJP', 'NotoSansSC', 'Twemoji']) + + pdf.set_auto_page_break(auto=True, margin=15) + + # Build HTML messages + messages_html_list: List[str] = [self._build_html_message(msg) for msg in self.form_data.messages] + self.messages_html = '
      ' + ''.join(messages_html_list) + '
      ' + + # Generate full HTML body + self.html_body = self._generate_html_body() + + pdf.write_html(self.html_body) + + # Save the pdf with name .pdf + pdf_bytes = pdf.output() + + return bytes(pdf_bytes) + except Exception as e: + raise e diff --git a/backend/open_webui/utils/plugin.py b/backend/open_webui/utils/plugin.py new file mode 100644 index 0000000000000000000000000000000000000000..43ff4fe2e7487fcfa5ee7d57f5923be2bf878b7a --- /dev/null +++ b/backend/open_webui/utils/plugin.py @@ -0,0 +1,436 @@ +import os +import re +import subprocess +import sys +from importlib import util +import types +import tempfile +import logging +from typing import Any + +from open_webui.env import ( + PIP_OPTIONS, + PIP_PACKAGE_INDEX_OPTIONS, + OFFLINE_MODE, + ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS, +) +from open_webui.models.functions import FunctionModel, Functions +from open_webui.models.tools import Tools + +log = logging.getLogger(__name__) + + +def resolve_valves_schema_options(valves_class: type, schema: dict, user: Any = None) -> dict: + """ + Resolve dynamic options in a Valves schema. + + For properties with `input.options`, this function handles two cases: + - List: Used directly as dropdown options + - String: Treated as method name, called to get options dynamically + + Usage in Valves: + class UserValves(BaseModel): + # Static options + priority: str = Field( + default="medium", + json_schema_extra={ + "input": { + "type": "select", + "options": ["low", "medium", "high"] + } + } + ) + + # Dynamic options (method name) + model: str = Field( + default="", + json_schema_extra={ + "input": { + "type": "select", + "options": "get_model_options" + } + } + ) + + @classmethod + def get_model_options(cls, __user__=None) -> list[dict]: + return [{"value": "gpt-4", "label": "GPT-4"}] + + Args: + valves_class: The Valves or UserValves Pydantic model class + schema: The JSON schema dict from valves_class.schema() + user: Optional user object passed to methods that accept __user__ + + Returns: + Modified schema dict with resolved options + """ + if not schema or 'properties' not in schema: + return schema + + # Make a copy to avoid mutating the original + schema = dict(schema) + schema['properties'] = dict(schema.get('properties', {})) + + for prop_name, prop_schema in list(schema['properties'].items()): + # Get the original field info from the Pydantic model + if not hasattr(valves_class, 'model_fields'): + continue + + field_info = valves_class.model_fields.get(prop_name) + if not field_info: + continue + + # Check json_schema_extra for options + json_schema_extra = field_info.json_schema_extra + if not json_schema_extra or not isinstance(json_schema_extra, dict): + continue + + input_config = json_schema_extra.get('input') + if not input_config or not isinstance(input_config, dict): + continue + + options = input_config.get('options') + if options is None: + continue + + resolved_options = None + + # Case 1: options is already a list - use directly + if isinstance(options, list): + resolved_options = options + + # Case 2: options is a string - treat as method name + elif isinstance(options, str) and options: + method = getattr(valves_class, options, None) + if method is None or not callable(method): + log.warning(f"options '{options}' not found or not callable on {valves_class.__name__}") + continue + + try: + import inspect + + sig = inspect.signature(method) + params = sig.parameters + + # Prepare kwargs based on what the method accepts + kwargs = {} + if '__user__' in params and user is not None: + kwargs['__user__'] = user.model_dump() if hasattr(user, 'model_dump') else user + if 'user' in params and user is not None: + kwargs['user'] = user.model_dump() if hasattr(user, 'model_dump') else user + + resolved_options = method(**kwargs) if kwargs else method() + + # Validate return type + if not isinstance(resolved_options, list): + log.warning(f"Method '{options}' did not return a list for {prop_name}") + continue + + except Exception as e: + log.warning(f'Failed to resolve options for {prop_name}: {e}') + continue + else: + # Invalid options type - skip + continue + + # Update the schema with resolved options + schema['properties'][prop_name] = dict(prop_schema) + if 'input' not in schema['properties'][prop_name]: + schema['properties'][prop_name]['input'] = {'type': 'select'} + else: + schema['properties'][prop_name]['input'] = dict(schema['properties'][prop_name].get('input', {})) + schema['properties'][prop_name]['input']['options'] = resolved_options + + return schema + + +def extract_frontmatter(content): + """ + Extract frontmatter as a dictionary from the provided content string. + """ + frontmatter = {} + frontmatter_started = False + frontmatter_ended = False + frontmatter_pattern = re.compile(r'^\s*([a-z_]+):\s*(.*)\s*$', re.IGNORECASE) + + try: + lines = content.splitlines() + if len(lines) < 1 or lines[0].strip() != '"""': + # The content doesn't start with triple quotes + return {} + + frontmatter_started = True + + for line in lines[1:]: + if '"""' in line: + if frontmatter_started: + frontmatter_ended = True + break + + if frontmatter_started and not frontmatter_ended: + match = frontmatter_pattern.match(line) + if match: + key, value = match.groups() + frontmatter[key.strip()] = value.strip() + + except Exception as e: + log.exception(f'Failed to extract frontmatter: {e}') + return {} + + return frontmatter + + +def replace_imports(content): + """ + Replace the import paths in the content. + """ + replacements = { + 'from utils': 'from open_webui.utils', + 'from apps': 'from open_webui.apps', + 'from main': 'from open_webui.main', + 'from config': 'from open_webui.config', + } + + for old, new in replacements.items(): + content = content.replace(old, new) + + return content + + +# May the intent of the one who wrote it survive every +# import and transformation, as a deed survives the generations. +async def load_tool_module_by_id(tool_id, content=None): + if content is None: + tool = await Tools.get_tool_by_id(tool_id) + if not tool: + raise Exception(f'Toolkit not found: {tool_id}') + + content = tool.content + + content = replace_imports(content) + await Tools.update_tool_by_id(tool_id, {'content': content}) + else: + frontmatter = extract_frontmatter(content) + # Install required packages found within the frontmatter + install_frontmatter_requirements(frontmatter.get('requirements', '')) + + module_name = f'tool_{tool_id}' + 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. + temp_file = tempfile.NamedTemporaryFile(delete=False) + temp_file.close() + try: + with open(temp_file.name, 'w', encoding='utf-8') as f: + f.write(content) + module.__dict__['__file__'] = temp_file.name + + # Executing the modified content in the created module's namespace + exec(content, module.__dict__) + frontmatter = extract_frontmatter(content) + log.info(f'Loaded module: {module.__name__}') + + # Create and return the object if the class 'Tools' is found in the module + if hasattr(module, 'Tools'): + return module.Tools(), frontmatter + else: + raise Exception('No Tools class found in the module') + except Exception as e: + log.error(f'Error loading module: {tool_id}: {e}') + del sys.modules[module_name] # Clean up + raise e + finally: + os.unlink(temp_file.name) + + +async def load_function_module_by_id(function_id: str, content: str | None = None): + if content is None: + function = await Functions.get_function_by_id(function_id) + if not function: + raise Exception(f'Function not found: {function_id}') + content = function.content + + content = replace_imports(content) + await Functions.update_function_by_id(function_id, {'content': content}) + else: + frontmatter = extract_frontmatter(content) + install_frontmatter_requirements(frontmatter.get('requirements', '')) + + module_name = f'function_{function_id}' + 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. + temp_file = tempfile.NamedTemporaryFile(delete=False) + temp_file.close() + try: + with open(temp_file.name, 'w', encoding='utf-8') as f: + f.write(content) + module.__dict__['__file__'] = temp_file.name + + # Execute the modified content in the created module's namespace + exec(content, module.__dict__) + frontmatter = extract_frontmatter(content) + log.info(f'Loaded module: {module.__name__}') + + # Create appropriate object based on available class type in the module + if hasattr(module, 'Pipe'): + return module.Pipe(), 'pipe', frontmatter + elif hasattr(module, 'Filter'): + return module.Filter(), 'filter', frontmatter + elif hasattr(module, 'Action'): + return module.Action(), 'action', frontmatter + else: + raise Exception('No Function class found in the module') + except Exception as e: + log.error(f'Error loading module: {function_id}: {e}') + # Cleanup by removing the module in case of error + del sys.modules[module_name] + + await Functions.update_function_by_id(function_id, {'is_active': False}) + raise e + finally: + os.unlink(temp_file.name) + + +async def get_tool_module_from_cache(request, tool_id, load_from_db=True): + if load_from_db: + # Always load from the database by default + tool = await Tools.get_tool_by_id(tool_id) + if not tool: + raise Exception(f'Tool not found: {tool_id}') + content = tool.content + + new_content = replace_imports(content) + if new_content != content: + content = new_content + # Update the tool content in the database + await Tools.update_tool_by_id(tool_id, {'content': content}) + + if (hasattr(request.app.state, 'TOOL_CONTENTS') and tool_id in request.app.state.TOOL_CONTENTS) and ( + hasattr(request.app.state, 'TOOLS') and tool_id in request.app.state.TOOLS + ): + if request.app.state.TOOL_CONTENTS[tool_id] == content: + return request.app.state.TOOLS[tool_id], None + + tool_module, frontmatter = await load_tool_module_by_id(tool_id, content) + else: + if hasattr(request.app.state, 'TOOLS') and tool_id in request.app.state.TOOLS: + return request.app.state.TOOLS[tool_id], None + + tool_module, frontmatter = await load_tool_module_by_id(tool_id) + + if not hasattr(request.app.state, 'TOOLS'): + request.app.state.TOOLS = {} + + if not hasattr(request.app.state, 'TOOL_CONTENTS'): + request.app.state.TOOL_CONTENTS = {} + + request.app.state.TOOLS[tool_id] = tool_module + request.app.state.TOOL_CONTENTS[tool_id] = content + + return tool_module, frontmatter + + +async def get_function_module_from_cache( + request, function_id, function: FunctionModel | None = None, load_from_db=True +): + if load_from_db: + # Always load from the database by default + # This is useful for hooks like "inlet" or "outlet" where the content might change + # and we want to ensure the latest content is used. + + if function is None: + function = await Functions.get_function_by_id(function_id) + if not function: + raise Exception(f'Function not found: {function_id}') + content = function.content + + new_content = replace_imports(content) + if new_content != content: + content = new_content + # Update the function content in the database + await Functions.update_function_by_id(function_id, {'content': content}) + + if ( + hasattr(request.app.state, 'FUNCTION_CONTENTS') and function_id in request.app.state.FUNCTION_CONTENTS + ) and (hasattr(request.app.state, 'FUNCTIONS') and function_id in request.app.state.FUNCTIONS): + if request.app.state.FUNCTION_CONTENTS[function_id] == content: + return request.app.state.FUNCTIONS[function_id], None, None + + function_module, function_type, frontmatter = await load_function_module_by_id(function_id, content) + else: + # Load from cache (e.g. "stream" hook) + # This is useful for performance reasons + + if hasattr(request.app.state, 'FUNCTIONS') and function_id in request.app.state.FUNCTIONS: + return request.app.state.FUNCTIONS[function_id], None, None + + function_module, function_type, frontmatter = await load_function_module_by_id(function_id) + + if not hasattr(request.app.state, 'FUNCTIONS'): + request.app.state.FUNCTIONS = {} + + if not hasattr(request.app.state, 'FUNCTION_CONTENTS'): + request.app.state.FUNCTION_CONTENTS = {} + + request.app.state.FUNCTIONS[function_id] = function_module + request.app.state.FUNCTION_CONTENTS[function_id] = content + + return function_module, function_type, frontmatter + + +def install_frontmatter_requirements(requirements: str): + if not ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS: + log.info('ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS is disabled, skipping installation of requirements.') + return + + if OFFLINE_MODE: + log.info('Offline mode enabled, skipping installation of requirements.') + return + + if requirements: + try: + req_list = [req.strip() for req in requirements.split(',')] + log.info(f'Installing requirements: {" ".join(req_list)}') + subprocess.check_call( + [sys.executable, '-m', 'pip', 'install'] + PIP_OPTIONS + req_list + PIP_PACKAGE_INDEX_OPTIONS + ) + except Exception as e: + log.error(f'Error installing packages: {" ".join(req_list)}') + raise e + + else: + log.info('No requirements found in frontmatter.') + + +async def install_tool_and_function_dependencies(): + """ + Install all dependencies for all admin tools and active functions. + + By first collecting all dependencies from the frontmatter of each tool and function, + and then installing them using pip. Duplicates or similar version specifications are + handled by pip as much as possible. + """ + function_list = await Functions.get_functions(active_only=True) + tool_list = await Tools.get_tools() + + all_dependencies = '' + try: + for function in function_list: + frontmatter = extract_frontmatter(replace_imports(function.content)) + if dependencies := frontmatter.get('requirements'): + all_dependencies += f'{dependencies}, ' + for tool in tool_list: + # Only install requirements for admin tools + if tool.user and tool.user.role == 'admin': + frontmatter = extract_frontmatter(replace_imports(tool.content)) + if dependencies := frontmatter.get('requirements'): + all_dependencies += f'{dependencies}, ' + + install_frontmatter_requirements(all_dependencies.strip(', ')) + except Exception as e: + log.error(f'Error installing requirements: {e}') diff --git a/backend/open_webui/utils/rate_limit.py b/backend/open_webui/utils/rate_limit.py new file mode 100644 index 0000000000000000000000000000000000000000..93f3851d1f5a8f620a230058c13b5e7e2ac7ee7f --- /dev/null +++ b/backend/open_webui/utils/rate_limit.py @@ -0,0 +1,135 @@ +import time +from typing import Optional, Dict +from open_webui.env import REDIS_KEY_PREFIX + + +class RateLimiter: + """ + General-purpose rate limiter using Redis with a rolling window strategy. + Falls back to in-memory storage if Redis is not available. + """ + + # In-memory fallback storage + _memory_store: Dict[str, Dict[int, int]] = {} + + def __init__( + self, + redis_client, + limit: int, + window: int, + bucket_size: int = 60, + enabled: bool = True, + ): + """ + :param redis_client: Redis client instance or None + :param limit: Max allowed events in the window + :param window: Time window in seconds + :param bucket_size: Bucket resolution + :param enabled: Turn on/off rate limiting globally + """ + self.r = redis_client + self.limit = limit + self.window = window + self.bucket_size = bucket_size + self.num_buckets = window // bucket_size + self.enabled = enabled + + def _bucket_key(self, key: str, bucket_index: int) -> str: + return f'{REDIS_KEY_PREFIX}:ratelimit:{key.lower()}:{bucket_index}' + + def _current_bucket(self) -> int: + return int(time.time()) // self.bucket_size + + def _redis_available(self) -> bool: + return self.r is not None + + def is_limited(self, key: str) -> bool: + """ + Main rate-limit check. + Gracefully handles missing or failing Redis. + """ + if not self.enabled: + return False + + if self._redis_available(): + try: + return self._is_limited_redis(key) + except Exception: + return self._is_limited_memory(key) + else: + return self._is_limited_memory(key) + + def get_count(self, key: str) -> int: + if not self.enabled: + return 0 + + if self._redis_available(): + try: + return self._get_count_redis(key) + except Exception: + return self._get_count_memory(key) + else: + return self._get_count_memory(key) + + def remaining(self, key: str) -> int: + used = self.get_count(key) + return max(0, self.limit - used) + + def _is_limited_redis(self, key: str) -> bool: + now_bucket = self._current_bucket() + bucket_key = self._bucket_key(key, now_bucket) + + attempts = self.r.incr(bucket_key) + if attempts == 1: + self.r.expire(bucket_key, self.window + self.bucket_size) + + # Collect buckets + buckets = [self._bucket_key(key, now_bucket - i) for i in range(self.num_buckets + 1)] + + counts = self.r.mget(buckets) + total = sum(int(c) for c in counts if c) + + return total > self.limit + + def _get_count_redis(self, key: str) -> int: + now_bucket = self._current_bucket() + buckets = [self._bucket_key(key, now_bucket - i) for i in range(self.num_buckets + 1)] + counts = self.r.mget(buckets) + return sum(int(c) for c in counts if c) + + def _is_limited_memory(self, key: str) -> bool: + now_bucket = self._current_bucket() + + # Init storage + if key not in self._memory_store: + self._memory_store[key] = {} + + store = self._memory_store[key] + + # Increment bucket + store[now_bucket] = store.get(now_bucket, 0) + 1 + + # Drop expired buckets + min_bucket = now_bucket - self.num_buckets + expired = [b for b in store if b < min_bucket] + for b in expired: + del store[b] + + # Count totals + total = sum(store.values()) + return total > self.limit + + def _get_count_memory(self, key: str) -> int: + now_bucket = self._current_bucket() + if key not in self._memory_store: + return 0 + + store = self._memory_store[key] + min_bucket = now_bucket - self.num_buckets + + # Remove expired + expired = [b for b in store if b < min_bucket] + for b in expired: + del store[b] + + return sum(store.values()) diff --git a/backend/open_webui/utils/redis.py b/backend/open_webui/utils/redis.py new file mode 100644 index 0000000000000000000000000000000000000000..e14a0079ec9c04b418bbfdde2b2d6a4913dbc102 --- /dev/null +++ b/backend/open_webui/utils/redis.py @@ -0,0 +1,306 @@ +import inspect +from urllib.parse import urlparse +import asyncio +import time + +import logging + +import redis + +from open_webui.env import ( + REDIS_CLUSTER, + REDIS_HEALTH_CHECK_INTERVAL, + REDIS_SOCKET_CONNECT_TIMEOUT, + REDIS_SOCKET_KEEPALIVE, + REDIS_SENTINEL_HOSTS, + REDIS_SENTINEL_MAX_RETRY_COUNT, + REDIS_SENTINEL_PORT, + REDIS_URL, + REDIS_RECONNECT_DELAY, +) + +log = logging.getLogger(__name__) + +MAX_RETRY_COUNT = REDIS_SENTINEL_MAX_RETRY_COUNT + + +# Let not our connections be timed out but deliver them from +# partition. For the cache and the socket and the uptime +# belong to the one who first opened them, now and always. +_CONNECTION_CACHE = {} + + +class SentinelRedisProxy: + def __init__(self, sentinel, service, *, async_mode: bool = True, **kw): + self._sentinel = sentinel + self._service = service + self._kw = kw + self._async_mode = async_mode + + def _master(self): + return self._sentinel.master_for(self._service, **self._kw) + + def __getattr__(self, item): + master = self._master() + orig_attr = getattr(master, item) + + if not callable(orig_attr): + return orig_attr + + FACTORY_METHODS = {'pipeline', 'pubsub', 'monitor', 'client', 'transaction'} + if item in FACTORY_METHODS: + return orig_attr + + if self._async_mode: + if inspect.isasyncgenfunction(orig_attr): + + def _wrapped_iter(*args, **kwargs): + async def _iter(): + for i in range(REDIS_SENTINEL_MAX_RETRY_COUNT): + try: + method = getattr(self._master(), item) + async for value in method(*args, **kwargs): + yield value + return + except ( + redis.exceptions.ConnectionError, + redis.exceptions.ReadOnlyError, + ) as e: + if i < REDIS_SENTINEL_MAX_RETRY_COUNT - 1: + log.debug( + 'Redis sentinel fail-over (%s). Retry %s/%s', + type(e).__name__, + i + 1, + REDIS_SENTINEL_MAX_RETRY_COUNT, + ) + if REDIS_RECONNECT_DELAY: + time.sleep(REDIS_RECONNECT_DELAY / 1000) + continue + log.error( + 'Redis operation failed after %s retries: %s', + REDIS_SENTINEL_MAX_RETRY_COUNT, + e, + ) + raise e from e + + return _iter() + + return _wrapped_iter + + async def _wrapped(*args, **kwargs): + for i in range(REDIS_SENTINEL_MAX_RETRY_COUNT): + try: + method = getattr(self._master(), item) + result = method(*args, **kwargs) + if inspect.iscoroutine(result): + return await result + return result + except ( + redis.exceptions.ConnectionError, + redis.exceptions.ReadOnlyError, + ) as e: + if i < REDIS_SENTINEL_MAX_RETRY_COUNT - 1: + log.debug( + 'Redis sentinel fail-over (%s). Retry %s/%s', + type(e).__name__, + i + 1, + REDIS_SENTINEL_MAX_RETRY_COUNT, + ) + if REDIS_RECONNECT_DELAY: + await asyncio.sleep(REDIS_RECONNECT_DELAY / 1000) + continue + log.error( + 'Redis operation failed after %s retries: %s', + REDIS_SENTINEL_MAX_RETRY_COUNT, + e, + ) + raise e from e + + return _wrapped + + else: + + def _wrapped(*args, **kwargs): + for i in range(REDIS_SENTINEL_MAX_RETRY_COUNT): + try: + method = getattr(self._master(), item) + return method(*args, **kwargs) + except ( + redis.exceptions.ConnectionError, + redis.exceptions.ReadOnlyError, + ) as e: + if i < REDIS_SENTINEL_MAX_RETRY_COUNT - 1: + log.debug( + 'Redis sentinel fail-over (%s). Retry %s/%s', + type(e).__name__, + i + 1, + REDIS_SENTINEL_MAX_RETRY_COUNT, + ) + if REDIS_RECONNECT_DELAY: + time.sleep(REDIS_RECONNECT_DELAY / 1000) + continue + log.error( + 'Redis operation failed after %s retries: %s', + REDIS_SENTINEL_MAX_RETRY_COUNT, + e, + ) + raise e from e + + return _wrapped + + +def parse_redis_service_url(redis_url): + parsed_url = urlparse(redis_url) + if parsed_url.scheme != 'redis' and parsed_url.scheme != 'rediss': + raise ValueError("Invalid Redis URL scheme. Must be 'redis' or 'rediss'.") + + return { + 'username': parsed_url.username or None, + 'password': parsed_url.password or None, + 'service': parsed_url.hostname or 'mymaster', + 'port': parsed_url.port or 6379, + 'db': int(parsed_url.path.lstrip('/') or 0), + } + + +def get_redis_client(async_mode=False): + try: + return get_redis_connection( + redis_url=REDIS_URL, + redis_sentinels=get_sentinels_from_env(REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT), + redis_cluster=REDIS_CLUSTER, + async_mode=async_mode, + ) + except Exception as e: + log.debug(f'Failed to get Redis client: {e}') + return None + + +def get_redis_connection( + redis_url, + redis_sentinels, + redis_cluster=False, + async_mode=False, + decode_responses=True, +): + cache_key = ( + redis_url, + tuple(redis_sentinels) if redis_sentinels else (), + async_mode, + decode_responses, + ) + + if cache_key in _CONNECTION_CACHE: + return _CONNECTION_CACHE[cache_key] + + connection = None + + connect_timeout_kwargs = ( + {'socket_connect_timeout': REDIS_SOCKET_CONNECT_TIMEOUT} if REDIS_SOCKET_CONNECT_TIMEOUT is not None else {} + ) + + keepalive_kwargs = {'socket_keepalive': True} if REDIS_SOCKET_KEEPALIVE else {} + + health_check_kwargs = {'health_check_interval': REDIS_HEALTH_CHECK_INTERVAL} if REDIS_HEALTH_CHECK_INTERVAL else {} + + if async_mode: + import redis.asyncio as redis + + # If using sentinel in async mode + if redis_sentinels: + redis_config = parse_redis_service_url(redis_url) + sentinel = redis.sentinel.Sentinel( + redis_sentinels, + port=redis_config['port'], + db=redis_config['db'], + username=redis_config['username'], + password=redis_config['password'], + decode_responses=decode_responses, + socket_connect_timeout=REDIS_SOCKET_CONNECT_TIMEOUT, + **keepalive_kwargs, + **health_check_kwargs, + ) + connection = SentinelRedisProxy( + sentinel, + redis_config['service'], + async_mode=async_mode, + ) + elif redis_cluster: + if not redis_url: + raise ValueError('Redis URL must be provided for cluster mode.') + return redis.cluster.RedisCluster.from_url( + redis_url, + decode_responses=decode_responses, + **connect_timeout_kwargs, + **keepalive_kwargs, + **health_check_kwargs, + ) + elif redis_url: + connection = redis.from_url( + redis_url, + decode_responses=decode_responses, + **connect_timeout_kwargs, + **keepalive_kwargs, + **health_check_kwargs, + ) + else: + import redis + + if redis_sentinels: + redis_config = parse_redis_service_url(redis_url) + sentinel = redis.sentinel.Sentinel( + redis_sentinels, + port=redis_config['port'], + db=redis_config['db'], + username=redis_config['username'], + password=redis_config['password'], + decode_responses=decode_responses, + socket_connect_timeout=REDIS_SOCKET_CONNECT_TIMEOUT, + **keepalive_kwargs, + **health_check_kwargs, + ) + connection = SentinelRedisProxy( + sentinel, + redis_config['service'], + async_mode=async_mode, + ) + elif redis_cluster: + if not redis_url: + raise ValueError('Redis URL must be provided for cluster mode.') + return redis.cluster.RedisCluster.from_url( + redis_url, + decode_responses=decode_responses, + **connect_timeout_kwargs, + **keepalive_kwargs, + **health_check_kwargs, + ) + elif redis_url: + connection = redis.Redis.from_url( + redis_url, + decode_responses=decode_responses, + **connect_timeout_kwargs, + **keepalive_kwargs, + **health_check_kwargs, + ) + + _CONNECTION_CACHE[cache_key] = connection + return connection + + +def get_sentinels_from_env(sentinel_hosts_env, sentinel_port_env): + if sentinel_hosts_env: + sentinel_hosts = sentinel_hosts_env.split(',') + sentinel_port = int(sentinel_port_env) + return [(host, sentinel_port) for host in sentinel_hosts] + return [] + + +def get_sentinel_url_from_env(redis_url, sentinel_hosts_env, sentinel_port_env): + redis_config = parse_redis_service_url(redis_url) + username = redis_config['username'] or '' + password = redis_config['password'] or '' + auth_part = '' + if username or password: + auth_part = f'{username}:{password}@' + hosts_part = ','.join(f'{host}:{sentinel_port_env}' for host in sentinel_hosts_env.split(',')) + return f'redis+sentinel://{auth_part}{hosts_part}/{redis_config["db"]}/{redis_config["service"]}' diff --git a/backend/open_webui/utils/response.py b/backend/open_webui/utils/response.py new file mode 100644 index 0000000000000000000000000000000000000000..676a07525ea05bc7d8820df15f357f31425b61d5 --- /dev/null +++ b/backend/open_webui/utils/response.py @@ -0,0 +1,243 @@ +import json +from uuid import uuid4 +from open_webui.utils.misc import ( + openai_chat_chunk_message_template, + openai_chat_completion_message_template, +) + + +# An honest ledger is worth more than a flattering one. +# Let every cost here be counted true. +def normalize_usage(usage: dict) -> dict: + """ + Normalize usage statistics to standard format. + Handles OpenAI, Ollama, and llama.cpp formats. + + Adds standardized token fields to the original data: + - input_tokens: Number of tokens in the prompt + - output_tokens: Number of tokens generated + - total_tokens: Sum of input and output tokens + """ + if not usage: + return {} + + # Map various field names to standard names + input_tokens = ( + usage.get('input_tokens') # Already standard + or usage.get('prompt_tokens') # OpenAI + or usage.get('prompt_eval_count') # Ollama + or usage.get('prompt_n') # llama.cpp + or 0 + ) + + output_tokens = ( + usage.get('output_tokens') # Already standard + or usage.get('completion_tokens') # OpenAI + or usage.get('eval_count') # Ollama + or usage.get('predicted_n') # llama.cpp + or 0 + ) + + total_tokens = usage.get('total_tokens') or (input_tokens + output_tokens) + + # Add standardized fields to original data + result = dict(usage) + result['input_tokens'] = int(input_tokens) + result['output_tokens'] = int(output_tokens) + result['total_tokens'] = int(total_tokens) + + return result + + +def convert_ollama_tool_call_to_openai(tool_calls: list) -> list: + openai_tool_calls = [] + for tool_call in tool_calls: + function = tool_call.get('function', {}) + openai_tool_call = { + 'index': tool_call.get('index', function.get('index', 0)), + 'id': tool_call.get('id', f'call_{str(uuid4())}'), + 'type': 'function', + 'function': { + 'name': function.get('name', ''), + 'arguments': json.dumps(function.get('arguments', {})), + }, + } + openai_tool_calls.append(openai_tool_call) + return openai_tool_calls + + +def convert_ollama_usage_to_openai(data: dict) -> dict: + input_tokens = int(data.get('prompt_eval_count', 0)) + output_tokens = int(data.get('eval_count', 0)) + total_tokens = input_tokens + output_tokens + + return { + # Standardized fields + 'input_tokens': input_tokens, + 'output_tokens': output_tokens, + 'total_tokens': total_tokens, + # OpenAI-compatible fields (for backward compatibility) + 'prompt_tokens': input_tokens, + 'completion_tokens': output_tokens, + # Ollama-specific metrics + 'response_token/s': ( + round( + ((data.get('eval_count', 0) / (data.get('eval_duration', 0) / 10_000_000)) * 100), + 2, + ) + if data.get('eval_duration', 0) > 0 + else 'N/A' + ), + 'prompt_token/s': ( + round( + ((data.get('prompt_eval_count', 0) / (data.get('prompt_eval_duration', 0) / 10_000_000)) * 100), + 2, + ) + if data.get('prompt_eval_duration', 0) > 0 + else 'N/A' + ), + 'total_duration': data.get('total_duration', 0), + 'load_duration': data.get('load_duration', 0), + 'prompt_eval_count': data.get('prompt_eval_count', 0), + 'prompt_eval_duration': data.get('prompt_eval_duration', 0), + 'eval_count': data.get('eval_count', 0), + 'eval_duration': data.get('eval_duration', 0), + 'approximate_total': (lambda s: f'{s // 3600}h{(s % 3600) // 60}m{s % 60}s')( + (data.get('total_duration', 0) or 0) // 1_000_000_000 + ), + 'completion_tokens_details': { + 'reasoning_tokens': 0, + 'accepted_prediction_tokens': 0, + 'rejected_prediction_tokens': 0, + }, + } + + +def convert_response_ollama_to_openai(ollama_response: dict) -> dict: + model = ollama_response.get('model', 'ollama') + message_content = ollama_response.get('message', {}).get('content', '') + reasoning_content = ollama_response.get('message', {}).get('thinking', None) + tool_calls = ollama_response.get('message', {}).get('tool_calls', None) + openai_tool_calls = None + + if tool_calls: + openai_tool_calls = convert_ollama_tool_call_to_openai(tool_calls) + + data = ollama_response + + usage = convert_ollama_usage_to_openai(data) + + response = openai_chat_completion_message_template( + model, message_content, reasoning_content, openai_tool_calls, usage + ) + return response + + +async def convert_streaming_response_ollama_to_openai(ollama_streaming_response): + has_tool_calls = False + # All chunks in a single completion must share the same id (OpenAI spec). + completion_id = f'chatcmpl-{str(uuid4())}' + first = True + async for data in ollama_streaming_response.body_iterator: + data = json.loads(data) + + model = data.get('model', 'ollama') + message_content = data.get('message', {}).get('content', None) + reasoning_content = data.get('message', {}).get('thinking', None) + tool_calls = data.get('message', {}).get('tool_calls', None) + openai_tool_calls = None + + if tool_calls: + openai_tool_calls = convert_ollama_tool_call_to_openai(tool_calls) + has_tool_calls = True + + done = data.get('done', False) + + usage = None + if done: + usage = convert_ollama_usage_to_openai(data) + + data = openai_chat_chunk_message_template(model, message_content, reasoning_content, openai_tool_calls, usage) + data['id'] = completion_id + + # First chunk must carry delta.role (OpenAI spec). + if first: + data['choices'][0]['delta']['role'] = 'assistant' + first = False + + if done and has_tool_calls: + data['choices'][0]['finish_reason'] = 'tool_calls' + + line = f'data: {json.dumps(data)}\n\n' + yield line + + yield 'data: [DONE]\n\n' + + +def convert_embedding_response_ollama_to_openai(response) -> dict: + """ + Convert the response from Ollama embeddings endpoint to the OpenAI-compatible format. + + Args: + response (dict): The response from the Ollama API, + e.g. {"embedding": [...], "model": "..."} + or {"embeddings": [{"embedding": [...], "index": 0}, ...], "model": "..."} + + Returns: + dict: Response adapted to OpenAI's embeddings API format. + e.g. { + "object": "list", + "data": [ + {"object": "embedding", "embedding": [...], "index": 0}, + ... + ], + "model": "...", + } + """ + # Ollama batch-style output from /api/embed + # Response format: {"embeddings": [[0.1, 0.2, ...], [0.3, 0.4, ...]], "model": "..."} + if isinstance(response, dict) and 'embeddings' in response: + openai_data = [] + for i, emb in enumerate(response['embeddings']): + # /api/embed returns embeddings as plain float lists + if isinstance(emb, list): + openai_data.append( + { + 'object': 'embedding', + 'embedding': emb, + 'index': i, + } + ) + # Also handle dict format for robustness + elif isinstance(emb, dict): + openai_data.append( + { + 'object': 'embedding', + 'embedding': emb.get('embedding'), + 'index': emb.get('index', i), + } + ) + return { + 'object': 'list', + 'data': openai_data, + 'model': response.get('model'), + } + # Ollama single output + elif isinstance(response, dict) and 'embedding' in response: + return { + 'object': 'list', + 'data': [ + { + 'object': 'embedding', + 'embedding': response['embedding'], + 'index': 0, + } + ], + 'model': response.get('model'), + } + # Already OpenAI-compatible? + elif isinstance(response, dict) and 'data' in response and isinstance(response['data'], list): + return response + + # Fallback: return as is if unrecognized + return response diff --git a/backend/open_webui/utils/sanitize.py b/backend/open_webui/utils/sanitize.py new file mode 100644 index 0000000000000000000000000000000000000000..7b65df375a3e71bab78cd9a73ab3cfcda063fc10 --- /dev/null +++ b/backend/open_webui/utils/sanitize.py @@ -0,0 +1,57 @@ +import re + +# ANSI escape code pattern - matches all common ANSI sequences +# This includes color codes, cursor movement, and other terminal control sequences +ANSI_ESCAPE_PATTERN = re.compile(r'\x1b\[[0-9;]*[A-Za-z]|\x1b\([AB]|\x1b[PX^_].*?\x1b\\|\x1b\].*?(?:\x07|\x1b\\)') + + +def strip_ansi_codes(text: str) -> str: + """ + Strip ANSI escape codes from text. + + ANSI escape codes can be introduced by LLMs that include terminal + color codes in their output. These codes cause syntax errors when + the code is sent to Jupyter for execution. + + Common ANSI codes include: + - Color codes: \x1b[31m (red), \x1b[32m (green), etc. + - Reset codes: \x1b[0m, \x1b[39m + - Cursor movement: \x1b[1A, \x1b[2J, etc. + """ + return ANSI_ESCAPE_PATTERN.sub('', text) + + +def strip_markdown_code_fences(code: str) -> str: + """ + Strip markdown code fences if present. + + This is a defensive, non-breaking change — if the code doesn't + contain fences, it passes through unchanged. + + Handles patterns like: + - ```python + - ```py + - ``` + """ + code = code.strip() + # Remove opening fence (```python, ```py, ``` etc.) + code = re.sub(r'^```\w*\n?', '', code) + # Remove closing fence + code = re.sub(r'\n?```\s*$', '', code) + return code.strip() + + +def sanitize_code(code: str) -> str: + """ + Sanitize code for execution by applying all necessary cleanup steps. + + This is the recommended function to use before sending code to + interpreters like Jupyter or Pyodide. + + Steps applied: + 1. Strip ANSI escape codes (from LLM output) + 2. Strip markdown code fences (if model included them) + """ + code = strip_ansi_codes(code) + code = strip_markdown_code_fences(code) + return code diff --git a/backend/open_webui/utils/security_headers.py b/backend/open_webui/utils/security_headers.py new file mode 100644 index 0000000000000000000000000000000000000000..ecc3b6eb30f8907219389e09a0a809c41408df28 --- /dev/null +++ b/backend/open_webui/utils/security_headers.py @@ -0,0 +1,180 @@ +import re +import os + +from fastapi import Request +from starlette.middleware.base import BaseHTTPMiddleware +from typing import Dict + + +class SecurityHeadersMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + response = await call_next(request) + response.headers.update(set_security_headers()) + return response + + +def set_security_headers() -> Dict[str, str]: + """ + Sets security headers based on environment variables. + + This function reads specific environment variables and uses their values + to set corresponding security headers. The headers that can be set are: + - cache-control + - permissions-policy + - strict-transport-security + - referrer-policy + - x-content-type-options + - x-download-options + - x-frame-options + - x-permitted-cross-domain-policies + - content-security-policy + - content-security-policy-report-only + - cross-origin-embedder-policy + - cross-origin-opener-policy + - cross-origin-resource-policy + - reporting-endpoints + + Each environment variable is associated with a specific setter function + that constructs the header. If the environment variable is set, the + corresponding header is added to the options dictionary. + + Returns: + dict: A dictionary containing the security headers and their values. + """ + options = {} + header_setters = { + 'CACHE_CONTROL': set_cache_control, + 'HSTS': set_hsts, + 'PERMISSIONS_POLICY': set_permissions_policy, + 'REFERRER_POLICY': set_referrer, + 'XCONTENT_TYPE': set_xcontent_type, + 'XDOWNLOAD_OPTIONS': set_xdownload_options, + 'XFRAME_OPTIONS': set_xframe, + 'XPERMITTED_CROSS_DOMAIN_POLICIES': set_xpermitted_cross_domain_policies, + 'CONTENT_SECURITY_POLICY': set_content_security_policy, + 'CONTENT_SECURITY_POLICY_REPORT_ONLY': set_content_security_policy_report_only, + 'CROSS_ORIGIN_EMBEDDER_POLICY': set_cross_origin_embedder_policy, + 'CROSS_ORIGIN_OPENER_POLICY': set_cross_origin_opener_policy, + 'CROSS_ORIGIN_RESOURCE_POLICY': set_cross_origin_resource_policy, + 'REPORTING_ENDPOINTS': set_reporting_endpoints, + } + + for env_var, setter in header_setters.items(): + value = os.environ.get(env_var, None) + if value: + header = setter(value) + if header: + options.update(header) + + return options + + +# Set HTTP Strict Transport Security(HSTS) response header +def set_hsts(value: str): + pattern = r'^max-age=(\d+)(;includeSubDomains)?(;preload)?$' + match = re.match(pattern, value, re.IGNORECASE) + if not match: + value = 'max-age=31536000;includeSubDomains' + return {'Strict-Transport-Security': value} + + +# Set X-Frame-Options response header +def set_xframe(value: str): + pattern = r'^(DENY|SAMEORIGIN)$' + match = re.match(pattern, value, re.IGNORECASE) + if not match: + value = 'DENY' + return {'X-Frame-Options': value} + + +# Set Permissions-Policy response header +def set_permissions_policy(value: str): + pattern = r'^(?:(accelerometer|autoplay|camera|clipboard-read|clipboard-write|fullscreen|geolocation|gyroscope|magnetometer|microphone|midi|payment|picture-in-picture|sync-xhr|usb|xr-spatial-tracking)=\((self)?\),?)*$' + match = re.match(pattern, value, re.IGNORECASE) + if not match: + value = 'none' + return {'Permissions-Policy': value} + + +# Set Referrer-Policy response header +def set_referrer(value: str): + pattern = r'^(no-referrer|no-referrer-when-downgrade|origin|origin-when-cross-origin|same-origin|strict-origin|strict-origin-when-cross-origin|unsafe-url)$' + match = re.match(pattern, value, re.IGNORECASE) + if not match: + value = 'no-referrer' + return {'Referrer-Policy': value} + + +# Set Cache-Control response header +def set_cache_control(value: str): + pattern = r'^(public|private|no-cache|no-store|must-revalidate|proxy-revalidate|max-age=\d+|s-maxage=\d+|no-transform|immutable)(,\s*(public|private|no-cache|no-store|must-revalidate|proxy-revalidate|max-age=\d+|s-maxage=\d+|no-transform|immutable))*$' + match = re.match(pattern, value, re.IGNORECASE) + if not match: + value = 'no-store, max-age=0' + + return {'Cache-Control': value} + + +# Set X-Download-Options response header +def set_xdownload_options(value: str): + if value != 'noopen': + value = 'noopen' + return {'X-Download-Options': value} + + +# Set X-Content-Type-Options response header +def set_xcontent_type(value: str): + if value != 'nosniff': + value = 'nosniff' + return {'X-Content-Type-Options': value} + + +# Set X-Permitted-Cross-Domain-Policies response header +def set_xpermitted_cross_domain_policies(value: str): + pattern = r'^(none|master-only|by-content-type|by-ftp-filename)$' + match = re.match(pattern, value, re.IGNORECASE) + if not match: + value = 'none' + return {'X-Permitted-Cross-Domain-Policies': value} + + +# Set Content-Security-Policy response header +def set_content_security_policy(value: str): + return {'Content-Security-Policy': value} + + +# Set Content-Security-Policy-Report-Only response header +def set_content_security_policy_report_only(value: str): + return {'Content-Security-Policy-Report-Only': value} + + +# Set Cross-Origin-Embedder-Policy response header +def set_cross_origin_embedder_policy(value: str): + pattern = r'^(unsafe-none|require-corp|credentialless)$' + match = re.match(pattern, value, re.IGNORECASE) + if not match: + value = 'require-corp' + return {'Cross-Origin-Embedder-Policy': value} + + +# Set Cross-Origin-Opener-Policy response header +def set_cross_origin_opener_policy(value: str): + pattern = r'^(unsafe-none|same-origin-allow-popups|same-origin)$' + match = re.match(pattern, value, re.IGNORECASE) + if not match: + value = 'same-origin' + return {'Cross-Origin-Opener-Policy': value} + + +# Set Cross-Origin-Resource-Policy response header +def set_cross_origin_resource_policy(value: str): + pattern = r'^(same-site|same-origin|cross-origin)$' + match = re.match(pattern, value, re.IGNORECASE) + if not match: + value = 'same-origin' + return {'Cross-Origin-Resource-Policy': value} + + +# Set Reporting-Endpoints response header +def set_reporting_endpoints(value: str): + return {'Reporting-Endpoints': value} diff --git a/backend/open_webui/utils/session_pool.py b/backend/open_webui/utils/session_pool.py new file mode 100644 index 0000000000000000000000000000000000000000..d74eae4f04ef80da05220786f1a55730e6d206b8 --- /dev/null +++ b/backend/open_webui/utils/session_pool.py @@ -0,0 +1,119 @@ +"""Shared aiohttp ClientSession pool. + +Instead of creating a new ClientSession (and TCPConnector) per request, +callers acquire a long-lived session from this module. The pool manages +a single TCPConnector with configurable limits, enabling TCP/SSL connection +reuse, shared DNS cache, and bounded concurrency. + +All pool parameters are configurable via environment variables: + - AIOHTTP_POOL_CONNECTIONS (default 100) — max total connections + - AIOHTTP_POOL_CONNECTIONS_PER_HOST (default 30) — per-host limit + - AIOHTTP_POOL_DNS_TTL (default 300) — DNS cache TTL in seconds + +Usage: + from open_webui.utils.session_pool import get_session, cleanup_response + + session = await get_session() + r = await session.request(...) + # When done with the *response* (not the session): + await cleanup_response(r) + +IMPORTANT: Callers must NOT close the shared session. Only the response +needs cleanup. The session is closed once during application shutdown +via ``close_session()``. +""" + +import logging +from typing import Optional + +import aiohttp + +from open_webui.env import ( + AIOHTTP_CLIENT_TIMEOUT, + AIOHTTP_POOL_CONNECTIONS, + AIOHTTP_POOL_CONNECTIONS_PER_HOST, + AIOHTTP_POOL_DNS_TTL, +) + +log = logging.getLogger(__name__) + +_session: Optional[aiohttp.ClientSession] = None + + +async def get_session() -> aiohttp.ClientSession: + """Return the shared aiohttp ClientSession, creating it lazily.""" + global _session + if _session is None or _session.closed: + connector_kwargs = { + 'ttl_dns_cache': AIOHTTP_POOL_DNS_TTL, + 'enable_cleanup_closed': True, + } + if AIOHTTP_POOL_CONNECTIONS is not None: + connector_kwargs['limit'] = AIOHTTP_POOL_CONNECTIONS + else: + connector_kwargs['limit'] = 0 # aiohttp: 0 = unlimited + if AIOHTTP_POOL_CONNECTIONS_PER_HOST is not None: + connector_kwargs['limit_per_host'] = AIOHTTP_POOL_CONNECTIONS_PER_HOST + else: + connector_kwargs['limit_per_host'] = 0 # aiohttp: 0 = unlimited + connector = aiohttp.TCPConnector(**connector_kwargs) + timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + _session = aiohttp.ClientSession( + connector=connector, + timeout=timeout, + trust_env=True, + ) + log.info( + 'Created shared aiohttp session pool (limit=%s, per_host=%s, dns_ttl=%d)', + AIOHTTP_POOL_CONNECTIONS or 'unlimited', + AIOHTTP_POOL_CONNECTIONS_PER_HOST or 'unlimited', + AIOHTTP_POOL_DNS_TTL, + ) + return _session + + +async def close_session(): + """Close the shared session. Called during application shutdown.""" + global _session + if _session and not _session.closed: + await _session.close() + log.info('Closed shared aiohttp session pool') + _session = None + + +async def cleanup_response( + response: Optional[aiohttp.ClientResponse], + session: Optional[aiohttp.ClientSession] = None, +): + """Release and close an aiohttp response, optionally closing the session. + + When using the shared pool, ``session`` should be ``None`` (the pool + session is never closed per-request). When a caller creates its own + one-off session, pass it here to close it after the response. + """ + if response: + if not response.closed: + # aiohttp 3.9+ made ClientResponse.close() synchronous (returns None). + # Older versions returned a coroutine. Handle both gracefully. + result = response.close() + if result is not None: + await result + if session: + if not session.closed: + result = session.close() + if result is not None: + await result + + +async def stream_wrapper(response, session=None, content_handler=None): + """Wrap a stream to ensure cleanup happens even if streaming is interrupted. + + This is more reliable than BackgroundTask which may not run if the client + disconnects. When using the shared pool, ``session`` should be ``None``. + """ + try: + stream = content_handler(response.content) if content_handler else response.content + async for chunk in stream: + yield chunk + finally: + await cleanup_response(response, session) diff --git a/backend/open_webui/utils/task.py b/backend/open_webui/utils/task.py new file mode 100644 index 0000000000000000000000000000000000000000..15213b1f03f1148b6493eac2e195d4301f2fa4e3 --- /dev/null +++ b/backend/open_webui/utils/task.py @@ -0,0 +1,390 @@ +import logging +import math +import re +from datetime import datetime +from typing import Optional, Any +import uuid + + +from open_webui.utils.misc import get_last_user_message, get_messages_content + +from open_webui.config import DEFAULT_RAG_TEMPLATE + +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 + if models.get(task_model_id, {}).get('connection_type') == 'local': + if task_model and task_model in models: + task_model_id = task_model + else: + if task_model_external and task_model_external in models: + task_model_id = task_model_external + + return task_model_id + + +def prompt_variables_template(template: str, variables: dict[str, str]) -> str: + for variable, value in variables.items(): + template = template.replace(variable, value) + return template + + +def prompt_template(template: str, user: Optional[Any] = None) -> str: + USER_VARIABLES = {} + + if user: + if hasattr(user, 'model_dump'): + user = user.model_dump() + + if isinstance(user, dict): + user_info = user.get('info', {}) or {} + birth_date = user.get('date_of_birth') + age = None + + if birth_date: + try: + # If birth_date is str, convert to datetime + if isinstance(birth_date, str): + birth_date = datetime.strptime(birth_date, '%Y-%m-%d') + + today = datetime.now() + age = today.year - birth_date.year - ((today.month, today.day) < (birth_date.month, birth_date.day)) + except Exception as e: + pass + + USER_VARIABLES = { + 'name': str(user.get('name')), + 'email': str(user.get('email')), + 'location': str(user_info.get('location')), + 'bio': str(user.get('bio')), + 'gender': str(user.get('gender')), + 'birth_date': str(birth_date), + 'age': str(age), + } + + # Get the current date + current_date = datetime.now() + + # Format the date to YYYY-MM-DD + formatted_date = current_date.strftime('%Y-%m-%d') + formatted_time = current_date.strftime('%I:%M:%S %p') + formatted_weekday = current_date.strftime('%A') + + template = template.replace('{{CURRENT_DATE}}', formatted_date) + template = template.replace('{{CURRENT_TIME}}', formatted_time) + template = template.replace('{{CURRENT_DATETIME}}', f'{formatted_date} {formatted_time}') + template = template.replace('{{CURRENT_WEEKDAY}}', formatted_weekday) + + template = template.replace('{{USER_NAME}}', USER_VARIABLES.get('name', 'Unknown')) + template = template.replace('{{USER_EMAIL}}', USER_VARIABLES.get('email', 'Unknown')) + template = template.replace('{{USER_BIO}}', USER_VARIABLES.get('bio', 'Unknown')) + template = template.replace('{{USER_GENDER}}', USER_VARIABLES.get('gender', 'Unknown')) + template = template.replace('{{USER_BIRTH_DATE}}', USER_VARIABLES.get('birth_date', 'Unknown')) + template = template.replace('{{USER_AGE}}', str(USER_VARIABLES.get('age', 'Unknown'))) + template = template.replace('{{USER_LOCATION}}', USER_VARIABLES.get('location', 'Unknown')) + + return template + + +def replace_prompt_variable(template: str, prompt: str) -> str: + def replacement_function(match): + full_match = match.group(0).lower() # Normalize to lowercase for consistent handling + start_length = match.group(1) + end_length = match.group(2) + middle_length = match.group(3) + + if full_match == '{{prompt}}': + return prompt + elif start_length is not None: + return prompt[: int(start_length)] + elif end_length is not None: + return prompt[-int(end_length) :] + elif middle_length is not None: + middle_length = int(middle_length) + if len(prompt) <= middle_length: + return prompt + start = prompt[: math.ceil(middle_length / 2)] + end = prompt[-math.floor(middle_length / 2) :] + return f'{start}...{end}' + return '' + + # Updated regex pattern to make it case-insensitive with the `(?i)` flag + pattern = r'(?i){{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}' + template = re.sub(pattern, replacement_function, template) + return template + + +def truncate_content(content: str, max_chars: int, mode: str = 'middletruncate') -> str: + """Truncate a string to max_chars using the specified mode. + + Modes: + - middletruncate: keep beginning and end, join with '...' + - start: keep first max_chars characters + - end: keep last max_chars characters + """ + if not content or len(content) <= max_chars: + return content + + if mode == 'start': + return content[:max_chars] + elif mode == 'end': + return content[-max_chars:] + else: # middletruncate + half = max_chars // 2 + return f'{content[:half]}...{content[-(max_chars - half) :]}' + + +def apply_content_filter(messages: list[dict], filter_str: str) -> list[dict]: + """Apply a content filter to each message's content. + + filter_str is like 'middletruncate:500', 'start:200', or 'end:200'. + Returns a new list with truncated content (original messages are not mutated). + """ + parts = filter_str.split(':') + if len(parts) != 2: + return messages + + mode = parts[0].lower() + try: + max_chars = int(parts[1]) + except ValueError: + return messages + + if mode not in ('middletruncate', 'start', 'end'): + return messages + + result = [] + for msg in messages: + new_msg = dict(msg) + if isinstance(new_msg.get('content'), str): + new_msg['content'] = truncate_content(new_msg['content'], max_chars, mode) + elif isinstance(new_msg.get('content'), list): + new_content = [] + for item in new_msg['content']: + if isinstance(item, dict) and item.get('type') == 'text': + new_item = dict(item) + new_item['text'] = truncate_content(item.get('text', ''), max_chars, mode) + new_content.append(new_item) + else: + new_content.append(item) + new_msg['content'] = new_content + result.append(new_msg) + return result + + +def replace_messages_variable(template: str, messages: Optional[list[dict]] = None) -> str: + def replacement_function(match): + # Groups: (1) filter for bare MESSAGES + # (2) START count, (3) filter for START + # (4) END count, (5) filter for END + # (6) MIDDLE count,(7) filter for MIDDLE + bare_filter = match.group(1) + start_length = match.group(2) + start_filter = match.group(3) + end_length = match.group(4) + end_filter = match.group(5) + middle_length = match.group(6) + middle_filter = match.group(7) + + # If messages is None, handle it as an empty list + if messages is None: + return '' + + # Select messages based on the variant + if start_length is not None: + selected = messages[: int(start_length)] + content_filter = start_filter + elif end_length is not None: + selected = messages[-int(end_length) :] + content_filter = end_filter + elif middle_length is not None: + mid = int(middle_length) + if len(messages) <= mid: + selected = messages + else: + half = mid // 2 + start_msgs = messages[:half] + end_msgs = messages[-half:] if mid % 2 == 0 else messages[-(half + 1) :] + selected = start_msgs + end_msgs + content_filter = middle_filter + else: + # Bare {{MESSAGES}} or {{MESSAGES|filter}} + selected = messages + content_filter = bare_filter + + # Apply content filter if present + if content_filter: + selected = apply_content_filter(selected, content_filter) + + return get_messages_content(selected) + + template = re.sub( + r'(?:' + r'\{\{MESSAGES(?:\|(\w+:\d+))?\}\}' + r'|\{\{MESSAGES:START:(\d+)(?:\|(\w+:\d+))?\}\}' + r'|\{\{MESSAGES:END:(\d+)(?:\|(\w+:\d+))?\}\}' + r'|\{\{MESSAGES:MIDDLETRUNCATE:(\d+)(?:\|(\w+:\d+))?\}\}' + r')', + replacement_function, + template, + ) + + return template + + +# {{prompt:middletruncate:8000}} + + +# Let the context given here not distort the question, +# but illuminate it, so that the answer serves the one who asked. +def rag_template(template: str, context: str, query: str): + if template.strip() == '': + template = DEFAULT_RAG_TEMPLATE + + template = prompt_template(template) + + if '[context]' not in template and '{{CONTEXT}}' not in template: + log.debug("WARNING: The RAG template does not contain the '[context]' or '{{CONTEXT}}' placeholder.") + + if '' in context and '' in context: + log.debug( + 'WARNING: Potential prompt injection attack: the RAG ' + "context contains '' and ''. This might be " + 'nothing, or the user might be trying to hack something.' + ) + + query_placeholders = [] + if '[query]' in context: + query_placeholder = '{{QUERY' + str(uuid.uuid4()) + '}}' + template = template.replace('[query]', query_placeholder) + query_placeholders.append((query_placeholder, '[query]')) + + if '{{QUERY}}' in context: + query_placeholder = '{{QUERY' + str(uuid.uuid4()) + '}}' + template = template.replace('{{QUERY}}', query_placeholder) + query_placeholders.append((query_placeholder, '{{QUERY}}')) + + template = template.replace('[context]', context) + template = template.replace('{{CONTEXT}}', context) + + template = template.replace('[query]', query) + template = template.replace('{{QUERY}}', query) + + for query_placeholder, original_placeholder in query_placeholders: + template = template.replace(query_placeholder, original_placeholder) + + return template + + +def title_generation_template(template: str, messages: list[dict], user: Optional[Any] = None) -> str: + prompt = get_last_user_message(messages) + template = replace_prompt_variable(template, prompt) + template = replace_messages_variable(template, messages) + + template = prompt_template(template, user) + + return template + + +def follow_up_generation_template(template: str, messages: list[dict], user: Optional[Any] = None) -> str: + prompt = get_last_user_message(messages) + template = replace_prompt_variable(template, prompt) + template = replace_messages_variable(template, messages) + + template = prompt_template(template, user) + return template + + +def tags_generation_template(template: str, messages: list[dict], user: Optional[Any] = None) -> str: + prompt = get_last_user_message(messages) + template = replace_prompt_variable(template, prompt) + template = replace_messages_variable(template, messages) + + template = prompt_template(template, user) + return template + + +def image_prompt_generation_template(template: str, messages: list[dict], user: Optional[Any] = None) -> str: + prompt = get_last_user_message(messages) + template = replace_prompt_variable(template, prompt) + template = replace_messages_variable(template, messages) + + template = prompt_template(template, user) + return template + + +def emoji_generation_template(template: str, prompt: str, user: Optional[Any] = None) -> str: + template = replace_prompt_variable(template, prompt) + template = prompt_template(template, user) + + return template + + +def autocomplete_generation_template( + template: str, + prompt: str, + messages: Optional[list[dict]] = None, + type: Optional[str] = None, + user: Optional[Any] = None, +) -> str: + template = template.replace('{{TYPE}}', type if type else '') + template = replace_prompt_variable(template, prompt) + template = replace_messages_variable(template, messages) + + template = prompt_template(template, user) + return template + + +def query_generation_template(template: str, messages: list[dict], user: Optional[Any] = None) -> str: + prompt = get_last_user_message(messages) + template = replace_prompt_variable(template, prompt) + template = replace_messages_variable(template, messages) + + template = prompt_template(template, user) + return template + + +def moa_response_generation_template(template: str, prompt: str, responses: list[str]) -> str: + def replacement_function(match): + full_match = match.group(0) + start_length = match.group(1) + end_length = match.group(2) + middle_length = match.group(3) + + if full_match == '{{prompt}}': + return prompt + elif start_length is not None: + return prompt[: int(start_length)] + elif end_length is not None: + return prompt[-int(end_length) :] + elif middle_length is not None: + middle_length = int(middle_length) + if len(prompt) <= middle_length: + return prompt + start = prompt[: math.ceil(middle_length / 2)] + end = prompt[-math.floor(middle_length / 2) :] + return f'{start}...{end}' + return '' + + template = re.sub( + r'{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}', + replacement_function, + template, + ) + + responses = [f'"""{response}"""' for response in responses] + responses = '\n\n'.join(responses) + + template = template.replace('{{responses}}', responses) + return template + + +def tools_function_calling_generation_template(template: str, tools_specs: str) -> str: + template = template.replace('{{TOOLS}}', tools_specs) + return template diff --git a/backend/open_webui/utils/telemetry/__init__.py b/backend/open_webui/utils/telemetry/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/open_webui/utils/telemetry/constants.py b/backend/open_webui/utils/telemetry/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..1f2102a86faec47bc6c9a31c1af05b4c8696a974 --- /dev/null +++ b/backend/open_webui/utils/telemetry/constants.py @@ -0,0 +1,26 @@ +from opentelemetry.semconv.trace import SpanAttributes as _SpanAttributes + +# Span Tags +SPAN_DB_TYPE = 'mysql' +SPAN_REDIS_TYPE = 'redis' +SPAN_DURATION = 'duration' +SPAN_SQL_STR = 'sql' +SPAN_SQL_EXPLAIN = 'explain' +SPAN_ERROR_TYPE = 'error' + + +class SpanAttributes(_SpanAttributes): + """ + Span Attributes + """ + + DB_INSTANCE = 'db.instance' + DB_TYPE = 'db.type' + DB_IP = 'db.ip' + DB_PORT = 'db.port' + ERROR_KIND = 'error.kind' + ERROR_OBJECT = 'error.object' + ERROR_MESSAGE = 'error.message' + RESULT_CODE = 'result.code' + RESULT_MESSAGE = 'result.message' + RESULT_ERRORS = 'result.errors' diff --git a/backend/open_webui/utils/telemetry/instrumentors.py b/backend/open_webui/utils/telemetry/instrumentors.py new file mode 100644 index 0000000000000000000000000000000000000000..fe8e9ba799b9b0323684366c3981a9d70ebe9dd5 --- /dev/null +++ b/backend/open_webui/utils/telemetry/instrumentors.py @@ -0,0 +1,200 @@ +import logging +import traceback +from typing import Collection, Union + +from aiohttp import ( + TraceRequestStartParams, + TraceRequestEndParams, + TraceRequestExceptionParams, +) +from fastapi import FastAPI +from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor +from opentelemetry.instrumentation.httpx import ( + HTTPXClientInstrumentor, + RequestInfo, + ResponseInfo, +) +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.logging import LoggingInstrumentor +from opentelemetry.instrumentation.redis import RedisInstrumentor +from opentelemetry.instrumentation.requests import RequestsInstrumentor +from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor +from opentelemetry.instrumentation.aiohttp_client import AioHttpClientInstrumentor +from opentelemetry.instrumentation.system_metrics import SystemMetricsInstrumentor +from opentelemetry.trace import Span, StatusCode +from redis import Redis +from redis.cluster import RedisCluster +from requests import PreparedRequest, Response +from sqlalchemy import Engine +from fastapi import status + +from open_webui.utils.telemetry.constants import SPAN_REDIS_TYPE, SpanAttributes + +logger = logging.getLogger(__name__) + + +def requests_hook(span: Span, request: PreparedRequest): + """ + Http Request Hook + """ + + span.update_name(f'{request.method} {request.url}') + span.set_attributes( + attributes={ + SpanAttributes.HTTP_URL: request.url, + SpanAttributes.HTTP_METHOD: request.method, + } + ) + + +def response_hook(span: Span, request: PreparedRequest, response: Response): + """ + HTTP Response Hook + """ + + span.set_attributes( + attributes={ + SpanAttributes.HTTP_STATUS_CODE: response.status_code, + } + ) + span.set_status(StatusCode.ERROR if response.status_code >= 400 else StatusCode.OK) + + +def redis_request_hook(span: Span, instance: Union[Redis | RedisCluster], args, kwargs): + """ + 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. + try: + db = '' + if hasattr(instance, 'nodes_manager'): + default_node = instance.nodes_manager.default_node + if not default_node: + return + host = default_node.host + port = default_node.port + else: + connection_kwargs: dict = instance.connection_pool.connection_kwargs + host = connection_kwargs.get('host') + port = connection_kwargs.get('port') + db = connection_kwargs.get('db') + span.set_attributes( + { + SpanAttributes.DB_INSTANCE: f'{host}/{db}', + SpanAttributes.DB_NAME: f'{host}/{db}', + SpanAttributes.DB_TYPE: SPAN_REDIS_TYPE, + SpanAttributes.DB_PORT: port, + SpanAttributes.DB_IP: host, + SpanAttributes.DB_STATEMENT: ' '.join([str(i) for i in args]), + SpanAttributes.DB_OPERATION: str(args[0]), + } + ) + except Exception: # pylint: disable=W0718 + logger.error(traceback.format_exc()) + + +def httpx_request_hook(span: Span, request: RequestInfo): + """ + HTTPX Request Hook + """ + + span.update_name(f'{request.method.decode()} {str(request.url)}') + span.set_attributes( + attributes={ + SpanAttributes.HTTP_URL: str(request.url), + SpanAttributes.HTTP_METHOD: request.method.decode(), + } + ) + + +def httpx_response_hook(span: Span, request: RequestInfo, response: ResponseInfo): + """ + HTTPX Response Hook + """ + + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status_code) + span.set_status(StatusCode.ERROR if response.status_code >= status.HTTP_400_BAD_REQUEST else StatusCode.OK) + + +async def httpx_async_request_hook(span: Span, request: RequestInfo): + """ + Async Request Hook + """ + + httpx_request_hook(span, request) + + +async def httpx_async_response_hook(span: Span, request: RequestInfo, response: ResponseInfo): + """ + Async Response Hook + """ + + httpx_response_hook(span, request, response) + + +def aiohttp_request_hook(span: Span, request: TraceRequestStartParams): + """ + Aiohttp Request Hook + """ + + span.update_name(f'{request.method} {str(request.url)}') + span.set_attributes( + attributes={ + SpanAttributes.HTTP_URL: str(request.url), + SpanAttributes.HTTP_METHOD: request.method, + } + ) + + +def aiohttp_response_hook(span: Span, response: Union[TraceRequestExceptionParams, TraceRequestEndParams]): + """ + Aiohttp Response Hook + """ + + if isinstance(response, TraceRequestEndParams): + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.response.status) + span.set_status(StatusCode.ERROR if response.response.status >= status.HTTP_400_BAD_REQUEST else StatusCode.OK) + elif isinstance(response, TraceRequestExceptionParams): + span.set_status(StatusCode.ERROR) + span.set_attribute(SpanAttributes.ERROR_MESSAGE, str(response.exception)) + + +class Instrumentor(BaseInstrumentor): + """ + Instrument OT + """ + + def __init__(self, app: FastAPI, db_engine: Engine): + self.app = app + self.db_engine = db_engine + + def instrumentation_dependencies(self) -> Collection[str]: + return [] + + def _instrument(self, **kwargs): + FastAPIInstrumentor.instrument_app(app=self.app) + SQLAlchemyInstrumentor().instrument(engine=self.db_engine) + RedisInstrumentor().instrument(request_hook=redis_request_hook) + RequestsInstrumentor().instrument(request_hook=requests_hook, response_hook=response_hook) + LoggingInstrumentor().instrument() + HTTPXClientInstrumentor().instrument( + request_hook=httpx_request_hook, + response_hook=httpx_response_hook, + async_request_hook=httpx_async_request_hook, + async_response_hook=httpx_async_response_hook, + ) + AioHttpClientInstrumentor().instrument( + request_hook=aiohttp_request_hook, + response_hook=aiohttp_response_hook, + ) + SystemMetricsInstrumentor().instrument() + + def _uninstrument(self, **kwargs): + if getattr(self, 'instrumentors', None) is None: + return + for instrumentor in self.instrumentors: + instrumentor.uninstrument() diff --git a/backend/open_webui/utils/telemetry/logs.py b/backend/open_webui/utils/telemetry/logs.py new file mode 100644 index 0000000000000000000000000000000000000000..e501c99ceacfe44dc3f9c0f42433cefea5cc6c07 --- /dev/null +++ b/backend/open_webui/utils/telemetry/logs.py @@ -0,0 +1,53 @@ +import logging +from base64 import b64encode +from opentelemetry.sdk._logs import ( + LoggingHandler, + LoggerProvider, +) +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter +from opentelemetry.exporter.otlp.proto.http._log_exporter import ( + OTLPLogExporter as HttpOTLPLogExporter, +) +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor +from opentelemetry._logs import set_logger_provider +from opentelemetry.sdk.resources import SERVICE_NAME, Resource +from open_webui.env import ( + OTEL_SERVICE_NAME, + OTEL_LOGS_EXPORTER_OTLP_ENDPOINT, + OTEL_LOGS_EXPORTER_OTLP_INSECURE, + OTEL_LOGS_BASIC_AUTH_USERNAME, + OTEL_LOGS_BASIC_AUTH_PASSWORD, + OTEL_LOGS_OTLP_SPAN_EXPORTER, +) + + +def setup_logging(): + headers = [] + if OTEL_LOGS_BASIC_AUTH_USERNAME and OTEL_LOGS_BASIC_AUTH_PASSWORD: + auth_string = f'{OTEL_LOGS_BASIC_AUTH_USERNAME}:{OTEL_LOGS_BASIC_AUTH_PASSWORD}' + auth_header = b64encode(auth_string.encode()).decode() + headers = [('authorization', f'Basic {auth_header}')] + resource = Resource.create(attributes={SERVICE_NAME: OTEL_SERVICE_NAME}) + + if OTEL_LOGS_OTLP_SPAN_EXPORTER == 'http': + exporter = HttpOTLPLogExporter( + endpoint=OTEL_LOGS_EXPORTER_OTLP_ENDPOINT, + headers=headers, + ) + else: + exporter = OTLPLogExporter( + endpoint=OTEL_LOGS_EXPORTER_OTLP_ENDPOINT, + insecure=OTEL_LOGS_EXPORTER_OTLP_INSECURE, + headers=headers, + ) + logger_provider = LoggerProvider(resource=resource) + set_logger_provider(logger_provider) + + logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter)) + + otel_handler = LoggingHandler(logger_provider=logger_provider) + + return otel_handler + + +otel_handler = setup_logging() diff --git a/backend/open_webui/utils/telemetry/metrics.py b/backend/open_webui/utils/telemetry/metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..a1d1dcb7cbbbac41578d3e3921f7ab088c19553f --- /dev/null +++ b/backend/open_webui/utils/telemetry/metrics.py @@ -0,0 +1,254 @@ +"""OpenTelemetry metrics bootstrap for Open WebUI. + +This module initialises a MeterProvider that sends metrics to an OTLP +collector. The collector is responsible for exposing a Prometheus +`/metrics` endpoint – WebUI does **not** expose it directly. + +Metrics collected: + +* http.server.requests (counter) +* http.server.duration (histogram, milliseconds) + +Attributes used: http.method, http.route, http.status_code + +If you wish to add more attributes (e.g. user-agent) you can, but beware of +high-cardinality label sets. +""" + +from __future__ import annotations + +import datetime +import logging +import time +from typing import Dict, Iterable, List, Optional +from base64 import b64encode + +from fastapi import FastAPI, Request +from opentelemetry import metrics +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( + OTLPMetricExporter, +) + +from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( + OTLPMetricExporter as OTLPHttpMetricExporter, +) +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.view import View +from opentelemetry.sdk.metrics.export import ( + PeriodicExportingMetricReader, +) +from opentelemetry.sdk.resources import Resource +from sqlalchemy import Engine, func, select +from sqlalchemy.orm import Session + +from open_webui.env import ( + OTEL_SERVICE_NAME, + OTEL_METRICS_EXPORTER_OTLP_ENDPOINT, + OTEL_METRICS_BASIC_AUTH_USERNAME, + OTEL_METRICS_BASIC_AUTH_PASSWORD, + OTEL_METRICS_OTLP_SPAN_EXPORTER, + OTEL_METRICS_EXPORTER_OTLP_INSECURE, + OTEL_METRICS_EXPORT_INTERVAL_MILLIS, +) +from open_webui.models.users import User + +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. +# --------------------------------------------------------------------------- + + +def _count_total_users(db_engine: Engine) -> Optional[int]: + """Return the total number of registered users (sync).""" + with Session(db_engine) as session: + return session.execute(select(func.count()).select_from(User)).scalar() + + +def _count_active_users(db_engine: Engine) -> Optional[int]: + """Return the number of users active within the last 3 minutes (sync).""" + three_minutes_ago = int(time.time()) - 180 + with Session(db_engine) as session: + return session.execute( + select(func.count()).select_from(User).filter(User.last_active_at >= three_minutes_ago) + ).scalar() + + +def _count_users_active_today(db_engine: Engine) -> Optional[int]: + """Return the number of users active since midnight today (sync).""" + now = int(datetime.datetime.now().timestamp()) + today_midnight = now - (now % 86400) + with Session(db_engine) as session: + return session.execute( + select(func.count()).select_from(User).filter(User.last_active_at > today_midnight) + ).scalar() + + +def _build_meter_provider(resource: Resource) -> MeterProvider: + """Return a configured MeterProvider.""" + headers = [] + if OTEL_METRICS_BASIC_AUTH_USERNAME and OTEL_METRICS_BASIC_AUTH_PASSWORD: + auth_string = f'{OTEL_METRICS_BASIC_AUTH_USERNAME}:{OTEL_METRICS_BASIC_AUTH_PASSWORD}' + auth_header = b64encode(auth_string.encode()).decode() + headers = [('authorization', f'Basic {auth_header}')] + + # Periodic reader pushes metrics over OTLP/gRPC to collector + if OTEL_METRICS_OTLP_SPAN_EXPORTER == 'http': + readers: List[PeriodicExportingMetricReader] = [ + PeriodicExportingMetricReader( + OTLPHttpMetricExporter(endpoint=OTEL_METRICS_EXPORTER_OTLP_ENDPOINT, headers=headers), + export_interval_millis=OTEL_METRICS_EXPORT_INTERVAL_MILLIS, + ) + ] + else: + readers: List[PeriodicExportingMetricReader] = [ + PeriodicExportingMetricReader( + OTLPMetricExporter( + endpoint=OTEL_METRICS_EXPORTER_OTLP_ENDPOINT, + insecure=OTEL_METRICS_EXPORTER_OTLP_INSECURE, + headers=headers, + ), + export_interval_millis=OTEL_METRICS_EXPORT_INTERVAL_MILLIS, + ) + ] + + # Optional view to limit cardinality: drop user-agent etc. + views: List[View] = [ + View( + instrument_name='http.server.duration', + attribute_keys=['http.method', 'http.route', 'http.status_code'], + ), + View( + instrument_name='http.server.requests', + attribute_keys=['http.method', 'http.route', 'http.status_code'], + ), + View( + instrument_name='webui.users.total', + ), + View( + instrument_name='webui.users.active', + ), + View( + instrument_name='webui.users.active.today', + ), + ] + + provider = MeterProvider( + resource=resource, + metric_readers=list(readers), + views=views, + ) + return provider + + +def setup_metrics(app: FastAPI, resource: Resource, db_engine: Engine) -> None: + """Attach OTel metrics middleware to *app* and initialise provider.""" + + metrics.set_meter_provider(_build_meter_provider(resource)) + meter = metrics.get_meter(__name__) + + # Instruments + request_counter = meter.create_counter( + name='http.server.requests', + description='Counts the total number of inbound HTTP requests.', + unit='1', + ) + duration_histogram = meter.create_histogram( + name='http.server.duration', + description='Measures the duration of inbound HTTP requests.', + 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. + + def observe_total_users( + options: metrics.CallbackOptions, + ) -> Iterable[metrics.Observation]: + try: + value = _count_total_users(db_engine) + if value is not None: + yield metrics.Observation(value=value) + except Exception: + logger.debug('Failed to observe total users', exc_info=True) + + def observe_active_users( + options: metrics.CallbackOptions, + ) -> Iterable[metrics.Observation]: + try: + value = _count_active_users(db_engine) + if value is not None: + yield metrics.Observation(value=value) + except Exception: + logger.debug('Failed to observe active users', exc_info=True) + + def observe_users_active_today( + options: metrics.CallbackOptions, + ) -> Iterable[metrics.Observation]: + try: + value = _count_users_active_today(db_engine) + if value is not None: + yield metrics.Observation(value=value) + except Exception: + logger.debug('Failed to observe users active today', exc_info=True) + + meter.create_observable_gauge( + name='webui.users.total', + description='Total number of registered users', + unit='users', + callbacks=[observe_total_users], + ) + + meter.create_observable_gauge( + name='webui.users.active', + description='Number of currently active users', + unit='users', + callbacks=[observe_active_users], + ) + + meter.create_observable_gauge( + name='webui.users.active.today', + description='Number of users active since midnight today', + unit='users', + callbacks=[observe_users_active_today], + ) + + # FastAPI middleware + @app.middleware('http') + async def _metrics_middleware(request: Request, call_next): + start_time = time.perf_counter() + + status_code = None + try: + response = await call_next(request) + status_code = getattr(response, 'status_code', 500) + return response + except Exception: + status_code = 500 + raise + finally: + elapsed_ms = (time.perf_counter() - start_time) * 1000.0 + + # Route template e.g. "/items/{item_id}" instead of real path. + route = request.scope.get('route') + route_path = getattr(route, 'path', request.url.path) + + attrs: Dict[str, str | int] = { + 'http.method': request.method, + 'http.route': route_path, + 'http.status_code': status_code, + } + + request_counter.add(1, attrs) + duration_histogram.record(elapsed_ms, attrs) diff --git a/backend/open_webui/utils/telemetry/setup.py b/backend/open_webui/utils/telemetry/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..14f10ef97f0cbf438d8e6cb6db42ff5e0186df99 --- /dev/null +++ b/backend/open_webui/utils/telemetry/setup.py @@ -0,0 +1,58 @@ +from fastapi import FastAPI +from opentelemetry import trace + +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( + OTLPSpanExporter as HttpOTLPSpanExporter, +) +from opentelemetry.sdk.resources import SERVICE_NAME, Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from sqlalchemy import Engine +from base64 import b64encode + +from open_webui.utils.telemetry.instrumentors import Instrumentor +from open_webui.utils.telemetry.metrics import setup_metrics +from open_webui.env import ( + OTEL_SERVICE_NAME, + OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_INSECURE, + ENABLE_OTEL_TRACES, + ENABLE_OTEL_METRICS, + OTEL_BASIC_AUTH_USERNAME, + OTEL_BASIC_AUTH_PASSWORD, + OTEL_OTLP_SPAN_EXPORTER, +) + + +def setup(app: FastAPI, db_engine: Engine): + # set up trace + resource = Resource.create(attributes={SERVICE_NAME: OTEL_SERVICE_NAME}) + if ENABLE_OTEL_TRACES: + trace.set_tracer_provider(TracerProvider(resource=resource)) + + # Add basic auth header only if both username and password are not empty + headers = [] + if OTEL_BASIC_AUTH_USERNAME and OTEL_BASIC_AUTH_PASSWORD: + auth_string = f'{OTEL_BASIC_AUTH_USERNAME}:{OTEL_BASIC_AUTH_PASSWORD}' + auth_header = b64encode(auth_string.encode()).decode() + headers = [('authorization', f'Basic {auth_header}')] + + # otlp export + if OTEL_OTLP_SPAN_EXPORTER == 'http': + exporter = HttpOTLPSpanExporter( + endpoint=OTEL_EXPORTER_OTLP_ENDPOINT, + headers=headers, + ) + else: + exporter = OTLPSpanExporter( + endpoint=OTEL_EXPORTER_OTLP_ENDPOINT, + insecure=OTEL_EXPORTER_OTLP_INSECURE, + headers=headers, + ) + trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(exporter)) + Instrumentor(app=app, db_engine=db_engine).instrument() + + # set up metrics only if enabled + if ENABLE_OTEL_METRICS: + setup_metrics(app, resource, db_engine) diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..9f3ab0bce4e55a8126c13931536dff9cfc693d8d --- /dev/null +++ b/backend/open_webui/utils/tools.py @@ -0,0 +1,1430 @@ +import base64 +import inspect +import logging +import re +import inspect +import aiohttp +import asyncio +import yaml +import json + +from pydantic import BaseModel +from pydantic.fields import FieldInfo +from typing import ( + Any, + Awaitable, + Callable, + get_type_hints, + get_args, + get_origin, + Dict, + List, + Tuple, + Union, + Optional, + Type, +) +from functools import update_wrapper, partial + + +from fastapi import Request +from pydantic import BaseModel, Field, create_model + +from langchain_core.utils.function_calling import ( + convert_to_openai_function as convert_pydantic_model_to_openai_function_spec, +) + + +from open_webui.utils.misc import is_string_allowed +from open_webui.models.tools import Tools +from open_webui.models.users import UserModel +from open_webui.models.groups import Groups +from open_webui.models.access_grants import AccessGrants +from open_webui.utils.plugin import load_tool_module_by_id +from open_webui.utils.access_control import has_access, has_connection_access +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL +from open_webui.env import ( + AIOHTTP_CLIENT_SESSION_SSL, + AIOHTTP_CLIENT_TIMEOUT, + AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER, + AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA, + AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, + ENABLE_FORWARD_USER_INFO_HEADERS, + FORWARD_SESSION_INFO_HEADER_CHAT_ID, + FORWARD_SESSION_INFO_HEADER_MESSAGE_ID, + REDIS_KEY_PREFIX, +) +from open_webui.utils.headers import include_user_info_headers +from open_webui.tools.builtin import ( + search_web, + fetch_url, + generate_image, + edit_image, + execute_code, + search_memories, + add_memory, + replace_memory_content, + delete_memory, + list_memories, + get_current_timestamp, + calculate_timestamp, + search_notes, + search_chats, + search_channels, + search_channel_messages, + view_note, + view_chat, + view_channel_message, + view_channel_thread, + replace_note_content, + write_note, + list_knowledge_bases, + search_knowledge_bases, + query_knowledge_bases, + search_knowledge_files, + query_knowledge_files, + list_knowledge, + view_file, + view_knowledge_file, + view_skill, + create_tasks, + update_task, + create_automation, + update_automation, + list_automations, + toggle_automation, + delete_automation, + search_calendar_events, + create_calendar_event, + update_calendar_event, + delete_calendar_event, +) + +import copy +from open_webui.utils.access_control import has_permission + +log = logging.getLogger(__name__) + + +# Let no function be called without need, and let what +# it yields justify the cost of running it. +async def get_async_tool_function_and_apply_extra_params( + function: Callable, extra_params: dict +) -> Callable[..., Awaitable]: + sig = inspect.signature(function) + extra_params = {k: v for k, v in extra_params.items() if k in sig.parameters} + partial_func = partial(function, **extra_params) + + # Remove the 'frozen' keyword arguments from the signature + # python-genai uses the signature to infer the tool properties for native function calling + parameters = [] + for name, parameter in sig.parameters.items(): + # Exclude keyword arguments that are frozen + if name in extra_params: + continue + # Keep remaining parameters + parameters.append(parameter) + + new_sig = inspect.Signature(parameters=parameters, return_annotation=sig.return_annotation) + + if inspect.iscoroutinefunction(function): + # wrap the functools.partial as python-genai has trouble with it + # https://github.com/googleapis/python-genai/issues/907 + async def new_function(*args, **kwargs): + return await partial_func(*args, **kwargs) + + else: + # Make it a coroutine function when it is not already + async def new_function(*args, **kwargs): + return partial_func(*args, **kwargs) + + update_wrapper(new_function, function) + new_function.__signature__ = new_sig + + new_function.__function__ = function # type: ignore + new_function.__extra_params__ = extra_params # type: ignore + + return new_function + + +async def get_updated_tool_function(function: Callable, extra_params: dict): + # Get the original function and merge updated params + __function__ = getattr(function, '__function__', None) + __extra_params__ = getattr(function, '__extra_params__', None) + + if __function__ is not None and __extra_params__ is not None: + return await get_async_tool_function_and_apply_extra_params( + __function__, + {**__extra_params__, **extra_params}, + ) + + return function + + +async def get_tools(request: Request, tool_ids: list[str], user: UserModel, extra_params: dict) -> dict[str, dict]: + """Load tools for the given tool_ids, checking access control.""" + if not tool_ids: + return {} + + tools_dict = {} + + # Get user's group memberships for access control checks + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id)} + + for tool_id in tool_ids: + tool = await Tools.get_tool_by_id(tool_id) + if tool: + # Check access control for local tools + if ( + not (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + and tool.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tool.id, + permission='read', + user_group_ids=user_group_ids, + ) + ): + log.warning(f'Access denied to tool {tool_id} for user {user.id}') + continue + + module = request.app.state.TOOLS.get(tool_id, None) + if module is None: + module, _ = await load_tool_module_by_id(tool_id) + request.app.state.TOOLS[tool_id] = module + + __user__ = { + **extra_params['__user__'], + } + + # Set valves for the tool + if hasattr(module, 'valves') and hasattr(module, 'Valves'): + valves = await Tools.get_tool_valves_by_id(tool_id) or {} + module.valves = module.Valves(**valves) + if hasattr(module, 'UserValves'): + __user__['valves'] = module.UserValves( # type: ignore + **await Tools.get_user_valves_by_id_and_user_id(tool_id, user.id) + ) + + for spec in tool.specs: + # TODO: Fix hack for OpenAI API + # Some times breaks OpenAI but others don't. Leaving the comment + for val in spec.get('parameters', {}).get('properties', {}).values(): + if val.get('type') == 'str': + val['type'] = 'string' + + # Remove internal reserved parameters (e.g. __id__, __user__) + spec['parameters']['properties'] = { + key: val for key, val in spec['parameters']['properties'].items() if not key.startswith('__') + } + + # convert to function that takes only model params and inserts custom params + function_name = spec['name'] + tool_function = getattr(module, function_name) + callable = await get_async_tool_function_and_apply_extra_params( + tool_function, + { + **extra_params, + '__id__': tool_id, + '__user__': __user__, + }, + ) + + # TODO: Support Pydantic models as parameters + if callable.__doc__ and callable.__doc__.strip() != '': + s = re.split(':(param|return)', callable.__doc__, 1) + spec['description'] = s[0] + else: + spec['description'] = function_name + + tool_dict = { + 'tool_id': tool_id, + 'callable': callable, + 'spec': spec, + # Misc info + 'metadata': { + 'file_handler': hasattr(module, 'file_handler') and module.file_handler, + 'citation': hasattr(module, 'citation') and module.citation, + }, + } + + # Handle function name collisions + while function_name in tools_dict: + log.warning(f'Tool {function_name} already exists in another tools!') + # Prepend tool ID to function name + function_name = f'{tool_id}_{function_name}' + + tools_dict[function_name] = tool_dict + else: + if tool_id.startswith('server:'): + splits = tool_id.split(':') + + if len(splits) == 2: + type = 'openapi' + server_id = splits[1] + elif len(splits) == 3: + type = splits[1] + server_id = splits[2] + + server_id_splits = server_id.split('|') + if len(server_id_splits) == 2: + server_id = server_id_splits[0] + function_names = server_id_splits[1].split(',') + + if type == 'openapi': + tool_server_data = None + for server in await get_tool_servers(request): + if server['id'] == server_id: + tool_server_data = server + break + + if tool_server_data is None: + log.warning(f'Tool server data not found for {server_id}') + continue + + tool_server_idx = tool_server_data.get('idx', 0) + connections = request.app.state.config.TOOL_SERVER_CONNECTIONS + if tool_server_idx >= len(connections): + log.warning( + f'Tool server index {tool_server_idx} out of range ' + f'(have {len(connections)} connections), skipping server {server_id}' + ) + continue + tool_server_connection = connections[tool_server_idx] + + # Check access control for tool server + if not await has_connection_access(user, tool_server_connection, user_group_ids): + log.warning(f'Access denied to tool server {server_id} for user {user.id}') + continue + + specs = tool_server_data.get('specs', []) + function_name_filter_list = tool_server_connection.get('config', {}).get( + 'function_name_filter_list', '' + ) + + if isinstance(function_name_filter_list, str): + function_name_filter_list = function_name_filter_list.split(',') + + for spec in specs: + function_name = spec['name'] + if function_name_filter_list: + if not is_string_allowed(function_name, function_name_filter_list): + # Skip this function + continue + + auth_type = tool_server_connection.get('auth_type', 'bearer') + + cookies = {} + headers = { + 'Content-Type': 'application/json', + } + + if auth_type == 'bearer': + headers['Authorization'] = f'Bearer {tool_server_connection.get("key", "")}' + elif auth_type == 'none': + # No authentication + pass + elif auth_type == 'session': + cookies = request.cookies + headers['Authorization'] = f'Bearer {request.state.token.credentials}' + elif auth_type == 'system_oauth': + cookies = request.cookies + oauth_token = extra_params.get('__oauth_token__', None) + if oauth_token: + headers['Authorization'] = f'Bearer {oauth_token.get("access_token", "")}' + + connection_headers = tool_server_connection.get('headers', None) + if connection_headers and isinstance(connection_headers, dict): + for key, value in connection_headers.items(): + headers[key] = value + + # Add user info headers if enabled + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) + metadata = extra_params.get('__metadata__', {}) + if metadata and metadata.get('chat_id'): + headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = metadata.get('chat_id') + if metadata and metadata.get('message_id'): + headers[FORWARD_SESSION_INFO_HEADER_MESSAGE_ID] = metadata.get('message_id') + + async def make_tool_function(function_name, tool_server_data, headers): + async def tool_function(**kwargs): + return await execute_tool_server( + url=tool_server_data['url'], + headers=headers, + cookies=cookies, + name=function_name, + params=kwargs, + server_data=tool_server_data, + ) + + return tool_function + + tool_function = await make_tool_function(function_name, tool_server_data, headers) + + callable = await get_async_tool_function_and_apply_extra_params( + tool_function, + {}, + ) + + tool_dict = { + 'tool_id': tool_id, + 'callable': callable, + 'spec': clean_openai_tool_schema(spec), + # Misc info + 'type': 'external', + } + + # Handle function name collisions + while function_name in tools_dict: + log.warning(f'Tool {function_name} already exists in another tools!') + # Prepend server ID to function name + function_name = f'{server_id}_{function_name}' + + tools_dict[function_name] = tool_dict + + else: + continue + + return tools_dict + + +async def get_builtin_tools( + request: Request, extra_params: dict, features: dict = None, model: dict = None +) -> dict[str, dict]: + """ + Get built-in tools for native function calling. + Only returns tools when BOTH the global config is enabled AND the model capability allows it. + """ + tools_dict = {} + builtin_functions = [] + features = features or {} + model = model or {} + + # Helper to get model capabilities (defaults to True if not specified) + def get_model_capability(name: str, default: bool = True) -> bool: + return (model.get('info', {}).get('meta', {}).get('capabilities') or {}).get(name, default) + + # Helper to check if a builtin tool category is enabled via meta.builtinTools + # Defaults to True if not specified (backward compatible) + def is_builtin_tool_enabled(category: str) -> bool: + builtin_tools = model.get('info', {}).get('meta', {}).get('builtinTools', {}) + return builtin_tools.get(category, True) + + # Helper to check user-level feature permission (admins always pass) + user = extra_params.get('__user__', {}) + + async def has_user_permission(feature_key: str) -> bool: + if user.get('role') == 'admin': + return True + return await has_permission( + user.get('id', ''), + f'features.{feature_key}', + request.app.state.config.USER_PERMISSIONS, + ) + + # Time utilities - available for date calculations + if is_builtin_tool_enabled('time'): + builtin_functions.extend([get_current_timestamp, calculate_timestamp]) + + # Knowledge base tools - conditional injection based on model knowledge + # If model has attached knowledge (any type), only provide query_knowledge_files + # Otherwise, provide all KB browsing tools + model_knowledge = model.get('info', {}).get('meta', {}).get('knowledge', []) + # Merge folder-attached knowledge so builtin tools can search it + folder_knowledge = extra_params.get('__metadata__', {}).get('folder_knowledge') + if folder_knowledge: + model_knowledge = list(model_knowledge or []) + list(folder_knowledge) + if is_builtin_tool_enabled('knowledge'): + if model_knowledge: + # Model has attached knowledge - provide discovery, search and semantic tools + builtin_functions.append(list_knowledge) + builtin_functions.append(search_knowledge_files) + builtin_functions.append(query_knowledge_files) + + knowledge_types = {item.get('type') for item in model_knowledge} + if 'file' in knowledge_types or 'collection' in knowledge_types: + builtin_functions.append(view_file) + builtin_functions.append(view_knowledge_file) + if 'note' in knowledge_types: + builtin_functions.append(view_note) + else: + # No model knowledge - allow full KB browsing + builtin_functions.extend( + [ + list_knowledge_bases, + search_knowledge_bases, + query_knowledge_bases, + search_knowledge_files, + query_knowledge_files, + view_knowledge_file, + ] + ) + + # Chats tools - search and fetch user's chat history + if is_builtin_tool_enabled('chats'): + builtin_functions.extend([search_chats, view_chat]) + + # Add memory tools if builtin category enabled AND enabled for this chat + if ( + is_builtin_tool_enabled('memory') + and (features.get('memory') or get_model_capability('memory', False)) + and await has_user_permission('memories') + ): + builtin_functions.extend( + [ + search_memories, + add_memory, + replace_memory_content, + delete_memory, + list_memories, + ] + ) + + # Add web search tools if builtin category enabled AND enabled globally AND model has web_search capability + if ( + is_builtin_tool_enabled('web_search') + and getattr(request.app.state.config, 'ENABLE_WEB_SEARCH', False) + and get_model_capability('web_search') + and features.get('web_search') + and await has_user_permission('web_search') + ): + builtin_functions.extend([search_web, fetch_url]) + + # Add image generation/edit tools if builtin category enabled AND enabled globally AND model has image_generation capability + if ( + is_builtin_tool_enabled('image_generation') + and getattr(request.app.state.config, 'ENABLE_IMAGE_GENERATION', False) + and get_model_capability('image_generation') + and features.get('image_generation') + and await has_user_permission('image_generation') + ): + builtin_functions.append(generate_image) + if ( + is_builtin_tool_enabled('image_generation') + and getattr(request.app.state.config, 'ENABLE_IMAGE_EDIT', False) + and get_model_capability('image_generation') + and features.get('image_generation') + and await has_user_permission('image_generation') + ): + builtin_functions.append(edit_image) + + # Add code interpreter tool if builtin category enabled AND enabled globally AND model has code_interpreter capability + if ( + is_builtin_tool_enabled('code_interpreter') + and getattr(request.app.state.config, 'ENABLE_CODE_INTERPRETER', True) + and get_model_capability('code_interpreter') + and features.get('code_interpreter') + and await has_user_permission('code_interpreter') + ): + builtin_functions.append(execute_code) + + # Notes tools - search, view, create, and update user's notes + if ( + is_builtin_tool_enabled('notes') + and getattr(request.app.state.config, 'ENABLE_NOTES', False) + and await has_user_permission('notes') + ): + builtin_functions.extend([search_notes, view_note, write_note, replace_note_content]) + + # Channels tools - search channels and messages + if ( + is_builtin_tool_enabled('channels') + and getattr(request.app.state.config, 'ENABLE_CHANNELS', False) + and await has_user_permission('channels') + ): + builtin_functions.extend( + [ + search_channels, + search_channel_messages, + view_channel_thread, + view_channel_message, + ] + ) + + # Skills tools - view_skill allows model to load full skill instructions on demand + if extra_params.get('__skill_ids__'): + builtin_functions.append(view_skill) + + # Task management - break down complex work into trackable steps + if is_builtin_tool_enabled('tasks'): + builtin_functions.extend([create_tasks, update_task]) + + # Automation tools - create and manage scheduled automations from chat + if ( + is_builtin_tool_enabled('automations') + and getattr(request.app.state.config, 'ENABLE_AUTOMATIONS', False) + and await has_user_permission('automations') + ): + builtin_functions.extend( + [create_automation, update_automation, list_automations, toggle_automation, delete_automation] + ) + + # Calendar tools - search/create/update/delete events + if ( + is_builtin_tool_enabled('calendar') + and getattr(request.app.state.config, 'ENABLE_CALENDAR', False) + and await has_user_permission('calendar') + ): + builtin_functions.extend( + [search_calendar_events, create_calendar_event, update_calendar_event, delete_calendar_event] + ) + + for func in builtin_functions: + callable = await get_async_tool_function_and_apply_extra_params( + func, + { + '__request__': request, + '__user__': extra_params.get('__user__', {}), + '__event_emitter__': extra_params.get('__event_emitter__'), + '__event_call__': extra_params.get('__event_call__'), + '__metadata__': extra_params.get('__metadata__'), + '__chat_id__': extra_params.get('__chat_id__'), + '__message_id__': extra_params.get('__message_id__'), + '__model_knowledge__': model_knowledge, + }, + ) + + # Generate spec from function + pydantic_model = convert_function_to_pydantic_model(func) + spec = convert_pydantic_model_to_openai_function_spec(pydantic_model) + spec = clean_openai_tool_schema(spec) + + tools_dict[func.__name__] = { + 'tool_id': f'builtin:{func.__name__}', + 'callable': callable, + 'spec': spec, + 'type': 'builtin', + } + + return tools_dict + + +def parse_description(docstring: str | None) -> str: + """ + Parse a function's docstring to extract the description. + + Args: + docstring (str): The docstring to parse. + + Returns: + str: The description. + """ + + if not docstring: + return '' + + lines = [line.strip() for line in docstring.strip().split('\n')] + description_lines: list[str] = [] + + for line in lines: + if re.match(r':param', line) or re.match(r':return', line): + break + + description_lines.append(line) + + return '\n'.join(description_lines) + + +def parse_docstring(docstring): + """ + Parse a function's docstring to extract parameter descriptions in reST format. + + Args: + docstring (str): The docstring to parse. + + Returns: + dict: A dictionary where keys are parameter names and values are descriptions. + """ + if not docstring: + return {} + + # Regex to match `:param name: description` format + param_pattern = re.compile(r':param (\w+):\s*(.+)') + param_descriptions = {} + + for line in docstring.splitlines(): + match = param_pattern.match(line.strip()) + if not match: + continue + param_name, param_description = match.groups() + if param_name.startswith('__'): + continue + param_descriptions[param_name] = param_description + + return param_descriptions + + +def convert_function_to_pydantic_model(func: Callable) -> type[BaseModel]: + """ + Converts a Python function's type hints and docstring to a Pydantic model, + including support for nested types, default values, and descriptions. + + Args: + func: The function whose type hints and docstring should be converted. + model_name: The name of the generated Pydantic model. + + Returns: + A Pydantic model class. + """ + type_hints = get_type_hints(func) + signature = inspect.signature(func) + parameters = signature.parameters + + docstring = func.__doc__ + + function_description = parse_description(docstring) + function_param_descriptions = parse_docstring(docstring) + + field_defs = {} + for name, param in parameters.items(): + type_hint = type_hints.get(name, Any) + default_value = param.default if param.default is not param.empty else ... + + param_description = function_param_descriptions.get(name, None) + + if param_description: + field_defs[name] = ( + type_hint, + Field(default_value, description=param_description), + ) + else: + field_defs[name] = type_hint, default_value + + model = create_model(func.__name__, **field_defs) + model.__doc__ = function_description + + return model + + +def clean_properties(schema: dict): + if not isinstance(schema, dict): + return + + if 'anyOf' in schema: + non_null_types = [t for t in schema['anyOf'] if t.get('type') != 'null'] + if len(non_null_types) == 1: + schema.update(non_null_types[0]) + del schema['anyOf'] + else: + schema['anyOf'] = non_null_types + + if 'default' in schema and schema['default'] is None: + del schema['default'] + + # fix missing type + if 'type' not in schema and 'anyOf' not in schema and 'properties' not in schema: + schema['type'] = 'string' + + if 'properties' in schema: + for prop_name, prop_schema in schema['properties'].items(): + clean_properties(prop_schema) + + if 'items' in schema: + clean_properties(schema['items']) + + +def clean_openai_tool_schema(spec: dict) -> dict: + import copy + + cleaned_spec = copy.deepcopy(spec) + + if 'parameters' in cleaned_spec: + clean_properties(cleaned_spec['parameters']) + + return cleaned_spec + + +def get_functions_from_tool(tool: object) -> list[Callable]: + return [ + getattr(tool, func) + for func in dir(tool) + if callable(getattr(tool, func)) # checks if the attribute is callable (a method or function). + and not func.startswith('_') # filters out internal methods (starting with _) and special (dunder) methods. + and not inspect.isclass( + getattr(tool, func) + ) # ensures that the callable is not a class itself, just a method or function. + ] + + +def get_tool_specs(tool_module: object) -> list[dict]: + function_models = map(convert_function_to_pydantic_model, get_functions_from_tool(tool_module)) + + specs = [ + clean_openai_tool_schema(convert_pydantic_model_to_openai_function_spec(function_model)) + for function_model in function_models + ] + + return specs + + +def resolve_schema(schema, components, resolved_schemas=None): + """ + Recursively resolves a JSON schema using OpenAPI components. + """ + if not schema: + return {} + + if resolved_schemas is None: + resolved_schemas = set() + + if '$ref' in schema: + ref_path = schema['$ref'] + schema_name = ref_path.split('/')[-1] + + if schema_name in resolved_schemas: + # Avoid infinite recursion on circular references + return {} + + resolved_schemas.add(schema_name) + + ref_parts = ref_path.strip('#/').split('/') + resolved = components + for part in ref_parts[1:]: # Skip the initial 'components' + resolved = resolved.get(part, {}) + return resolve_schema(resolved, components, resolved_schemas) + + resolved_schema = copy.deepcopy(schema) + + # Recursively resolve inner schemas + if 'properties' in resolved_schema: + for prop, prop_schema in resolved_schema['properties'].items(): + resolved_schema['properties'][prop] = resolve_schema(prop_schema, components) + + if 'items' in resolved_schema: + resolved_schema['items'] = resolve_schema(resolved_schema['items'], components) + + return resolved_schema + + +def convert_openapi_to_tool_payload(openapi_spec): + """ + Converts an OpenAPI specification into a custom tool payload structure. + + Args: + openapi_spec (dict): The OpenAPI specification as a Python dict. + + Returns: + list: A list of tool payloads. + """ + tool_payload = [] + + for path, methods in openapi_spec.get('paths', {}).items(): + for method, operation in methods.items(): + if operation.get('operationId'): + tool = { + 'name': operation.get('operationId'), + 'description': operation.get( + 'description', + operation.get('summary', 'No description available.'), + ), + 'parameters': {'type': 'object', 'properties': {}, 'required': []}, + } + + for param in operation.get('parameters', []): + param_name = param.get('name') + if not param_name: + continue + param_schema = param.get('schema', {}) + description = param_schema.get('description', '') + if not description: + description = param.get('description') or '' + if param_schema.get('enum') and isinstance(param_schema.get('enum'), list): + description += f'. Possible values: {", ".join(str(v) for v in param_schema.get("enum"))}' + param_property = { + 'type': param_schema.get('type') or 'string', + 'description': description, + } + + # Include items property for array types (required by OpenAI) + if param_schema.get('type') == 'array' and 'items' in param_schema: + param_property['items'] = param_schema['items'] + + # Filter out None values to prevent schema validation errors + param_property = {k: v for k, v in param_property.items() if v is not None} + + tool['parameters']['properties'][param_name] = param_property + if param.get('required'): + tool['parameters']['required'].append(param_name) + + # Extract and resolve requestBody if available + request_body = operation.get('requestBody') + if request_body: + content = request_body.get('content', {}) + json_schema = content.get('application/json', {}).get('schema') + if json_schema: + resolved_schema = resolve_schema(json_schema, openapi_spec.get('components', {})) + + if resolved_schema.get('properties'): + tool['parameters']['properties'].update(resolved_schema['properties']) + if 'required' in resolved_schema: + tool['parameters']['required'] = list( + set(tool['parameters']['required'] + resolved_schema['required']) + ) + elif resolved_schema.get('type') == 'array': + tool['parameters'] = resolved_schema # special case for array + + tool_payload.append(tool) + + return tool_payload + + +async def set_tool_servers(request: Request): + request.app.state.TOOL_SERVERS = await get_tool_servers_data(request.app.state.config.TOOL_SERVER_CONNECTIONS) + + if request.app.state.redis is not None: + await request.app.state.redis.set( + f'{REDIS_KEY_PREFIX}:tool_servers', json.dumps(request.app.state.TOOL_SERVERS) + ) + + return request.app.state.TOOL_SERVERS + + +async def get_tool_servers(request: Request): + tool_servers = [] + if request.app.state.redis is not None: + try: + tool_servers = json.loads(await request.app.state.redis.get(f'{REDIS_KEY_PREFIX}:tool_servers')) + request.app.state.TOOL_SERVERS = tool_servers + except Exception as e: + log.error(f'Error fetching tool_servers from Redis: {e}') + + if not tool_servers: + tool_servers = await set_tool_servers(request) + + return tool_servers + + +async def get_terminal_cwd( + base_url: str, + headers: dict, + cookies: Optional[dict] = None, +) -> Optional[str]: + """Fetch the current working directory from a terminal server.""" + try: + cwd_url = f'{base_url.rstrip("/")}/files/cwd' + async with aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=5), + trust_env=True, + ) as session: + async with session.get( + cwd_url, headers=headers, cookies=cookies or {}, ssl=AIOHTTP_CLIENT_SESSION_SSL + ) as resp: + if resp.status == 200: + data = await resp.json() + return data.get('cwd') + except Exception as e: + log.debug(f'Failed to fetch terminal CWD: {e}') + return None + + +async def get_terminal_system_prompt( + base_url: str, + headers: dict, + cookies: Optional[dict] = None, +) -> Optional[str]: + """Fetch the system prompt from a terminal server. + + Checks ``/api/config`` for the ``system`` feature flag first; + only fetches ``/system`` if the flag is present. Returns *None* + silently when the server doesn't support the endpoint. + """ + base = base_url.rstrip('/') + try: + async with aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=3), + trust_env=True, + ) as session: + # 1. Check feature flag + async with session.get(f'{base}/api/config', ssl=AIOHTTP_CLIENT_SESSION_SSL) as resp: + if resp.status != 200: + return None + config = await resp.json() + if not config.get('features', {}).get('system'): + return None + + # 2. Fetch system prompt + async with session.get( + f'{base}/system', headers=headers, cookies=cookies or {}, ssl=AIOHTTP_CLIENT_SESSION_SSL + ) as resp: + if resp.status == 200: + data = await resp.json() + return data.get('prompt') + except Exception as e: + log.debug(f'Failed to fetch terminal system prompt: {e}') + return None + + +async def set_terminal_servers(request: Request): + """Load and cache OpenAPI specs from all TERMINAL_SERVER_CONNECTIONS.""" + connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or [] + + # Build server configs compatible with get_tool_servers_data + # Terminal connections store id/name at top level; translate to info dict + server_configs = [] + for connection in connections: + if not connection.get('url'): + continue + + enabled = connection.get('enabled', True) + + base_url = connection.get('url', '').rstrip('/') + policy_id = connection.get('policy_id', '') + + # Orchestrator connections route through /p/{policy_id}/ — the + # OpenAPI spec lives on the proxied terminal, not the orchestrator. + if connection.get('server_type') == 'orchestrator' and policy_id: + base_url = f'{base_url}/p/{policy_id}' + + server_configs.append( + { + 'url': base_url, + 'key': connection.get('key', ''), + 'auth_type': connection.get('auth_type', 'bearer'), + 'path': connection.get('path', '/openapi.json'), + 'spec_type': 'url', + # get_tool_servers_data reads config.enable to filter active servers + 'config': {'enable': enabled}, + 'info': { + 'id': connection.get('id', ''), + 'name': connection.get('name', ''), + }, + } + ) + + request.app.state.TERMINAL_SERVERS = await get_tool_servers_data(server_configs) + + # Fetch system prompts concurrently (runs at cache time, not per-request) + connections_by_id = {c.get('id'): c for c in connections if c.get('id')} + + async def _fetch_system_prompt(server): + connection = connections_by_id.get(server.get('id')) + if not connection: + return + headers = {} + if connection.get('auth_type', 'bearer') == 'bearer': + headers['Authorization'] = f'Bearer {connection.get("key", "")}' + prompt = await get_terminal_system_prompt(server['url'], headers) + if prompt: + server['system_prompt'] = prompt + + await asyncio.gather( + *[_fetch_system_prompt(s) for s in request.app.state.TERMINAL_SERVERS], + return_exceptions=True, + ) + + if request.app.state.redis is not None: + await request.app.state.redis.set( + f'{REDIS_KEY_PREFIX}:terminal_servers', json.dumps(request.app.state.TERMINAL_SERVERS) + ) + + return request.app.state.TERMINAL_SERVERS + + +async def get_terminal_servers(request: Request): + """Return cached terminal server specs, loading if needed.""" + terminal_servers = [] + if request.app.state.redis is not None: + try: + terminal_servers = json.loads(await request.app.state.redis.get(f'{REDIS_KEY_PREFIX}:terminal_servers')) + request.app.state.TERMINAL_SERVERS = terminal_servers + except Exception as e: + log.error(f'Error fetching terminal_servers from Redis: {e}') + + if not terminal_servers: + terminal_servers = await set_terminal_servers(request) + + return terminal_servers + + +async def get_terminal_tools( + request: Request, + terminal_id: str, + user: UserModel, + extra_params: dict, +) -> dict[str, dict] | tuple[dict[str, dict], Optional[str]]: + """Resolve tools for a terminal server identified by terminal_id. + + - Finds the connection in TERMINAL_SERVER_CONNECTIONS + - Checks access_grants + - Loads specs from cache + - Builds callables that route through the terminal proxy + """ + connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or [] + connection = next((c for c in connections if c.get('id') == terminal_id), None) + if connection is None: + log.warning(f'Terminal server not found: {terminal_id}') + return {} + + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id)} + if not await has_connection_access(user, connection, user_group_ids): + log.warning(f'Access denied to terminal {terminal_id} for user {user.id}') + return {} + + # Find the cached spec data for this terminal + terminal_servers = await get_terminal_servers(request) + server_data = next((s for s in terminal_servers if s.get('id') == terminal_id), None) + if server_data is None: + log.warning(f'Terminal server spec not found for {terminal_id}') + return {} + + specs = server_data.get('specs', []) + if not specs: + return {} + + # Build auth headers + auth_type = connection.get('auth_type', 'bearer') + cookies = {} + headers = {'Content-Type': 'application/json', 'X-User-Id': user.id} + + if auth_type == 'bearer': + headers['Authorization'] = f'Bearer {connection.get("key", "")}' + elif auth_type == 'session': + cookies = request.cookies + headers['Authorization'] = f'Bearer {request.state.token.credentials}' + elif auth_type == 'system_oauth': + cookies = request.cookies + oauth_token = extra_params.get('__oauth_token__', None) + if oauth_token: + headers['Authorization'] = f'Bearer {oauth_token.get("access_token", "")}' + # auth_type == "none": no Authorization header + + system_prompt = server_data.get('system_prompt') + + # Use chat_id as the per-session key for cwd tracking + metadata = extra_params.get('__metadata__', {}) + session_id = metadata.get('chat_id') + if session_id: + headers['X-Session-Id'] = session_id + + terminal_cwd = await get_terminal_cwd(connection.get('url', ''), headers, cookies) + + tools_dict = {} + for spec in specs: + function_name = spec['name'] + tool_spec = clean_openai_tool_schema(spec) + + if function_name == 'run_command' and terminal_cwd: + tool_spec['description'] = ( + tool_spec.get('description', '') + f'\n\nThe current working directory is: {terminal_cwd}' + ) + + async def make_tool_function(fn_name, srv_data, hdrs, cks): + async def tool_function(**kwargs): + return await execute_tool_server( + url=srv_data['url'], + headers=hdrs, + cookies=cks, + name=fn_name, + params=kwargs, + server_data=srv_data, + ) + + return tool_function + + tool_function = await make_tool_function(function_name, server_data, headers, cookies) + callable = await get_async_tool_function_and_apply_extra_params(tool_function, {}) + + tools_dict[function_name] = { + 'tool_id': f'terminal:{terminal_id}', + 'callable': callable, + 'spec': tool_spec, + 'type': 'terminal', + } + + return tools_dict, system_prompt + + +async def get_tool_server_data(url: str, headers: Optional[dict]) -> Dict[str, Any]: + _headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + } + + if headers: + _headers.update(headers) + + error = None + try: + timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.get(url, headers=_headers, ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL) as response: + if response.status != 200: + error_body = await response.json() + raise Exception(error_body) + + text_content = None + + # Check if URL ends with .yaml or .yml to determine format + if url.lower().endswith(('.yaml', '.yml')): + text_content = await response.text() + res = yaml.safe_load(text_content) + else: + text_content = await response.text() + + try: + res = json.loads(text_content) + except json.JSONDecodeError: + try: + res = yaml.safe_load(text_content) + except Exception as e: + raise e + + except Exception as err: + log.exception(f'Could not fetch tool server spec from {url}') + if isinstance(err, dict) and 'detail' in err: + error = err['detail'] + else: + error = str(err) + raise Exception(error) + + log.debug(f'Fetched data: {res}') + return res + + +async def get_tool_servers_data(servers: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + # Prepare list of enabled servers along with their original index + + tasks = [] + server_entries = [] + for idx, server in enumerate(servers): + if server.get('config', {}).get('enable') and server.get('type', 'openapi') == 'openapi': + info = server.get('info', {}) + + auth_type = server.get('auth_type', 'bearer') + token = None + + if auth_type == 'bearer': + token = server.get('key', '') + elif auth_type == 'none': + # No authentication + pass + + id = info.get('id') + if not id: + id = str(idx) + + server_url = server.get('url') + spec_type = server.get('spec_type', 'url') + + # Create async tasks to fetch data + task = None + if spec_type == 'url': + # Path (to OpenAPI spec URL) can be either a full URL or a path to append to the base URL + openapi_path = server.get('path', 'openapi.json') + spec_url = get_tool_server_url(server_url, openapi_path) + # Fetch from URL + task = get_tool_server_data( + spec_url, + {'Authorization': f'Bearer {token}'} if token else None, + ) + elif spec_type == 'json' and server.get('spec', ''): + # Use provided JSON spec + spec_json = None + try: + spec_json = json.loads(server.get('spec', '')) + except Exception as e: + log.error(f'Error parsing JSON spec for tool server {id}: {e}') + + if spec_json: + task = asyncio.sleep( + 0, + result=spec_json, + ) + + if task: + tasks.append(task) + server_entries.append((id, idx, server, server_url, info, token)) + + # Execute tasks concurrently + responses = await asyncio.gather(*tasks, return_exceptions=True) + + # Build final results with index and server metadata + results = [] + for (id, idx, server, url, info, _), response in zip(server_entries, responses): + if isinstance(response, Exception): + log.error(f'Failed to connect to {url} OpenAPI tool server') + continue + + # Guard against invalid or non-OpenAPI specs (e.g., MCP-style configs) + if not isinstance(response, dict) or 'paths' not in response: + log.warning(f"Invalid OpenAPI spec from {url}: missing 'paths'") + continue + + response = { + 'openapi': response, + 'info': response.get('info', {}), + 'specs': convert_openapi_to_tool_payload(response), + } + + openapi_data = response.get('openapi', {}) + if info and isinstance(openapi_data, dict): + openapi_data['info'] = openapi_data.get('info', {}) + + if 'name' in info: + openapi_data['info']['title'] = info.get('name', 'Tool Server') + + if 'description' in info: + openapi_data['info']['description'] = info.get('description', '') + + results.append( + { + 'id': str(id), + 'idx': idx, + 'url': (server.get('url') or '').rstrip('/'), + 'openapi': openapi_data, + 'info': response.get('info'), + 'specs': response.get('specs'), + } + ) + + return results + + +async def execute_tool_server( + url: str, + headers: Dict[str, str], + cookies: Dict[str, str], + name: str, + params: Dict[str, Any], + server_data: Dict[str, Any], +) -> Tuple[Dict[str, Any], Optional[Dict[str, Any]]]: + error = None + try: + openapi = server_data.get('openapi', {}) + paths = openapi.get('paths', {}) + + matching_route = None + for route_path, methods in paths.items(): + for http_method, operation in methods.items(): + if isinstance(operation, dict) and operation.get('operationId') == name: + matching_route = (route_path, methods) + break + if matching_route: + break + + if not matching_route: + raise Exception(f'No matching route found for operationId: {name}') + + route_path, methods = matching_route + + method_entry = None + for http_method, operation in methods.items(): + if operation.get('operationId') == name: + method_entry = (http_method.lower(), operation) + break + + if not method_entry: + raise Exception(f'No matching method found for operationId: {name}') + + http_method, operation = method_entry + + path_params = {} + query_params = {} + body_params = {} + + for param in operation.get('parameters', []): + param_name = param.get('name') + if not param_name: + continue + param_in = param.get('in') + if param_name in params: + if param_in == 'path': + path_params[param_name] = params[param_name] + if param_in == 'query': + value = params[param_name] + # Skip empty values for optional params (LLMs sometimes + # pass "" instead of omitting optional parameters). + if value is None or (value == '' and not param.get('required')): + continue + query_params[param_name] = value + + final_url = f'{url.rstrip("/")}{route_path}' + for key, value in path_params.items(): + final_url = final_url.replace(f'{{{key}}}', str(value)) + + if query_params: + query_string = '&'.join(f'{k}={v}' for k, v in query_params.items()) + final_url = f'{final_url}?{query_string}' + + if operation.get('requestBody', {}).get('content'): + if params: + body_params = params + + async with aiohttp.ClientSession( + trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER) + ) as session: + request_method = getattr(session, http_method.lower()) + + if http_method in ['post', 'put', 'patch', 'delete']: + async with request_method( + final_url, + json=body_params, + headers=headers, + cookies=cookies, + ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, + allow_redirects=False, + ) as response: + if response.status >= 400: + text = await response.text() + raise Exception(f'HTTP error {response.status}: {text}') + + try: + response_data = await response.json() + except Exception: + content_type = response.headers.get('Content-Type', '').split(';')[0].strip() + if content_type.startswith('text/') or not content_type: + response_data = await response.text() + else: + raw = await response.read() + b64 = base64.b64encode(raw).decode() + response_data = f'data:{content_type};base64,{b64}' + + response_headers = response.headers + return (response_data, response_headers) + else: + async with request_method( + final_url, + headers=headers, + cookies=cookies, + ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, + allow_redirects=False, + ) as response: + if response.status >= 400: + text = await response.text() + raise Exception(f'HTTP error {response.status}: {text}') + + try: + response_data = await response.json() + except Exception: + content_type = response.headers.get('Content-Type', '').split(';')[0].strip() + if content_type.startswith('text/') or not content_type: + response_data = await response.text() + else: + raw = await response.read() + b64 = base64.b64encode(raw).decode() + response_data = f'data:{content_type};base64,{b64}' + + response_headers = response.headers + return (response_data, response_headers) + + except Exception as err: + error = str(err) + log.exception(f'API Request Error: {error}') + return ({'error': error}, None) + + +def get_tool_server_url(url: Optional[str], path: str) -> str: + """ + Build the full URL for a tool server, given a base url and a path. + """ + if '://' in path: + # If it contains "://", it's a full URL + return path + if url: + url = url.rstrip('/') + if not path.startswith('/'): + # Ensure the path starts with a slash + path = f'/{path}' + return f'{url}{path}' diff --git a/backend/open_webui/utils/validate.py b/backend/open_webui/utils/validate.py new file mode 100644 index 0000000000000000000000000000000000000000..1e98b4110593d007e97544a2c777f78bf3975f75 --- /dev/null +++ b/backend/open_webui/utils/validate.py @@ -0,0 +1,82 @@ +"""Validation utilities for user-supplied input.""" + +import re +from urllib.parse import urlparse + +# Matches the OWUI-generated profile image route. ``[^/?#]+`` accepts +# any user-ID without allowing path-traversal or query/fragment injection, +# and the ``$`` anchor rejects trailing path components. +_USER_PROFILE_IMAGE_RE = re.compile(r'^/api/v1/users/[^/?#]+/profile/image$') + +# Validates MIME type and structure of base64 data URIs. Only the prefix +# is checked — validating the full base64 payload would mean running a +# regex across megabytes of data on every Pydantic instantiation for zero +# security benefit (corrupt base64 simply renders a broken image, same as +# a 404 URL). SVG is intentionally excluded: it can carry embedded scripts. +_SAFE_DATA_URI_RE = re.compile(r'^data:image/(png|jpeg|gif|webp);base64,', re.IGNORECASE) + +# Exact relative paths accepted as profile images. These are the only +# static-asset paths OWUI itself assigns; no prefix/wildcard matching is +# used so that arbitrary relative paths cannot trigger authenticated GETs +# against internal endpoints when rendered as ```` sources. +_SAFE_STATIC_PATHS = frozenset( + { + '/user.png', + '/favicon.png', + '/static/favicon.png', + } +) + + +def validate_profile_image_url(url: str) -> str: + """ + Pydantic-compatible validator for profile image URLs. + + Allowed formats: + - Empty string (falls back to default avatar) + - Known static-asset paths assigned by OWUI (exact match) + - The OWUI profile-image API route ``/api/v1/users/{id}/profile/image`` + - ``http://`` and ``https://`` URLs with a valid hostname + - ``data:image/{png,jpeg,gif,webp};base64,...`` URIs + + Everything else is rejected, including: + - Dangerous schemes (javascript:, file:, ftp:, …) + - SVG data URIs (can contain embedded scripts) + - Arbitrary relative paths (prevents authenticated GET triggers) + - Scheme-relative URLs (``//host/path``) + """ + if not url: + return url + + # --- Relative paths (exact match + anchored regex only) ----------- + + if url in _SAFE_STATIC_PATHS: + return url + + if _USER_PROFILE_IMAGE_RE.match(url): + return url + + # --- Absolute URLs ------------------------------------------------- + + # urlparse normalises the scheme to lowercase, giving us + # case-insensitive scheme matching for free. + parsed = urlparse(url) + + # External images served over HTTP(S), e.g. OAuth provider avatars. + # Require a non-empty hostname (not just netloc, which can be ":80" + # for a URL like http://:80/path with no actual host). + if parsed.scheme in ('http', 'https'): + if not parsed.hostname: + raise ValueError('Invalid profile image URL: HTTP(S) URLs must include a host.') + return url + + # Base64-encoded raster images uploaded via the frontend. + # The regex enforces the ;base64, boundary and is case-insensitive + # per the data-URI / MIME-type specs. + if _SAFE_DATA_URI_RE.match(url): + return url + + raise ValueError( + 'Invalid profile image URL: must be a known internal path, ' + 'an HTTP(S) URL with a host, or a data:image URI (png/jpeg/gif/webp).' + ) diff --git a/backend/open_webui/utils/webhook.py b/backend/open_webui/utils/webhook.py new file mode 100644 index 0000000000000000000000000000000000000000..ee7f3ab3b2e901318415eb2729484c63ff55020e --- /dev/null +++ b/backend/open_webui/utils/webhook.py @@ -0,0 +1,64 @@ +import json +import logging +import aiohttp + +from open_webui.config import WEBUI_FAVICON_URL +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT, VERSION + +log = logging.getLogger(__name__) + + +# Let this message reach those for whom it was written, and +# may no network partition deny the word its destination. +async def post_webhook(name: str, url: str, message: str, event_data: dict) -> bool: + try: + log.debug(f'post_webhook: {url}, {message}, {event_data}') + payload = {} + + # Slack and Google Chat Webhooks + if 'https://hooks.slack.com' in url or 'https://chat.googleapis.com' in url: + payload['text'] = message + # Discord Webhooks + elif 'https://discord.com/api/webhooks' in url: + payload['content'] = message if len(message) < 2000 else f'{message[: 2000 - 20]}... (truncated)' + # Microsoft Teams Webhooks + elif 'webhook.office.com' in url: + action = event_data.get('action', 'undefined') + user_data = event_data.get('user', '{}') + if isinstance(user_data, dict): + user_dict = user_data + else: + user_dict = json.loads(user_data) + facts = [{'name': name, 'value': value} for name, value in user_dict.items()] + payload = { + '@type': 'MessageCard', + '@context': 'http://schema.org/extensions', + 'themeColor': '0076D7', + 'summary': message, + 'sections': [ + { + 'activityTitle': message, + 'activitySubtitle': f'{name} ({VERSION}) - {action}', + 'activityImage': WEBUI_FAVICON_URL, + 'facts': facts, + 'markdown': True, + } + ], + } + # Default Payload + else: + payload = {**event_data} + + log.debug(f'payload: {payload}') + async with aiohttp.ClientSession( + trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + ) as session: + async with session.post(url, json=payload, ssl=AIOHTTP_CLIENT_SESSION_SSL) as r: + r_text = await r.text() + r.raise_for_status() + log.debug(f'r.text: {r_text}') + + return True + except Exception as e: + log.exception(e) + return False diff --git a/backend/requirements-min.txt b/backend/requirements-min.txt new file mode 100644 index 0000000000000000000000000000000000000000..05c28deba66216a003dffc3f88376e0ab435c948 --- /dev/null +++ b/backend/requirements-min.txt @@ -0,0 +1,59 @@ +# Minimal requirements for backend to run +# WIP: use this as a reference to build a minimal docker image + +fastapi==0.135.1 +uvicorn[standard]==0.41.0 +pydantic==2.12.5 +python-multipart==0.0.22 +itsdangerous==2.2.0 + +python-socketio==5.16.1 +python-jose==3.5.0 +cryptography +bcrypt==5.0.0 +argon2-cffi==25.1.0 +PyJWT[crypto]==2.11.0 +authlib==1.6.10 + +requests==2.33.1 +aiohttp==3.13.5 # do not update to 3.13.3 - broken +async-timeout +aiocache +aiofiles +starlette-compress==1.7.0 +Brotli==1.2.0 +brotlicffi==1.2.0.1 +httpx[socks,http2,zstd,cli,brotli]==0.28.1 +starsessions[redis]==2.2.1 + +sqlalchemy==2.0.48 +aiosqlite==0.21.0 +psycopg[binary]==3.2.9 +alembic==1.18.4 +peewee==3.19.0 +peewee-migrate==1.14.3 + +pycrdt==0.12.47 +redis + +APScheduler==3.11.2 +RestrictedPython==8.1 + +loguru==0.7.3 +asgiref==3.11.1 + +mcp==1.26.0 +openai + +langchain==1.2.10 +langchain-community==0.4.1 +langchain-classic==1.0.1 +langchain-text-splitters==1.1.1 + +fake-useragent==2.2.0 + +chromadb==1.5.2 +black==26.3.1 +pydub +chardet==5.2.0 +beautifulsoup4 diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..a7d2b1cb53793c65d44254dad4279671b6945300 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,160 @@ +fastapi==0.135.1 +uvicorn[standard]==0.41.0 +pydantic==2.12.5 +python-multipart==0.0.22 +itsdangerous==2.2.0 + +python-socketio==5.16.1 +python-jose==3.5.0 +cryptography==46.0.5 +bcrypt==5.0.0 +argon2-cffi==25.1.0 +PyJWT[crypto]==2.11.0 +authlib==1.6.10 + +requests==2.33.1 +aiohttp==3.13.5 # do not update to 3.13.3 - broken +async-timeout==5.0.1 +aiocache==0.12.3 +aiofiles==25.1.0 +starlette-compress==1.7.0 +Brotli==1.2.0 +brotlicffi==1.2.0.1 +httpx[socks,http2,zstd,cli,brotli]==0.28.1 +starsessions[redis]==2.2.1 +python-mimeparse==2.0.0 + +sqlalchemy[asyncio]==2.0.48 +aiosqlite==0.21.0 +psycopg[binary]==3.2.9 +alembic==1.18.4 +peewee==3.19.0 +peewee-migrate==1.14.3 + +pycrdt==0.12.47 +redis==7.4.0 + +APScheduler==3.11.2 +RestrictedPython==8.1 +pytz==2026.1.post1 + +loguru==0.7.3 +asgiref==3.11.1 + +# AI libraries +tiktoken==0.12.0 +mcp==1.26.0 + +openai==2.29.0 +anthropic==0.86.0 +google-genai==1.66.0 + +langchain==1.2.10 +langchain-community==0.4.1 +langchain-classic==1.0.1 +langchain-text-splitters==1.1.1 + +fake-useragent==2.2.0 +chromadb==1.5.2 +weaviate-client==4.20.3 +opensearch-py==3.1.0 + +transformers==5.5.4 +sentence-transformers==5.4.0 +accelerate==1.13.0 +pyarrow==20.0.0 # fix: pin pyarrow version to 20 for rpi compatibility #15897 +einops==0.8.2 + +ftfy==6.3.1 +chardet==5.2.0 +pypdf==6.7.5 +fpdf2==2.8.7 +pymdown-extensions==10.21 +docx2txt==0.9 +python-pptx==1.0.2 +msoffcrypto-tool==6.0.0 +unstructured==0.18.31 + +nltk==3.9.3 +Markdown==3.10.2 +beautifulsoup4==4.14.3 +pypandoc==1.16.2 +pandas==3.0.1 +openpyxl==3.1.5 +pyxlsb==1.0.10 +xlrd==2.0.2 +validators==0.35.0 +psutil==7.2.2 +sentencepiece==0.2.1 +soundfile==0.13.1 + +pillow==12.1.1 +opencv-python-headless==4.13.0.92 +rapidocr-onnxruntime==1.4.4 +rank-bm25==0.2.2 + +onnxruntime==1.24.3 +faster-whisper==1.2.1 + +black==26.3.1 +youtube-transcript-api==1.2.4 +pytube==15.0.0 + +pydub==0.25.1 +ddgs==9.11.3 + +azure-ai-documentintelligence==1.0.2 +azure-identity==1.25.2 +azure-storage-blob==12.28.0 +azure-search-documents==11.6.0 + +## Google Drive +google-api-python-client==2.193.0 +google-auth-httplib2==0.3.0 +google-auth-oauthlib==1.3.0 + +googleapis-common-protos==1.72.0 +google-cloud-storage==3.9.0 + +## Databases +pymongo==4.16.0 +psycopg2-binary==2.9.11 +pgvector==0.4.2 + +PyMySQL==1.1.2 +boto3==1.42.62 +# mariadb==1.1.14 should be added if you want to support MariaDB + +pymilvus==2.6.9 +qdrant-client==1.17.0 +playwright==1.58.0 # Caution: version must match docker-compose.playwright.yaml - Update the docker-compose.yaml if necessary +elasticsearch==9.3.0 +pinecone==6.0.2 +oracledb==3.4.2 + +av==14.0.1 # Caution: Set due to FATAL FIPS SELFTEST FAILURE, see discussion https://github.com/open-webui/open-webui/discussions/15720 + +colbert-ai==0.2.22 + + +## Tests +docker~=7.1.0 +pytest~=8.4.1 +pytest-docker~=3.2.5 + +## LDAP +ldap3==2.9.1 + +## Trace +opentelemetry-api==1.40.0 +opentelemetry-sdk==1.40.0 +opentelemetry-exporter-otlp==1.40.0 +opentelemetry-instrumentation==0.61b0 +opentelemetry-instrumentation-fastapi==0.61b0 +opentelemetry-instrumentation-sqlalchemy==0.61b0 +opentelemetry-instrumentation-redis==0.61b0 +opentelemetry-instrumentation-requests==0.61b0 +opentelemetry-instrumentation-logging==0.61b0 +opentelemetry-instrumentation-httpx==0.61b0 +opentelemetry-instrumentation-aiohttp-client==0.61b0 +opentelemetry-instrumentation-system-metrics==0.61b0 diff --git a/backend/start.sh b/backend/start.sh new file mode 100644 index 0000000000000000000000000000000000000000..00d02f326b7f1a7e25d3516d1ec65db840474cdf --- /dev/null +++ b/backend/start.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +cd "$SCRIPT_DIR" || exit + +# Add conditional Playwright browser installation +if [[ "${WEB_LOADER_ENGINE,,}" == "playwright" ]]; then + if [[ -z "${PLAYWRIGHT_WS_URL}" ]]; then + echo "Installing Playwright browsers..." + playwright install chromium + playwright install-deps chromium + fi + + python -c "import nltk; nltk.download('punkt_tab')" +fi + +if [ -n "${WEBUI_SECRET_KEY_FILE}" ]; then + KEY_FILE="${WEBUI_SECRET_KEY_FILE}" +else + KEY_FILE=".webui_secret_key" +fi + +PORT="${PORT:-8080}" +HOST="${HOST:-0.0.0.0}" +if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then + echo "Loading WEBUI_SECRET_KEY from file, not provided as an environment variable." + + if ! [ -e "$KEY_FILE" ]; then + echo "Generating WEBUI_SECRET_KEY" + # Generate a random value to use as a WEBUI_SECRET_KEY in case the user didn't provide one. + echo $(head -c 12 /dev/random | base64) > "$KEY_FILE" + fi + + echo "Loading WEBUI_SECRET_KEY from $KEY_FILE" + WEBUI_SECRET_KEY=$(cat "$KEY_FILE") +fi + +if [[ "${USE_OLLAMA_DOCKER,,}" == "true" ]]; then + echo "USE_OLLAMA is set to true, starting ollama serve." + ollama serve & +fi + +if [[ "${USE_CUDA_DOCKER,,}" == "true" ]]; then + echo "CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries." + export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib/python3.11/site-packages/torch/lib:/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib" +fi + +# Check if SPACE_ID is set, if so, configure for space +if [ -n "$SPACE_ID" ]; then + echo "Configuring for HuggingFace Space deployment" + if [ -n "$ADMIN_USER_EMAIL" ] && [ -n "$ADMIN_USER_PASSWORD" ]; then + echo "Admin user configured, creating" + WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" uvicorn open_webui.main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips "${FORWARDED_ALLOW_IPS:-*}" & + webui_pid=$! + echo "Waiting for webui to start..." + while ! curl -s "http://localhost:${PORT}/health" > /dev/null; do + sleep 1 + done + echo "Creating admin user..." + curl \ + -X POST "http://localhost:${PORT}/api/v1/auths/signup" \ + -H "accept: application/json" \ + -H "Content-Type: application/json" \ + -d "{ \"email\": \"${ADMIN_USER_EMAIL}\", \"password\": \"${ADMIN_USER_PASSWORD}\", \"name\": \"Admin\" }" + echo "Shutting down webui..." + kill $webui_pid + fi + + export WEBUI_URL=${SPACE_HOST} +fi + +PYTHON_CMD=$(command -v python3 || command -v python) +UVICORN_WORKERS="${UVICORN_WORKERS:-1}" + +# If script is called with arguments, use them; otherwise use default workers +if [ "$#" -gt 0 ]; then + ARGS=("$@") +else + ARGS=(--workers "$UVICORN_WORKERS") +fi + +# Run uvicorn +WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec "$PYTHON_CMD" -m uvicorn open_webui.main:app \ + --host "$HOST" \ + --port "$PORT" \ + --forwarded-allow-ips "${FORWARDED_ALLOW_IPS:-*}" \ + "${ARGS[@]}" \ No newline at end of file diff --git a/backend/start_windows.bat b/backend/start_windows.bat new file mode 100644 index 0000000000000000000000000000000000000000..c5f96e0e6f8b2c2bb3e602e109880b2382ae28ac --- /dev/null +++ b/backend/start_windows.bat @@ -0,0 +1,51 @@ +:: This method is not recommended, and we recommend you use the `start.sh` file with WSL instead. +@echo off +SETLOCAL ENABLEDELAYEDEXPANSION + +:: Get the directory of the current script +SET "SCRIPT_DIR=%~dp0" +cd /d "%SCRIPT_DIR%" || exit /b + +:: Add conditional Playwright browser installation +IF /I "%WEB_LOADER_ENGINE%" == "playwright" ( + IF "%PLAYWRIGHT_WS_URL%" == "" ( + echo Installing Playwright browsers... + playwright install chromium + playwright install-deps chromium + ) + + python -c "import nltk; nltk.download('punkt_tab')" +) + +SET "KEY_FILE=.webui_secret_key" +IF NOT "%WEBUI_SECRET_KEY_FILE%" == "" ( + SET "KEY_FILE=%WEBUI_SECRET_KEY_FILE%" +) + +IF "%PORT%"=="" SET PORT=8080 +IF "%HOST%"=="" SET HOST=0.0.0.0 +IF "%FORWARDED_ALLOW_IPS%"=="" SET "FORWARDED_ALLOW_IPS='*'" +SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%" +SET "WEBUI_JWT_SECRET_KEY=%WEBUI_JWT_SECRET_KEY%" + +:: Check if WEBUI_SECRET_KEY and WEBUI_JWT_SECRET_KEY are not set +IF "%WEBUI_SECRET_KEY% %WEBUI_JWT_SECRET_KEY%" == " " ( + echo Loading WEBUI_SECRET_KEY from file, not provided as an environment variable. + + IF NOT EXIST "%KEY_FILE%" ( + echo Generating WEBUI_SECRET_KEY + :: Generate a random value to use as a WEBUI_SECRET_KEY in case the user didn't provide one + SET /p WEBUI_SECRET_KEY=>%KEY_FILE% + echo WEBUI_SECRET_KEY generated + ) + + echo Loading WEBUI_SECRET_KEY from %KEY_FILE% + SET /p WEBUI_SECRET_KEY=<%KEY_FILE% +) + +:: Execute uvicorn +SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%" +IF "%UVICORN_WORKERS%"=="" SET UVICORN_WORKERS=1 +uvicorn open_webui.main:app --host "%HOST%" --port "%PORT%" --forwarded-allow-ips %FORWARDED_ALLOW_IPS% --workers %UVICORN_WORKERS% --ws auto +:: For ssl user uvicorn open_webui.main:app --host "%HOST%" --port "%PORT%" --forwarded-allow-ips '*' --ssl-keyfile "key.pem" --ssl-certfile "cert.pem" --ws auto diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..35554ac1d4a9f249a14e48c24a811c9287ca0e5f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,16593 @@ +{ + "name": "open-webui", + "version": "0.9.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "open-webui", + "version": "0.9.2", + "dependencies": { + "@azure/msal-browser": "^4.5.0", + "@codemirror/lang-javascript": "^6.2.2", + "@codemirror/lang-python": "^6.1.6", + "@codemirror/language-data": "^6.5.1", + "@codemirror/theme-one-dark": "^6.1.2", + "@floating-ui/dom": "^1.7.2", + "@huggingface/transformers": "^3.0.0", + "@joplin/turndown-plugin-gfm": "^1.0.62", + "@mediapipe/tasks-vision": "^0.10.17", + "@pyscript/core": "^0.4.32", + "@sveltejs/adapter-node": "^2.0.0", + "@sveltejs/svelte-virtual-list": "^3.0.1", + "@tiptap/core": "^3.0.7", + "@tiptap/extension-bubble-menu": "^3.0.7", + "@tiptap/extension-code": "^3.0.7", + "@tiptap/extension-code-block-lowlight": "^3.0.7", + "@tiptap/extension-drag-handle": "^3.4.5", + "@tiptap/extension-file-handler": "^3.0.7", + "@tiptap/extension-floating-menu": "^3.0.7", + "@tiptap/extension-highlight": "^3.3.0", + "@tiptap/extension-image": "^3.0.7", + "@tiptap/extension-link": "^3.0.7", + "@tiptap/extension-list": "^3.0.7", + "@tiptap/extension-mention": "^3.0.9", + "@tiptap/extension-table": "^3.0.7", + "@tiptap/extension-typography": "^3.0.7", + "@tiptap/extension-youtube": "^3.0.7", + "@tiptap/extensions": "^3.0.7", + "@tiptap/pm": "^3.0.7", + "@tiptap/starter-kit": "^3.0.7", + "@tiptap/suggestion": "^3.4.2", + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-web-links": "^0.12.0", + "@xterm/xterm": "^6.0.0", + "@xyflow/svelte": "^0.1.19", + "alpinejs": "^3.15.0", + "async": "^3.2.5", + "bits-ui": "^2.0.0", + "chart.js": "^4.5.0", + "codemirror": "^6.0.1", + "codemirror-lang-elixir": "^4.0.0", + "codemirror-lang-hcl": "^0.1.0", + "crc-32": "^1.2.2", + "dayjs": "^1.11.10", + "dompurify": "^3.2.6", + "eventsource-parser": "^1.1.2", + "fast-deep-equal": "^3.1.3", + "file-saver": "^2.0.5", + "focus-trap": "^7.6.4", + "fuse.js": "^7.0.0", + "heic2any": "^0.0.4", + "highlight.js": "^11.9.0", + "html-entities": "^2.5.3", + "html2canvas-pro": "^1.5.11", + "i18next": "^23.10.0", + "i18next-browser-languagedetector": "^7.2.0", + "i18next-resources-to-backend": "^1.2.0", + "idb": "^7.1.1", + "js-sha256": "^0.10.1", + "jspdf": "^4.0.0", + "jszip": "^3.10.1", + "katex": "^0.16.22", + "kokoro-js": "^1.1.1", + "leaflet": "^1.9.4", + "lowlight": "^3.3.0", + "mammoth": "^1.11.0", + "marked": "^9.1.0", + "mermaid": "^11.10.1", + "paneforge": "^0.0.6", + "panzoom": "^9.4.3", + "pdfjs-dist": "^5.4.149", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.6.0", + "prosemirror-example-setup": "^1.2.3", + "prosemirror-history": "^1.4.1", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.13.1", + "prosemirror-model": "^1.23.0", + "prosemirror-schema-basic": "^1.2.3", + "prosemirror-schema-list": "^1.5.1", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.7.1", + "prosemirror-view": "^1.34.3", + "pyodide": "^0.28.2", + "shiki": "^4.0.1", + "socket.io-client": "^4.2.0", + "sortablejs": "^1.15.6", + "sql.js": "^1.14.1", + "svelte-sonner": "^0.3.19", + "tippy.js": "^6.3.7", + "turndown": "^7.2.0", + "turndown-plugin-gfm": "^1.0.2", + "undici": "^7.3.0", + "uuid": "^9.0.1", + "vega": "^6.2.0", + "vega-lite": "^6.4.1", + "vite-plugin-static-copy": "^2.2.0", + "xlsx": "^0.18.5", + "y-prosemirror": "^1.3.7", + "y-protocols": "^1.0.7", + "yaml": "^2.7.1", + "yjs": "^13.6.27" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "3.2.2", + "@sveltejs/adapter-static": "^3.0.2", + "@sveltejs/kit": "^2.5.27", + "@sveltejs/vite-plugin-svelte": "^4.0.0", + "@tailwindcss/container-queries": "^0.1.1", + "@tailwindcss/postcss": "^4.0.0", + "@tailwindcss/typography": "^0.5.13", + "@typescript-eslint/eslint-plugin": "^8.31.1", + "@typescript-eslint/parser": "^8.31.1", + "cypress": "^13.15.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-cypress": "^3.4.0", + "eslint-plugin-svelte": "^2.45.1", + "i18next-parser": "^9.0.1", + "postcss": "^8.4.31", + "prettier": "^3.3.3", + "prettier-plugin-svelte": "^3.2.6", + "sass-embedded": "^1.81.0", + "svelte": "^5.53.10", + "svelte-check": "^4.0.0", + "svelte-confetti": "^2.3.2", + "tailwindcss": "^4.0.0", + "tslib": "^2.4.1", + "typescript": "^5.5.4", + "vite": "^5.4.21", + "vitest": "^1.6.1" + }, + "engines": { + "node": ">=18.13.0 <=22.x.x", + "npm": ">=6.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@azure/msal-browser": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.29.1.tgz", + "integrity": "sha512-1Vrt27du1cl4QHkzLc6L4aeXqliPIDIs5l/1I4hWWMXkXccY/EznJT1+pBdoVze0azTAI8sCyq5B4cBVYG1t9w==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.16.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "15.16.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.16.1.tgz", + "integrity": "sha512-qxUG9TCl+TVSSX58onVDHDWrvT5CE0+NeeUAbkQqaESpSm79u5IePLnPWMMjCUnUR2zJd4+Bt9vioVRzLmJb2g==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", + "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", + "devOptional": true, + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-12.0.0.tgz", + "integrity": "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "12.0.0", + "@chevrotain/types": "12.0.0" + } + }, + "node_modules/@chevrotain/gast": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-12.0.0.tgz", + "integrity": "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "12.0.0" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-12.0.0.tgz", + "integrity": "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-12.0.0.tgz", + "integrity": "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-12.0.0.tgz", + "integrity": "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==", + "license": "Apache-2.0" + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz", + "integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-angular": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-angular/-/lang-angular-0.1.4.tgz", + "integrity": "sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.3" + } + }, + "node_modules/@codemirror/lang-cpp": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz", + "integrity": "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/cpp": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-go": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-go/-/lang-go-6.0.1.tgz", + "integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/go": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", + "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.12" + } + }, + "node_modules/@codemirror/lang-java": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.2.tgz", + "integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/java": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", + "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-jinja": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-jinja/-/lang-jinja-6.0.0.tgz", + "integrity": "sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.4.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-less": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-less/-/lang-less-6.0.2.tgz", + "integrity": "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-liquid": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.3.2.tgz", + "integrity": "sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.1" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz", + "integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-php": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.2.tgz", + "integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/php": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz", + "integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/python": "^1.1.4" + } + }, + "node_modules/@codemirror/lang-rust": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz", + "integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/rust": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-sass": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sass/-/lang-sass-6.0.2.tgz", + "integrity": "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/sass": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-sql": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz", + "integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-vue": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-vue/-/lang-vue-0.1.3.tgz", + "integrity": "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.1" + } + }, + "node_modules/@codemirror/lang-wast": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-6.0.2.tgz", + "integrity": "sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-xml": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", + "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz", + "integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.0.0", + "@lezer/yaml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz", + "integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/language-data": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.5.2.tgz", + "integrity": "sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-angular": "^0.1.0", + "@codemirror/lang-cpp": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-go": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-java": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/lang-jinja": "^6.0.0", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-less": "^6.0.0", + "@codemirror/lang-liquid": "^6.0.0", + "@codemirror/lang-markdown": "^6.0.0", + "@codemirror/lang-php": "^6.0.0", + "@codemirror/lang-python": "^6.0.0", + "@codemirror/lang-rust": "^6.0.0", + "@codemirror/lang-sass": "^6.0.0", + "@codemirror/lang-sql": "^6.0.0", + "@codemirror/lang-vue": "^0.1.1", + "@codemirror/lang-wast": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", + "@codemirror/lang-yaml": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/legacy-modes": "^6.4.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz", + "integrity": "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.5.tgz", + "integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz", + "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.40.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.40.0.tgz", + "integrity": "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cypress/request": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.10.tgz", + "integrity": "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~4.0.4", + "http-signature": "~1.4.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "~6.14.1", + "safe-buffer": "^5.1.2", + "tough-cookie": "^5.0.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/request/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@huggingface/jinja": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.6.tgz", + "integrity": "sha512-MyMWyLnjqo+KRJYSH7oWNbsOn5onuIvfXYPcc0WOGxU0eHUV7oAYUoQTl2BMdu7ml+ea/bu11UM+EshbeHwtIA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/transformers": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.8.1.tgz", + "integrity": "sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==", + "license": "Apache-2.0", + "dependencies": { + "@huggingface/jinja": "^0.5.3", + "onnxruntime-node": "1.21.0", + "onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4", + "sharp": "^0.34.1" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", + "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "mlly": "^1.8.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@internationalized/date": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@joplin/turndown-plugin-gfm": { + "version": "1.0.64", + "resolved": "https://registry.npmjs.org/@joplin/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.64.tgz", + "integrity": "sha512-8GJ7f9OenE3zkSVII5B6qzIkvgF7C/a20gaASEjM6jWPLPJFFQ2nQ3Ou/kXH1mPUTs9dC9VYs8QXVPvZabKXBQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@lezer/common": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", + "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", + "license": "MIT" + }, + "node_modules/@lezer/cpp": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.5.tgz", + "integrity": "sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/css": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.1.tgz", + "integrity": "sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/go": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz", + "integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/java": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz", + "integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.3.tgz", + "integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@lezer/php": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.5.tgz", + "integrity": "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.1.0" + } + }, + "node_modules/@lezer/python": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz", + "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/rust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz", + "integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/sass": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lezer/sass/-/sass-1.1.0.tgz", + "integrity": "sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/xml": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", + "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz", + "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.32", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.32.tgz", + "integrity": "sha512-3tiAZnmKloYnRXYoO3dKltTUGnqeCwzC4lV03uY0vCsE+aveJTyEVQyZHOlQGQNsjK+gRHzkf9q08C99Qm2K0Q==", + "license": "Apache-2.0" + }, + "node_modules/@mermaid-js/parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.0.tgz", + "integrity": "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==", + "license": "MIT", + "dependencies": { + "langium": "^4.0.0" + } + }, + "node_modules/@mixmark-io/domino": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", + "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==", + "license": "BSD-2-Clause" + }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.97.tgz", + "integrity": "sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.97", + "@napi-rs/canvas-darwin-arm64": "0.1.97", + "@napi-rs/canvas-darwin-x64": "0.1.97", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.97", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.97", + "@napi-rs/canvas-linux-arm64-musl": "0.1.97", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.97", + "@napi-rs/canvas-linux-x64-gnu": "0.1.97", + "@napi-rs/canvas-linux-x64-musl": "0.1.97", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.97", + "@napi-rs/canvas-win32-x64-msvc": "0.1.97" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.97.tgz", + "integrity": "sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.97.tgz", + "integrity": "sha512-ok+SCEF4YejcxuJ9Rm+WWunHHpf2HmiPxfz6z1a/NFQECGXtsY7A4B8XocK1LmT1D7P174MzwPF9Wy3AUAwEPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.97.tgz", + "integrity": "sha512-PUP6e6/UGlclUvAQNnuXCcnkpdUou6VYZfQOQxExLp86epOylmiwLkqXIvpFmjoTEDmPmXrI+coL/9EFU1gKPA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.97.tgz", + "integrity": "sha512-XyXH2L/cic8eTNtbrXCcvqHtMX/nEOxN18+7rMrAM2XtLYC/EB5s0wnO1FsLMWmK+04ZSLN9FBGipo7kpIkcOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.97.tgz", + "integrity": "sha512-Kuq/M3djq0K8ktgz6nPlK7Ne5d4uWeDxPpyKWOjWDK2RIOhHVtLtyLiJw2fuldw7Vn4mhw05EZXCEr4Q76rs9w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.97.tgz", + "integrity": "sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.97.tgz", + "integrity": "sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.97.tgz", + "integrity": "sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.97.tgz", + "integrity": "sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.97.tgz", + "integrity": "sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.97.tgz", + "integrity": "sha512-sWtD2EE3fV0IzN+iiQUqr/Q1SwqWhs2O1FKItFlxtdDkikpEj5g7DKQpY3x55H/MAOnL8iomnlk3mcEeGiUMoQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@pyscript/core": { + "version": "0.4.56", + "resolved": "https://registry.npmjs.org/@pyscript/core/-/core-0.4.56.tgz", + "integrity": "sha512-pdjzc16C8zAGzFRP8qVy2lmrEdRH9khCOedPRlDr/5PG5tYEquPggbO1hLb/eUpJH6r3jP/uhW59vuG7yuKwqw==", + "license": "APACHE-2.0", + "dependencies": { + "@ungap/with-resolvers": "^0.1.0", + "basic-devtools": "^0.1.6", + "polyscript": "^0.13.10", + "sticky-module": "^0.1.1", + "to-json-callback": "^0.1.1", + "type-checked-collections": "^0.1.7" + } + }, + "node_modules/@remirror/core-constants": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", + "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", + "license": "MIT" + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "25.0.8", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz", + "integrity": "sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz", + "integrity": "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.0.2", + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.0.2.tgz", + "integrity": "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.2.tgz", + "integrity": "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.2.tgz", + "integrity": "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.0.2.tgz", + "integrity": "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.0.2.tgz", + "integrity": "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz", + "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@svelte-put/shortcut": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@svelte-put/shortcut/-/shortcut-3.1.1.tgz", + "integrity": "sha512-2L5EYTZXiaKvbEelVkg5znxqvfZGZai3m97+cAiUBhLZwXnGtviTDpHxOoZBsqz41szlfRMcamW/8o0+fbW3ZQ==", + "license": "MIT", + "peerDependencies": { + "svelte": "^3.55.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-auto": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.2.2.tgz", + "integrity": "sha512-Mso5xPCA8zgcKrv+QioVlqMZkyUQ5MjDJiEPuG/Z7cV/5tmwV7LmcVWk5tZ+H0NCOV1x12AsoSpt/CwFwuVXMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-meta-resolve": "^4.1.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/adapter-node": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-2.1.2.tgz", + "integrity": "sha512-ZfVY5buBclWHoBT+RbkMUViJGEIZ3IfT/0Hvhlgp+qC3LRZwp+wS1Zsw5dgkB2sFDZXctbLNXJtwlkjSp1mw0g==", + "license": "MIT", + "dependencies": { + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "rollup": "^4.8.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/adapter-static": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", + "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.58.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.58.0.tgz", + "integrity": "sha512-kT9GCN8yJTkCK1W+Gi/bvGooWAM7y7WXP+yd+rf6QOIjyoK1ERPrMwSufXJUNu2pMWIqruhFvmz+LbOqsEmKmA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", + "cookie": "^0.6.0", + "devalue": "^5.6.4", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "set-cookie-parser": "^3.0.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.3.3 || ^6.0.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/svelte-virtual-list": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/svelte-virtual-list/-/svelte-virtual-list-3.0.1.tgz", + "integrity": "sha512-aF9TptS7NKKS7/TqpsxQBSDJ9Q0XBYzBehCeIC5DzdMEgrJZpIYao9LRLnyyo6SVodpapm2B7FE/Lj+FSA5/SQ==", + "license": "LIL" + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.4.tgz", + "integrity": "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==", + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0", + "debug": "^4.3.7", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.12", + "vitefu": "^1.0.3" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0-next.96 || ^5.0.0", + "vite": "^5.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz", + "integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0", + "svelte": "^5.0.0-next.96 || ^5.0.0", + "vite": "^5.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", + "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/container-queries": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz", + "integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.2.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz", + "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "postcss": "^8.5.6", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tiptap/core": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.2.tgz", + "integrity": "sha512-zKW4LqZt+aNdvz9o4R0/j+D+gfhwzuFItwh7wbqz8g8bWi0jaV95VybeVFVKeg/KGTc3sAa4mm+hGgvgrY+Gvg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.20.2.tgz", + "integrity": "sha512-tkzZzBdwu8pP6pRfYjGanyj4aMSdcr4TS/Z9dcFxA8SYhmBXB4FYTbURME8Eg+n5VIOh1/2c4R2mbOkfQd4GtQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.20.2.tgz", + "integrity": "sha512-NLqh6ewHcDDPveTCL2f6BQcsDI5lubNjiyzvuYr0ZO9AV5Fqw8TkYwoKNijiYlgGRtm+pZLhMnf45gbLJQoymg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.20.2.tgz", + "integrity": "sha512-Np9jHUBWPnFJMy3Qhup3udARJQnWkbwVxVaHlJdgEy5Hfy/HE/EnItXAxXeFjVZ57cl+kJYamdn1t8VfMOV3mg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2", + "@tiptap/pm": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.20.2.tgz", + "integrity": "sha512-LHmp945at3YYl2VPIg0bopyJioi52xK+YRurOz8A440EgCdnAkFa0UDGHxK/e4Y0R2y3xbPl+VBl3HzZjXPFuw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.20.2.tgz", + "integrity": "sha512-4mwWtt88Cl7PT5IbQpwigPBlNmB3JUiOchPXJIfbGu7wUxAk7a37vhn8ptiM2IGKpBFum/1PZFUI+Ik7TLIZLg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.20.2.tgz", + "integrity": "sha512-BpClHuUrOYArL8skifo6RSlBiAVDYkGkq22zVb9lNfrrRqJIlPhwDI8tCZh1sHbgDQPukb4lE5VMPnsEv5M1tw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2", + "@tiptap/pm": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-code-block-lowlight": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-3.20.2.tgz", + "integrity": "sha512-9/HZyXlKWD+MoxJX0TvIRWxOuN3UO0gNbyQrQlxKjT/3V3rmSqkzVs1XLBFYbR87LCovD9HwZNM30r4y6s4eYw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2", + "@tiptap/extension-code-block": "^3.20.2", + "@tiptap/pm": "^3.20.2", + "highlight.js": "^11", + "lowlight": "^2 || ^3" + } + }, + "node_modules/@tiptap/extension-collaboration": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-collaboration/-/extension-collaboration-3.20.2.tgz", + "integrity": "sha512-VV15cmp2PrqsBYJLiV8LyWU9HmbBdVXNx2XotTCGrz6FWKfcj2N2UN2cjw+HB5/rJiMHa0nQU3oDEKlh4XjSHg==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2", + "@tiptap/pm": "^3.20.2", + "@tiptap/y-tiptap": "^3.0.2", + "yjs": "^13" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.20.2.tgz", + "integrity": "sha512-HHlpUs1Y22YwDmJ0cmTGPrFPuHk8Q2wvYZeG5eFOEeBu7t4IiCU114slvIR+yrDZrzpPmwzMb8H71scR+moizg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-drag-handle": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-drag-handle/-/extension-drag-handle-3.20.2.tgz", + "integrity": "sha512-P6gFl5aw5E7tVg7hkkeQ8A/D8yozj03lx05AApeQjhqPK3MTl/5xkHPUsjeLAbVvsr00Mdddow+dReYVEtb70A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.6.13" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2", + "@tiptap/extension-collaboration": "^3.20.2", + "@tiptap/extension-node-range": "^3.20.2", + "@tiptap/pm": "^3.20.2", + "@tiptap/y-tiptap": "^3.0.2" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.20.2.tgz", + "integrity": "sha512-LpBZOOgTrFWkYneOWOd0xyB7HUGIZqrgEhL+Beohzxkx63uNRC3PxFAAXhju6wxcvQ49e/WMg++Z8EDwHb6f2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-file-handler": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-file-handler/-/extension-file-handler-3.20.2.tgz", + "integrity": "sha512-+Jwqe1jKWPTH96/xaXZrwZxDgVm/8bcartLlkE06DN6T7WzoPHnMUlb5whxRlgNNDIwVmzVXvlFKbgkPBxCv/g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2", + "@tiptap/extension-text-style": "^3.20.2", + "@tiptap/pm": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.20.2.tgz", + "integrity": "sha512-Ev9QuNmV/A2fMuu+XpEy1W+u8FOu75S7GVPtS+cYRQc/TYTKaxha0+j0eYvJzKLzKRguJNRlmPBDHGN7MnSY/w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "^3.20.2", + "@tiptap/pm": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.20.2.tgz", + "integrity": "sha512-IfQuD5XctZa+Xxy3mdjo9NTYbiMFqGPuzyh2ypHUqyuvIwxOIRhxTFaCijOGVYn1g3BH8nzGMhZ5rnZ48zIb6Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.20.2.tgz", + "integrity": "sha512-TdjJ54483D1PsGLOBQBvzTqIKc027hixP/xFul9KfXIpGG+YGqX0U2RO3oUyuv32fbU1ZVLMDfEBzR/ropsyDg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.20.2.tgz", + "integrity": "sha512-XKpSEMcER00yfMXiASPpCHHa4Tw6G78AUELFt2PiS0tTWdxNpXZ8y29glyR4LO5eBxGjF1jncO49T1DzDh48TQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-highlight": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.20.2.tgz", + "integrity": "sha512-bKYXnGlXXwoEYAByws6VNsm0720YRe2nRMZr+WnwTlyaOzUyaGl1GKYmsbsZaOsZm9VPWKiu0T7y4UMjLvweSA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.20.2.tgz", + "integrity": "sha512-1Ds1xwl4XKWzXhZ8jG+G04BepExxuwtJuw+xbdMXkTD3YDE8KmbSrUIiLpA60Zq4qZmWIrX57F7mOBGaExyfCw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2", + "@tiptap/pm": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-image": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.20.2.tgz", + "integrity": "sha512-STo7T3NQ1TcF93NXRQDhb5YkepBRpYHY54yfBUmHl5cygYZzOMaGlM0nh8NeX54mh3wJ6+nxpApuM3Jbmg0I+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.20.2.tgz", + "integrity": "sha512-VAIXeJMx4g6WKqqNm8PYzoFrJaRNKLzLtqUXqYozKxnJLpF2HjsIrnBJV9PM+1FKK2Tic1UuowF4OI/6d162SA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.20.2.tgz", + "integrity": "sha512-vnC72CFMUiCJuAt7Hi4T/hKvbY4DqBjqo9G6dkBfNJHXHmqGiGKvkgzm1m7P/R1EX1XYk8nifeCpW6q2uliFRQ==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2", + "@tiptap/pm": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.20.2.tgz", + "integrity": "sha512-x9h1fDeaLah60WJTb6517nRbAKcbdBsTpmglqxQ9c827PUOUyiVEAu2o2cFEuOMw6Htvty4obOKBUgYi8EAgDA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2", + "@tiptap/pm": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.20.2.tgz", + "integrity": "sha512-6L+ZKOqD9jTmE313qFkrIk81jbk8v437zRa5Sa0/hyFMbupsNVKZoZZkzrq5vOkbz2oE0WpsFXIjcYaqAwJhyA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.20.2.tgz", + "integrity": "sha512-SKU+w93E9+TdSWeoJzjFLDbO+XC/QIRQ+PJ6Jruz1BJ6VXILmyVOw3jBwmiJjLo+5h4MNV2D/IHx1P7BpEkUhQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-mention": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-mention/-/extension-mention-3.20.2.tgz", + "integrity": "sha512-J6Y+5cPQZxXlA6Jh55NkxFFItN7iNEzEAOYWUzAuCewErqgziDFyswUK2BuoKyG/vkYcwU2nuAq2a3KGYgPKfA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2", + "@tiptap/pm": "^3.20.2", + "@tiptap/suggestion": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-node-range": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-node-range/-/extension-node-range-3.20.2.tgz", + "integrity": "sha512-jZCs918QlIFYi2jexLgKZnevyz22IBhWnQUHSLD7EfjRq9wOrjhXYkWahMKe7etJyzg+nT5aJLz5eiT12B4oXQ==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2", + "@tiptap/pm": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.20.2.tgz", + "integrity": "sha512-UmPrnvd9/cGcO2QQaESu57kXmkubxmazQmUTgRU2BiLMEWGPPvxnBbJkM/YYmYPFRCs+OTx+XO9ohCD1xqtQcQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.20.2.tgz", + "integrity": "sha512-gTWAUmvCnv7OThFsYdyhacL4TM+sJMC/UeuW+drWaTBbo7dvejzkl4hF5B0ytmF3d/ko1GNr4ldTikYZ2xypMw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.20.2.tgz", + "integrity": "sha512-Jivcc5Hgw2moIDfVoLEuJumDtw38k2mzEMYt3oZQnvE5d0ttXhWWR0LbLm5LBX3oxlc74I3NSi+Q4ixQHiDtvA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-table": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.20.2.tgz", + "integrity": "sha512-kURWr90j3sfuC2AkM73lwoWfeDxj+zWK38gbld58/cHUREoWK9lxFEdeCCZOge6Z97ZQRvqRiEGeQ/X3GmUiYg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2", + "@tiptap/pm": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.20.2.tgz", + "integrity": "sha512-I1rVt6JTi/itgsFqXp+JfxWn9fEewIxiIaaaMUmaCJ6HChQSvYlVWy1B/RixmbCCPaL/FxybEa/Tg9MugyOJYA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-text-style": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.20.2.tgz", + "integrity": "sha512-jvVRtFdEJNjKgIL8wtMg3W4BJMlKyH9aF2jYNU2rf3jA9GJM0rtqtth3jjFnApZoyINtx6jr4o4Ot6+/5d0XYA==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-typography": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-typography/-/extension-typography-3.20.2.tgz", + "integrity": "sha512-aDF9YG2qqw3MR1NuQYvnBiwi0I1SB6oEMlZF4odcvPOdNmbqLtNXz5ap8baPz7XqDn+5B4NC1GMNq8CQeAVA0Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.20.2.tgz", + "integrity": "sha512-oVszIGkRtg8NLhop/t5kco6suWlDUKW9cqhL6wwd19aLztr+tMU/u8+kPG2cjjYZ+XoMZOoKfkOt7he2iU5/0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2" + } + }, + "node_modules/@tiptap/extension-youtube": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-youtube/-/extension-youtube-3.20.2.tgz", + "integrity": "sha512-xks2aPgBDEkDvFq/tnbekJvCPDlAh2Jb8XBFfNhCJc4O5dz5nwALKB713bs1WUAFIVtoXCUqWjf0006TvgGeCA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.2.tgz", + "integrity": "sha512-gzntns6z/kTgwrX89ydc3rNqDsv8D8sAkyl8HE9X+2D9wtdCgNljevIR6MBNcxG7bVm2+XnId1P9YciCZLuefg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2", + "@tiptap/pm": "^3.20.2" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.2.tgz", + "integrity": "sha512-tEMZlLy/6ms41PQtyjmniZ3tTd8elavd8htjYzHLPSHtz11zaYV9YjnmnwHK8gynhcSES1orO9z1U4nT4ZLpqg==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.3.0", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.6.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.13.1", + "prosemirror-menu": "^1.2.4", + "prosemirror-model": "^1.24.1", + "prosemirror-schema-basic": "^1.2.3", + "prosemirror-schema-list": "^1.5.0", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.6.4", + "prosemirror-trailing-node": "^3.0.0", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.38.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.20.2.tgz", + "integrity": "sha512-QQ81QpwecXN3Rg0nWYC1nRCcWxlUtEua1X5j1NYUtY39SScuLbs3mMQ54P+u9ZeX31pzu/kuix5GQ0fw+SApOA==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^3.20.2", + "@tiptap/extension-blockquote": "^3.20.2", + "@tiptap/extension-bold": "^3.20.2", + "@tiptap/extension-bullet-list": "^3.20.2", + "@tiptap/extension-code": "^3.20.2", + "@tiptap/extension-code-block": "^3.20.2", + "@tiptap/extension-document": "^3.20.2", + "@tiptap/extension-dropcursor": "^3.20.2", + "@tiptap/extension-gapcursor": "^3.20.2", + "@tiptap/extension-hard-break": "^3.20.2", + "@tiptap/extension-heading": "^3.20.2", + "@tiptap/extension-horizontal-rule": "^3.20.2", + "@tiptap/extension-italic": "^3.20.2", + "@tiptap/extension-link": "^3.20.2", + "@tiptap/extension-list": "^3.20.2", + "@tiptap/extension-list-item": "^3.20.2", + "@tiptap/extension-list-keymap": "^3.20.2", + "@tiptap/extension-ordered-list": "^3.20.2", + "@tiptap/extension-paragraph": "^3.20.2", + "@tiptap/extension-strike": "^3.20.2", + "@tiptap/extension-text": "^3.20.2", + "@tiptap/extension-underline": "^3.20.2", + "@tiptap/extensions": "^3.20.2", + "@tiptap/pm": "^3.20.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/suggestion": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-3.20.2.tgz", + "integrity": "sha512-iyYknBAugaaaLDxn7NVjoucF9FYvsGtd4KeJNevAKXxuzMmoQlcU1HwJfFXdfmX2EMX2S5SwcKBjc63qdvLZDQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.2", + "@tiptap/pm": "^3.20.2" + } + }, + "node_modules/@tiptap/y-tiptap": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@tiptap/y-tiptap/-/y-tiptap-3.0.2.tgz", + "integrity": "sha512-flMn/YW6zTbc6cvDaUPh/NfLRTXDIqgpBUkYzM74KA1snqQwhOMjnRcnpu4hDFrTnPO6QGzr99vRyXEA7M44WA==", + "license": "MIT", + "peer": true, + "dependencies": { + "lib0": "^0.2.100" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1.7.1", + "prosemirror-state": "^1.2.3", + "prosemirror-view": "^1.9.10", + "y-protocols": "^1.0.1", + "yjs": "^13.5.38" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "license": "MIT" + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sizzle": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz", + "integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/symlink-or-copy": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/symlink-or-copy/-/symlink-or-copy-1.2.2.tgz", + "integrity": "sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", + "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/type-utils": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", + "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", + "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.0", + "@typescript-eslint/types": "^8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", + "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", + "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", + "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", + "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", + "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.0", + "@typescript-eslint/tsconfig-utils": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", + "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", + "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@ungap/with-resolvers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@ungap/with-resolvers/-/with-resolvers-0.1.0.tgz", + "integrity": "sha512-g7f0IkJdPW2xhY7H4iE72DAsIyfuwEFc6JWc2tYFwKDMWWAF699vGjrM348cwQuOXgHpe1gWFe+Eiyjx/ewvvw==", + "license": "ISC" + }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", + "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.1.5" + } + }, + "node_modules/@vue/shared": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz", + "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==", + "license": "MIT" + }, + "node_modules/@webreflection/fetch": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@webreflection/fetch/-/fetch-0.1.5.tgz", + "integrity": "sha512-zCcqCJoNLvdeF41asAK71XPlwSPieeRDsE09albBunJEksuYPYNillKNQjf8p5BqSoTKTuKrW3lUm3MNodUC4g==", + "license": "MIT" + }, + "node_modules/@webreflection/idb-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@webreflection/idb-map/-/idb-map-0.3.2.tgz", + "integrity": "sha512-VLBTx6EUYF/dPdLyyjWWKxQmTWnVXTT1YJekrJUmfGxBcqEVL0Ih2EQptNG/JezkTYgJ0uSTb0yAum/THltBvQ==", + "license": "MIT" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", + "license": "MIT" + }, + "node_modules/@xterm/addon-web-links": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz", + "integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==", + "license": "MIT" + }, + "node_modules/@xterm/xterm": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, + "node_modules/@xyflow/svelte": { + "version": "0.1.39", + "resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-0.1.39.tgz", + "integrity": "sha512-QZ5mzNysvJeJW7DxmqI4Urhhef9tclqtPr7WAS5zQF5Gk6k9INwzey4CYNtEZo8XMj9H8lzgoJRmgMPnJEc1kw==", + "license": "MIT", + "dependencies": { + "@svelte-put/shortcut": "3.1.1", + "@xyflow/system": "0.0.59", + "classcat": "^5.0.4" + }, + "peerDependencies": { + "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.59", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.59.tgz", + "integrity": "sha512-+xgqYhoBv5F10TQx0SiKZR/DcWtuxFYR+e/LluHb7DMtX4SsMDutZWEJ4da4fDco25jZxw5G9fOlmk7MWvYd5Q==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/alpinejs": { + "version": "3.15.8", + "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.8.tgz", + "integrity": "sha512-zxIfCRTBGvF1CCLIOMQOxAyBuqibxSEwS6Jm1a3HGA9rgrJVcjEWlwLcQTVGAWGS8YhAsTRLVrtQ5a5QT9bSSQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "~3.1.1" + } + }, + "node_modules/amator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/amator/-/amator-1.1.0.tgz", + "integrity": "sha512-V5+aH8pe+Z3u/UG3L3pG3BaFQGXAyXHVQDroRwjPHdh08bcUEchAVsU1MCuJSCaU5o60wTK6KaE6te5memzgYw==", + "license": "MIT", + "dependencies": { + "bezier-easing": "^2.0.3" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-devtools": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/basic-devtools/-/basic-devtools-0.1.6.tgz", + "integrity": "sha512-g9zJ63GmdUesS3/Fwv0B5SYX6nR56TQXmGr+wE5PRTNCnGQMYWhUx/nZB/mMWnQJVLPPAp89oxDNlasdtNkW5Q==", + "license": "ISC" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bezier-easing": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz", + "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bits-ui": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.16.3.tgz", + "integrity": "sha512-5hJ5dEhf5yPzkRFcxzgQHScGodeo0gK0MUUXrdLlRHWaBOBGZiacWLG96j/wwFatKwZvouw7q+sn14i0fx3RIg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.1", + "@floating-ui/dom": "^1.7.1", + "esm-env": "^1.1.2", + "runed": "^0.35.1", + "svelte-toolbelt": "^0.10.6", + "tabbable": "^6.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/huntabyte" + }, + "peerDependencies": { + "@internationalized/date": "^3.8.1", + "svelte": "^5.33.0" + } + }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/broccoli-node-api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/broccoli-node-api/-/broccoli-node-api-1.7.0.tgz", + "integrity": "sha512-QIqLSVJWJUVOhclmkmypJJH9u9s/aWH4+FH6Q6Ju5l+Io4dtwqdPUNmDfw40o6sxhbZHhqGujDJuHTML1wG8Yw==", + "dev": true, + "license": "MIT" + }, + "node_modules/broccoli-node-info": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/broccoli-node-info/-/broccoli-node-info-2.2.0.tgz", + "integrity": "sha512-VabSGRpKIzpmC+r+tJueCE5h8k6vON7EIMMWu6d/FyPdtijwLQ7QvzShEw+m3mHoDzUaj/kiZsDYrS8X2adsBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "8.* || >= 10.*" + } + }, + "node_modules/broccoli-output-wrapper": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/broccoli-output-wrapper/-/broccoli-output-wrapper-3.2.5.tgz", + "integrity": "sha512-bQAtwjSrF4Nu0CK0JOy5OZqw9t5U0zzv2555EA/cF8/a8SLDTIetk9UgrtMVw7qKLKdSpOZ2liZNeZZDaKgayw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-extra": "^8.1.0", + "heimdalljs-logger": "^0.1.10", + "symlink-or-copy": "^1.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + } + }, + "node_modules/broccoli-output-wrapper/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/broccoli-output-wrapper/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/broccoli-output-wrapper/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/broccoli-plugin": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/broccoli-plugin/-/broccoli-plugin-4.0.7.tgz", + "integrity": "sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "broccoli-node-api": "^1.7.0", + "broccoli-output-wrapper": "^3.2.5", + "fs-merger": "^3.2.1", + "promise-map-series": "^0.3.0", + "quick-temp": "^0.1.8", + "rimraf": "^3.0.2", + "symlink-or-copy": "^1.3.1" + }, + "engines": { + "node": "10.* || >= 12.*" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cachedir": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chevrotain": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-12.0.0.tgz", + "integrity": "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "12.0.0", + "@chevrotain/gast": "12.0.0", + "@chevrotain/regexp-to-ast": "12.0.0", + "@chevrotain/types": "12.0.0", + "@chevrotain/utils": "12.0.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.4.1.tgz", + "integrity": "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^12.0.0" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/codedent": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/codedent/-/codedent-0.1.2.tgz", + "integrity": "sha512-qEqzcy5viM3UoCN0jYHZeXZoyd4NZQzYFg0kOBj8O1CgoGG9WYYTF+VeQRsN0OSKFjF3G1u4WDUOtOsWEx6N2w==", + "license": "ISC", + "dependencies": { + "plain-tag": "^0.1.3" + } + }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/codemirror-lang-elixir": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/codemirror-lang-elixir/-/codemirror-lang-elixir-4.0.1.tgz", + "integrity": "sha512-z6W/XB4b7TZrp9EZYBGVq93vQfvKbff+1iM8YZaVErL0dguBAeLmVRlEv1NuDZHOP1qjJ3NwyibkUkNWn7q9VQ==", + "license": "Apache-2.0", + "dependencies": { + "@codemirror/language": "^6.0.0", + "lezer-elixir": "^1.0.0" + } + }, + "node_modules/codemirror-lang-hcl": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/codemirror-lang-hcl/-/codemirror-lang-hcl-0.1.0.tgz", + "integrity": "sha512-duwKEaQDhkJWad4YQ9pv4282BS6hCdR+gS/qTAj3f9bypXNNZ42bIN43h9WK3DjyZRENtVlUQdrQM1sA44wHmA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/coincident": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/coincident/-/coincident-1.2.3.tgz", + "integrity": "sha512-Uxz3BMTWIslzeWjuQnizGWVg0j6khbvHUQ8+5BdM7WuJEm4ALXwq3wluYoB+uF68uPBz/oUOeJnYURKyfjexlA==", + "license": "ISC", + "dependencies": { + "@ungap/structured-clone": "^1.2.0", + "@ungap/with-resolvers": "^0.1.0", + "gc-hook": "^0.3.1", + "proxy-target": "^3.0.2" + }, + "optionalDependencies": { + "ws": "^8.16.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorjs.io": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", + "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/core-js": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", + "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cypress": { + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz", + "integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@cypress/request": "^3.0.6", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.7.1", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "ci-info": "^4.0.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.5.3", + "supports-color": "^8.1.1", + "tmp": "~0.2.3", + "tree-kill": "1.2.2", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + } + }, + "node_modules/cytoscape": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", + "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo-projection": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz", + "integrity": "sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==", + "license": "ISC", + "dependencies": { + "commander": "7", + "d3-array": "1 - 3", + "d3-geo": "1.12.0 - 3" + }, + "bin": { + "geo2svg": "bin/geo2svg.js", + "geograticule": "bin/geograticule.js", + "geoproject": "bin/geoproject.js", + "geoquantize": "bin/geoquantize.js", + "geostitch": "bin/geostitch.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo-projection/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, + "node_modules/devalue": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", + "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", + "license": "BSD-2-Clause" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz", + "integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/duck": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "license": "BSD", + "dependencies": { + "underscore": "^1.13.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ensure-posix-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz", + "integrity": "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/eol": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", + "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-cypress": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-3.6.0.tgz", + "integrity": "sha512-7IAMcBbTVu5LpWeZRn5a9mQ30y4hKp3AfTz+6nSD/x/7YyLMoBI6X7XjDLYI6zFvuy4Q4QVGl563AGEXGW/aSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "globals": "^13.20.0" + }, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "2.46.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", + "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@jridgewell/sourcemap-codec": "^1.4.15", + "eslint-compat-utils": "^0.5.1", + "esutils": "^2.0.3", + "known-css-properties": "^0.35.0", + "postcss": "^8.4.38", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^6.0.0", + "postcss-selector-parser": "^6.1.0", + "semver": "^7.6.2", + "svelte-eslint-parser": "^0.43.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-svelte/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrap": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", + "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@typescript-eslint/types": "^8.2.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/eventsource-parser": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz", + "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==", + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatbuffers": { + "version": "25.9.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", + "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", + "license": "Apache-2.0" + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "license": "MIT", + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs-merger": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/fs-merger/-/fs-merger-3.2.1.tgz", + "integrity": "sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "broccoli-node-api": "^1.7.0", + "broccoli-node-info": "^2.1.0", + "fs-extra": "^8.0.1", + "fs-tree-diff": "^2.0.1", + "walk-sync": "^2.2.0" + } + }, + "node_modules/fs-merger/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-merger/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/fs-merger/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/fs-mkdirp-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", + "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/fs-tree-diff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-tree-diff/-/fs-tree-diff-2.0.1.tgz", + "integrity": "sha512-x+CfAZ/lJHQqwlD64pYM5QxWjzWhSjroaVsr8PW831zOApL55qPibed0c+xebaLWVr2BnHFoHdrwOv8pzt8R5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/symlink-or-copy": "^1.2.0", + "heimdalljs-logger": "^0.1.7", + "object-assign": "^4.1.0", + "path-posix": "^1.0.0", + "symlink-or-copy": "^1.1.8" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuse.js": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", + "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/gc-hook": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.1.tgz", + "integrity": "sha512-E5M+O/h2o7eZzGhzRZGex6hbB3k4NWqO0eA+OzLRLXxhdbYPajZnynPwAtphnh+cRHPwsj5Z80dqZlfI4eK55A==", + "license": "ISC" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getos": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-stream": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.3.tgz", + "integrity": "sha512-fqZVj22LtFJkHODT+M4N1RJQ3TjnnQhfE9GwZI8qXscYarnhpip70poMldRnP8ipQ/w0B621kOhfc53/J9bd/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "is-negated-glob": "^1.0.0", + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "license": "ISC" + }, + "node_modules/gulp-sort": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/gulp-sort/-/gulp-sort-2.0.0.tgz", + "integrity": "sha512-MyTel3FXOdh1qhw1yKhpimQrAmur9q1X0ZigLmCOxouQD+BD3za9/89O+HfbgBQvvh4igEbp0/PUWO+VqGYG1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "through2": "^2.0.1" + } + }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/heic2any": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/heic2any/-/heic2any-0.0.4.tgz", + "integrity": "sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA==", + "license": "MIT" + }, + "node_modules/heimdalljs": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/heimdalljs/-/heimdalljs-0.2.6.tgz", + "integrity": "sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rsvp": "~3.2.1" + } + }, + "node_modules/heimdalljs-logger": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/heimdalljs-logger/-/heimdalljs-logger-0.1.10.tgz", + "integrity": "sha512-pO++cJbhIufVI/fmB/u2Yty3KJD0TqNPecehFae0/eps0hkZ3b4Zc/PezUMOpYuHFQbA7FxHZxa305EhmjLj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^2.2.0", + "heimdalljs": "^0.2.6" + } + }, + "node_modules/heimdalljs-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/heimdalljs-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/heimdalljs/node_modules/rsvp": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.2.1.tgz", + "integrity": "sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/html2canvas-pro": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/html2canvas-pro/-/html2canvas-pro-1.6.7.tgz", + "integrity": "sha512-BzuCTXx0jf2TfnrJrtjMCY1FR9rxlPdy7yLWt9ZMhZm7Ylaw9MLb7agSheqv2mT/ARduBHDAqvJIFlbxrZfyOA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-signature": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.18.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/i18next": { + "version": "23.16.8", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz", + "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.2.tgz", + "integrity": "sha512-6b7r75uIJDWCcCflmbof+sJ94k9UQO4X0YR62oUfqGI/GjCLVzlCwu8TFdRZIqVLzWbzNcmkmhfqKEr4TLz4HQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-parser": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/i18next-parser/-/i18next-parser-9.4.0.tgz", + "integrity": "sha512-SLQJGDj/baBIB9ALmJVXSOXWh3Zn9+wH7J2IuQ4rvx8yuQYpUWitmt8cHFjj6FExjgr8dHfd1SGeQgkowXDO1Q==", + "deprecated": "Project is deprecated, use i18next-cli instead", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "broccoli-plugin": "^4.0.7", + "cheerio": "^1.0.0", + "colors": "^1.4.0", + "commander": "^12.1.0", + "eol": "^0.9.1", + "esbuild": "^0.25.0", + "fs-extra": "^11.2.0", + "gulp-sort": "^2.0.0", + "i18next": "^23.5.1 || ^24.2.0", + "js-yaml": "^4.1.0", + "lilconfig": "^3.1.3", + "rsvp": "^4.8.5", + "sort-keys": "^5.0.0", + "typescript": "^5.0.4", + "vinyl": "^3.0.0", + "vinyl-fs": "^4.0.0" + }, + "bin": { + "i18next": "bin/cli.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || ^22.0.0", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/i18next-parser/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/i18next-parser/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/i18next-resources-to-backend": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.1.tgz", + "integrity": "sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/immutable": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "license": "MIT" + }, + "node_modules/is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-sha256": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.10.1.tgz", + "integrity": "sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jspdf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/katex": { + "version": "0.16.38", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.38.tgz", + "integrity": "sha512-cjHooZUmIAUmDsHBN+1n8LaZdpmbj03LtYeYPyuYB7OuloiaeaV6N4LcfjcnHVzGWjVQmKrxxTrpDcmSzEZQwQ==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", + "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/kokoro-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/kokoro-js/-/kokoro-js-1.2.1.tgz", + "integrity": "sha512-oq0HZJWis3t8lERkMJh84WLU86dpYD0EuBPtqYnLlQzyFP1OkyBRDcweAqCfhNOpltyN9j/azp1H6uuC47gShw==", + "license": "Apache-2.0", + "dependencies": { + "@huggingface/transformers": "^3.5.1", + "phonemizer": "^1.2.1" + } + }, + "node_modules/langium": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.2.tgz", + "integrity": "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==", + "license": "MIT", + "dependencies": { + "@chevrotain/regexp-to-ast": "~12.0.0", + "chevrotain": "~12.0.0", + "chevrotain-allstar": "~0.4.1", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.1.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "> 0.8" + } + }, + "node_modules/lead": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lezer-elixir": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/lezer-elixir/-/lezer-elixir-1.1.3.tgz", + "integrity": "sha512-Ymc58/WhxdZS9yEOlnKbF3rdeBdFcPm4OEm26KMqA1Za9vztXi7I5qwGw1KxYmm3Nv0iDHq//EQyBwSEzKG9Mg==", + "license": "Apache-2.0", + "dependencies": { + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/lib0": { + "version": "0.2.117", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", + "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", + "license": "MIT", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "devOptional": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/linkifyjs": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", + "license": "MIT" + }, + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lop": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", + "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", + "license": "BSD-2-Clause", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lowlight": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", + "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "highlight.js": "~11.11.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mammoth": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz", + "integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==", + "license": "BSD-2-Clause", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.2", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/mammoth/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/mammoth/node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, + "node_modules/mammoth/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/marked": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", + "integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/matcher-collection": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-2.0.1.tgz", + "integrity": "sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@types/minimatch": "^3.0.3", + "minimatch": "^3.0.2" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/matcher-collection/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/matcher-collection/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/matcher-collection/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.14.0.tgz", + "integrity": "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.1.0", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.1", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.19", + "dompurify": "^3.3.1", + "katex": "^0.16.25", + "khroma": "^2.1.0", + "lodash-es": "^4.17.23", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, + "node_modules/mermaid/node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/mermaid/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mktemp": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mktemp/-/mktemp-2.0.2.tgz", + "integrity": "sha512-Q9wJ/xhzeD9Wua1MwDN2v3ah3HENsUVSlzzL9Qw149cL9hHZkXtQGl3Eq36BbdLV+/qUwaP1WtJQ+H/+Oxso8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || 22 || 24" + } + }, + "node_modules/mlly": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.1.tgz", + "integrity": "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.7.tgz", + "integrity": "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ngraph.events": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.4.0.tgz", + "integrity": "sha512-NeDGI4DSyjBNBRtA86222JoYietsmCXbs8CEB0dZ51Xeh4lhVl1y3wpWLumczvnha8sFQIW4E0vvVWwgmX2mGw==", + "license": "BSD-3-Clause" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, + "node_modules/node-readable-to-web-readable-stream": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz", + "integrity": "sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==", + "license": "MIT", + "optional": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz", + "integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/onnxruntime-common": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz", + "integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==", + "license": "MIT" + }, + "node_modules/onnxruntime-node": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.21.0.tgz", + "integrity": "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "global-agent": "^3.0.0", + "onnxruntime-common": "1.21.0", + "tar": "^7.0.1" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.22.0-dev.20250409-89f8206ba4", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.22.0-dev.20250409-89f8206ba4.tgz", + "integrity": "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==", + "license": "MIT", + "dependencies": { + "flatbuffers": "^25.1.24", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, + "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { + "version": "1.22.0-dev.20250409-89f8206ba4", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.22.0-dev.20250409-89f8206ba4.tgz", + "integrity": "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==", + "license": "MIT" + }, + "node_modules/option": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", + "license": "BSD-2-Clause" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/paneforge": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/paneforge/-/paneforge-0.0.6.tgz", + "integrity": "sha512-jYeN/wdREihja5c6nK3S5jritDQ+EbCqC5NrDo97qCZzZ9GkmEcN5C0ZCjF4nmhBwkDKr6tLIgz4QUKWxLXjAw==", + "license": "MIT", + "dependencies": { + "nanoid": "^5.0.4" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.1" + } + }, + "node_modules/panzoom": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/panzoom/-/panzoom-9.4.3.tgz", + "integrity": "sha512-xaxCpElcRbQsUtIdwlrZA90P90+BHip4Vda2BC8MEb4tkI05PmR6cKECdqUCZ85ZvBHjpI9htJrZBxV5Gp/q/w==", + "license": "MIT", + "dependencies": { + "amator": "^1.1.0", + "ngraph.events": "^1.2.2", + "wheel": "^1.0.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-posix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", + "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/pdfjs-dist": { + "version": "5.5.207", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.5.207.tgz", + "integrity": "sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0 || >=22.13.0 || >=24" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.95", + "node-readable-to-web-readable-stream": "^0.4.2" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/phonemizer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/phonemizer/-/phonemizer-1.2.1.tgz", + "integrity": "sha512-v0KJ4mi2T4Q7eJQ0W15Xd4G9k4kICSXE8bpDeJ8jisL4RyJhNWsweKTOi88QXFc4r4LZlz5jVL5lCHhkpdT71A==", + "license": "Apache-2.0" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/plain-tag": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/plain-tag/-/plain-tag-0.1.3.tgz", + "integrity": "sha512-yyVAOFKTAElc7KdLt2+UKGExNYwYb/Y/WE9i+1ezCQsJE8gbKSjewfpRqK2nQgZ4d4hhAAGgDCOcIZVilqE5UA==", + "license": "ISC" + }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, + "node_modules/polyscript": { + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/polyscript/-/polyscript-0.13.10.tgz", + "integrity": "sha512-lRbN48QNfUnUBa81J/0GR4A2FZlB+Qi9m46VE7J8r/Kcx5FopDulT1Z/BFiwUG+xYswUscuVgYND852nq6x2gA==", + "license": "APACHE-2.0", + "dependencies": { + "@ungap/structured-clone": "^1.2.0", + "@ungap/with-resolvers": "^0.1.0", + "@webreflection/fetch": "^0.1.5", + "@webreflection/idb-map": "^0.3.1", + "basic-devtools": "^0.1.6", + "codedent": "^0.1.2", + "coincident": "^1.2.3", + "gc-hook": "^0.3.1", + "html-escaper": "^3.0.3", + "proxy-target": "^3.0.2", + "sticky-module": "^0.1.1", + "to-json-callback": "^0.1.1" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz", + "integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/promise-map-series": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/promise-map-series/-/promise-map-series-0.3.0.tgz", + "integrity": "sha512-3npG2NGhTc8BWBolLLf8l/92OxMGaRLbqvIh9wjCHhDXNvk4zsxaTaCpiCunW09qWPrN2zeNSNwRLVBrQQtutA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "10.* || >= 12.*" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/prosemirror-changeset": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz", + "integrity": "sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-collab": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-example-setup": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-example-setup/-/prosemirror-example-setup-1.2.3.tgz", + "integrity": "sha512-+hXZi8+xbFvYM465zZH3rdZ9w7EguVKmUYwYLZjIJIjPK+I0nPTwn8j0ByW2avchVczRwZmOJGNvehblyIerSQ==", + "license": "MIT", + "dependencies": { + "prosemirror-commands": "^1.0.0", + "prosemirror-dropcursor": "^1.0.0", + "prosemirror-gapcursor": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-inputrules": "^1.0.0", + "prosemirror-keymap": "^1.0.0", + "prosemirror-menu": "^1.0.0", + "prosemirror-schema-list": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-markdown": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz", + "integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==", + "license": "MIT", + "dependencies": { + "@types/markdown-it": "^14.0.0", + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-menu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.0.tgz", + "integrity": "sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==", + "license": "MIT", + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.4", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", + "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz", + "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-trailing-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", + "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", + "license": "MIT", + "dependencies": { + "@remirror/core-constants": "3.0.0", + "escape-string-regexp": "^4.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1.22.1", + "prosemirror-state": "^1.4.2", + "prosemirror-view": "^1.33.8" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz", + "integrity": "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.41.6", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.6.tgz", + "integrity": "sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.20.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, + "node_modules/protobufjs": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", + "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-target": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/proxy-target/-/proxy-target-3.0.2.tgz", + "integrity": "sha512-FFE1XNwXX/FNC3/P8HiKaJSy/Qk68RitG/QEcLy/bVnTAPlgTAWPZKh0pARLAnpfXQPKyalBhk009NRTgsk8vQ==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pyodide": { + "version": "0.28.3", + "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.28.3.tgz", + "integrity": "sha512-rtCsyTU55oNGpLzSVuAd55ZvruJDEX8o6keSdWKN9jPeBVSNlynaKFG7eRqkiIgU7i2M6HEgYtm0atCEQX3u4A==", + "license": "MPL-2.0", + "dependencies": { + "ws": "^8.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-temp": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/quick-temp/-/quick-temp-0.1.9.tgz", + "integrity": "sha512-yI0h7tIhKVObn03kD+Ln9JFi4OljD28lfaOsTdfpTR0xzrhGOod+q66CjGafUqYX2juUfT9oHIGrTBBo22mkRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mktemp": "^2.0.1", + "rimraf": "^5.0.10", + "underscore.string": "~3.3.6" + } + }, + "node_modules/quick-temp/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/quick-temp/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/quick-temp/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/quick-temp/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/quick-temp/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true, + "license": "ISC" + }, + "node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "value-or-function": "^4.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "6.* || >= 7.*" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/runed": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz", + "integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==", + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "esm-env": "^1.0.0", + "lz-string": "^1.5.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.21.0", + "svelte": "^5.7.0" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + } + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", + "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-embedded": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.98.0.tgz", + "integrity": "sha512-Do7u6iRb6K+lrllcTkB1BXcHwOxcKe3rEfOF/GcCLE2w3WpddakRAosJOHFUR37DpsvimQXEt5abs3NzUjEIqg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^2.5.0", + "colorjs.io": "^0.5.0", + "immutable": "^5.1.5", + "rxjs": "^7.4.0", + "supports-color": "^8.1.1", + "sync-child-process": "^1.0.2", + "varint": "^6.0.0" + }, + "bin": { + "sass": "dist/bin/sass.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "optionalDependencies": { + "sass-embedded-all-unknown": "1.98.0", + "sass-embedded-android-arm": "1.98.0", + "sass-embedded-android-arm64": "1.98.0", + "sass-embedded-android-riscv64": "1.98.0", + "sass-embedded-android-x64": "1.98.0", + "sass-embedded-darwin-arm64": "1.98.0", + "sass-embedded-darwin-x64": "1.98.0", + "sass-embedded-linux-arm": "1.98.0", + "sass-embedded-linux-arm64": "1.98.0", + "sass-embedded-linux-musl-arm": "1.98.0", + "sass-embedded-linux-musl-arm64": "1.98.0", + "sass-embedded-linux-musl-riscv64": "1.98.0", + "sass-embedded-linux-musl-x64": "1.98.0", + "sass-embedded-linux-riscv64": "1.98.0", + "sass-embedded-linux-x64": "1.98.0", + "sass-embedded-unknown-all": "1.98.0", + "sass-embedded-win32-arm64": "1.98.0", + "sass-embedded-win32-x64": "1.98.0" + } + }, + "node_modules/sass-embedded-all-unknown": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.98.0.tgz", + "integrity": "sha512-6n4RyK7/1mhdfYvpP3CClS3fGoYqDvRmLClCESS6I7+SAzqjxvGG6u5Fo+cb1nrPNbbilgbM4QKdgcgWHO9NCA==", + "cpu": [ + "!arm", + "!arm64", + "!riscv64", + "!x64" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "sass": "1.98.0" + } + }, + "node_modules/sass-embedded-android-arm": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.98.0.tgz", + "integrity": "sha512-LjGiMhHgu7VL1n7EJxTCre1x14bUsWd9d3dnkS2rku003IWOI/fxc7OXgaKagoVzok1kv09rzO3vFXJR5ZeONQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-arm64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.98.0.tgz", + "integrity": "sha512-M9Ra98A6vYJHpwhoC/5EuH1eOshQ9ZyNwC8XifUDSbRl/cGeQceT1NReR9wFj3L7s1pIbmes1vMmaY2np0uAKQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-riscv64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.98.0.tgz", + "integrity": "sha512-WPe+0NbaJIZE1fq/RfCZANMeIgmy83x4f+SvFOG7LhUthHpZWcOcrPTsCKKmN3xMT3iw+4DXvqTYOCYGRL3hcQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-x64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.98.0.tgz", + "integrity": "sha512-zrD25dT7OHPEgLWuPEByybnIfx4rnCtfge4clBgjZdZ3lF6E7qNLRBtSBmoFflh6Vg0RlEjJo5VlpnTMBM5MQQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-arm64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.98.0.tgz", + "integrity": "sha512-cgr1z9rBnCdMf8K+JabIaYd9Rag2OJi5mjq08XJfbJGMZV/TA6hFJCLGkr5/+ZOn4/geTM5/3aSfQ8z5EIJAOg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-x64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.98.0.tgz", + "integrity": "sha512-OLBOCs/NPeiMqTdOrMFbVHBQFj19GS3bSVSxIhcCq16ZyhouUkYJEZjxQgzv9SWA2q6Ki8GCqp4k6jMeUY9dcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.98.0.tgz", + "integrity": "sha512-03baQZCxVyEp8v1NWBRlzGYrmVT/LK7ZrHlF1piscGiGxwfdxoLXVuxsylx3qn/dD/4i/rh7Bzk7reK1br9jvQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.98.0.tgz", + "integrity": "sha512-axOE3t2MTBwCtkUCbrdM++Gj0gC0fdHJPrgzQ+q1WUmY9NoNMGqflBtk5mBZaWUeha2qYO3FawxCB8lctFwCtw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.98.0.tgz", + "integrity": "sha512-OBkjTDPYR4hSaueOGIM6FDpl9nt/VZwbSRpbNu9/eEJcxE8G/vynRugW8KRZmCFjPy8j/jkGBvvS+k9iOqKV3g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.98.0.tgz", + "integrity": "sha512-LeqNxQA8y4opjhe68CcFvMzCSrBuJqYVFbwElEj9bagHXQHTp9xVPJRn6VcrC+0VLEDq13HVXMv7RslIuU0zmA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-riscv64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.98.0.tgz", + "integrity": "sha512-7w6hSuOHKt8FZsmjRb3iGSxEzM87fO9+M8nt5JIQYMhHTj5C+JY/vcske0v715HCVj5e1xyTnbGXf8FcASeAIw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-x64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.98.0.tgz", + "integrity": "sha512-QikNyDEJOVqPmxyCFkci8ZdCwEssdItfjQFJB+D+Uy5HFqcS5Lv3d3GxWNX/h1dSb23RPyQdQc267ok5SbEyJw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-riscv64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.98.0.tgz", + "integrity": "sha512-E7fNytc/v4xFBQKzgzBddV/jretA4ULAPO6XmtBiQu4zZBdBozuSxsQLe2+XXeb0X4S2GIl72V7IPABdqke/vA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-x64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.98.0.tgz", + "integrity": "sha512-VsvP0t/uw00mMNPv3vwyYKUrFbqzxQHnRMO+bHdAMjvLw4NFf6mscpym9Bzf+NXwi1ZNKnB6DtXjmcpcvqFqYg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-unknown-all": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.98.0.tgz", + "integrity": "sha512-C4MMzcAo3oEDQnW7L8SBgB9F2Fq5qHPnaYTZRMOH3Mp/7kM4OooBInXpCiiFjLnjY95hzP4KyctVx0uYR6MYlQ==", + "license": "MIT", + "optional": true, + "os": [ + "!android", + "!darwin", + "!linux", + "!win32" + ], + "dependencies": { + "sass": "1.98.0" + } + }, + "node_modules/sass-embedded-win32-arm64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.98.0.tgz", + "integrity": "sha512-nP/10xbAiPbhQkMr3zQfXE4TuOxPzWRQe1Hgbi90jv2R4TbzbqQTuZVOaJf7KOAN4L2Bo6XCTRjK5XkVnwZuwQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-x64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.98.0.tgz", + "integrity": "sha512-/lbrVsfbcbdZQ5SJCWcV0NVPd6YRs+FtAnfedp4WbCkO/ZO7Zt/58MvI4X2BVpRY/Nt5ZBo1/7v2gYcQ+J4svQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT" + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz", + "integrity": "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==", + "license": "MIT" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shiki": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.2.tgz", + "integrity": "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.0.2", + "@shikijs/engine-javascript": "4.0.2", + "@shikijs/engine-oniguruma": "4.0.2", + "@shikijs/langs": "4.0.2", + "@shikijs/themes": "4.0.2", + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sort-keys": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.1.0.tgz", + "integrity": "sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sortablejs": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz", + "integrity": "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/sql.js": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz", + "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==", + "license": "MIT" + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sticky-module": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/sticky-module/-/sticky-module-0.1.1.tgz", + "integrity": "sha512-IuYgnyIMUx/m6rtu14l/LR2MaqOLtpXcWkxPmtPsiScRHEo+S4Tojk+DWFHOncSdFX/OsoLOM4+T92yOmI1AMw==", + "license": "ISC" + }, + "node_modules/stream-composer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", + "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.13.2" + } + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.53.12", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.12.tgz", + "integrity": "sha512-4x/uk4rQe/d7RhfvS8wemTfNjQ0bJbKvamIzRBfTe2eHHjzBZ7PZicUQrC2ryj83xxEacfA1zHKd1ephD1tAxA==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.6.4", + "esm-env": "^1.2.1", + "esrap": "^2.2.2", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.5.tgz", + "integrity": "sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-confetti": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/svelte-confetti/-/svelte-confetti-2.3.2.tgz", + "integrity": "sha512-cfIwoGqMPYWRYDUz2g7mG1uHYWy7VBepelQdzCC3j/M42UrAqaBYmIi9xaoQfow4fbINHO9WuARnTyK2bjjGQg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "svelte": ">=5.0.0" + } + }, + "node_modules/svelte-eslint-parser": { + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", + "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "postcss": "^8.4.39", + "postcss-scss": "^4.0.9" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/svelte-sonner": { + "version": "0.3.28", + "resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.28.tgz", + "integrity": "sha512-K3AmlySeFifF/cKgsYNv5uXqMVNln0NBAacOYgmkQStLa/UoU0LhfAACU6Gr+YYC8bOCHdVmFNoKuDbMEsppJg==", + "license": "MIT", + "peerDependencies": { + "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1" + } + }, + "node_modules/svelte-toolbelt": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz", + "integrity": "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==", + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.35.1", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.30.2" + } + }, + "node_modules/svelte/node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/symlink-or-copy": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", + "integrity": "sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/sync-child-process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", + "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "sync-message-port": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/sync-message-port": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.2.0.tgz", + "integrity": "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", + "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tippy.js": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", + "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.9.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-json-callback": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/to-json-callback/-/to-json-callback-0.1.1.tgz", + "integrity": "sha512-BzOeinTT3NjE+FJ2iCvWB8HvyuyBzoH3WlSnJ+AYVC4tlePyZWSYdkQIFOARWiq0t35/XhmI0uQsFiUsRksRqg==", + "license": "ISC" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/to-through": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "license": "ISC", + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, + "node_modules/topojson-client/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/turndown": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.2.tgz", + "integrity": "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==", + "license": "MIT", + "dependencies": { + "@mixmark-io/domino": "^2.2.0" + } + }, + "node_modules/turndown-plugin-gfm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.2.tgz", + "integrity": "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==", + "license": "MIT" + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-checked-collections": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/type-checked-collections/-/type-checked-collections-0.1.7.tgz", + "integrity": "sha512-fLIydlJy7IG9XL4wjRwEcKhxx/ekLXiWiMvcGo01cOMF+TN+5ZqajM1mRNRz2bNNi1bzou2yofhjZEQi7kgl9A==", + "license": "ISC" + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT" + }, + "node_modules/underscore.string": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", + "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "^1.1.1", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.3.tgz", + "integrity": "sha512-eJdUmK/Wrx2d+mnWWmwwLRyA7OQCkLap60sk3dOK4ViZR7DKwwptwuIvFBg2HaiP9ESaEdhtpSymQPvytpmkCA==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/value-or-function": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/vega": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/vega/-/vega-6.2.0.tgz", + "integrity": "sha512-BIwalIcEGysJdQDjeVUmMWB3e50jPDNAMfLJscjEvpunU9bSt7X1OYnQxkg3uBwuRRI4nWfFZO9uIW910nLeGw==", + "license": "BSD-3-Clause", + "dependencies": { + "vega-crossfilter": "~5.1.0", + "vega-dataflow": "~6.1.0", + "vega-encode": "~5.1.0", + "vega-event-selector": "~4.0.0", + "vega-expression": "~6.1.0", + "vega-force": "~5.1.0", + "vega-format": "~2.1.0", + "vega-functions": "~6.1.0", + "vega-geo": "~5.1.0", + "vega-hierarchy": "~5.1.0", + "vega-label": "~2.1.0", + "vega-loader": "~5.1.0", + "vega-parser": "~7.1.0", + "vega-projection": "~2.1.0", + "vega-regression": "~2.1.0", + "vega-runtime": "~7.1.0", + "vega-scale": "~8.1.0", + "vega-scenegraph": "~5.1.0", + "vega-statistics": "~2.0.0", + "vega-time": "~3.1.0", + "vega-transforms": "~5.1.0", + "vega-typings": "~2.1.0", + "vega-util": "~2.1.0", + "vega-view": "~6.1.0", + "vega-view-transforms": "~5.1.0", + "vega-voronoi": "~5.1.0", + "vega-wordcloud": "~5.1.0" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + } + }, + "node_modules/vega-canvas": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vega-canvas/-/vega-canvas-2.0.0.tgz", + "integrity": "sha512-9x+4TTw/USYST5nx4yN272sy9WcqSRjAR0tkQYZJ4cQIeon7uVsnohvoPQK1JZu7K1QXGUqzj08z0u/UegBVMA==", + "license": "BSD-3-Clause" + }, + "node_modules/vega-crossfilter": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-crossfilter/-/vega-crossfilter-5.1.0.tgz", + "integrity": "sha512-EmVhfP3p6AM7o/lPan/QAoqjblI19BxWUlvl2TSs0xjQd8KbaYYbS4Ixt3cmEvl0QjRdBMF6CdJJ/cy9DTS4Fw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-dataflow": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vega-dataflow/-/vega-dataflow-6.1.0.tgz", + "integrity": "sha512-JxumGlODtFbzoQ4c/jQK8Tb/68ih0lrexlCozcMfTAwQ12XhTqCvlafh7MAKKTMBizjOfaQTHm4Jkyb1H5CfyQ==", + "license": "BSD-3-Clause", + "dependencies": { + "vega-format": "^2.1.0", + "vega-loader": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-encode": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-encode/-/vega-encode-5.1.0.tgz", + "integrity": "sha512-q26oI7B+MBQYcTQcr5/c1AMsX3FvjZLQOBi7yI0vV+GEn93fElDgvhQiYrgeYSD4Exi/jBPeUXuN6p4bLz16kA==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-interpolate": "^3.0.1", + "vega-dataflow": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-event-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vega-event-selector/-/vega-event-selector-4.0.0.tgz", + "integrity": "sha512-CcWF4m4KL/al1Oa5qSzZ5R776q8lRxCj3IafCHs5xipoEHrkgu1BWa7F/IH5HrDNXeIDnqOpSV1pFsAWRak4gQ==", + "license": "BSD-3-Clause" + }, + "node_modules/vega-expression": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-6.1.0.tgz", + "integrity": "sha512-hHgNx/fQ1Vn1u6vHSamH7lRMsOa/yQeHGGcWVmh8fZafLdwdhCM91kZD9p7+AleNpgwiwzfGogtpATFaMmDFYg==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/estree": "^1.0.8", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-force": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-force/-/vega-force-5.1.0.tgz", + "integrity": "sha512-wdnchOSeXpF9Xx8Yp0s6Do9F7YkFeOn/E/nENtsI7NOcyHpICJ5+UkgjUo9QaQ/Yu+dIDU+sP/4NXsUtq6SMaQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-force": "^3.0.0", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-format/-/vega-format-2.1.0.tgz", + "integrity": "sha512-i9Ht33IgqG36+S1gFDpAiKvXCPz+q+1vDhDGKK8YsgMxGOG4PzinKakI66xd7SdV4q97FgpR7odAXqtDN2wKqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-format": "^3.1.0", + "d3-time-format": "^4.1.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-functions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/vega-functions/-/vega-functions-6.1.1.tgz", + "integrity": "sha512-Due6jP0y0FfsGMTrHnzUGnEwXPu7VwE+9relfo+LjL/tRPYnnKqwWvzt7n9JkeBuZqjkgYjMzm/WucNn6Hkw5A==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-color": "^3.1.0", + "d3-geo": "^3.1.1", + "vega-dataflow": "^6.1.0", + "vega-expression": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-scenegraph": "^5.1.0", + "vega-selections": "^6.1.0", + "vega-statistics": "^2.0.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-geo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-geo/-/vega-geo-5.1.0.tgz", + "integrity": "sha512-H8aBBHfthc3rzDbz/Th18+Nvp00J73q3uXGAPDQqizioDm/CoXCK8cX4pMePydBY9S6ikBiGJrLKFDa80wI20g==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-color": "^3.1.0", + "d3-geo": "^3.1.1", + "vega-canvas": "^2.0.0", + "vega-dataflow": "^6.1.0", + "vega-projection": "^2.1.0", + "vega-statistics": "^2.0.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-hierarchy": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-hierarchy/-/vega-hierarchy-5.1.0.tgz", + "integrity": "sha512-rZlU8QJNETlB6o73lGCPybZtw2fBBsRIRuFE77aCLFHdGsh6wIifhplVarqE9icBqjUHRRUOmcEYfzwVIPr65g==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-hierarchy": "^3.1.2", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-label": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-label/-/vega-label-2.1.0.tgz", + "integrity": "sha512-/hgf+zoA3FViDBehrQT42Lta3t8In6YwtMnwjYlh72zNn1p3c7E3YUBwqmAqTM1x+tudgzMRGLYig+bX1ewZxQ==", + "license": "BSD-3-Clause", + "dependencies": { + "vega-canvas": "^2.0.0", + "vega-dataflow": "^6.1.0", + "vega-scenegraph": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-lite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-6.4.2.tgz", + "integrity": "sha512-Mv2PaRIpijz256LM0NdOJd9Md8cqyrXina54xW6Qp865YfY502zlXGUst+W/XznVwISGfatt0yLZuDqCUbBDuw==", + "license": "BSD-3-Clause", + "dependencies": { + "json-stringify-pretty-compact": "~4.0.0", + "tslib": "~2.8.1", + "vega-event-selector": "~4.0.0", + "vega-expression": "~6.1.0", + "vega-util": "~2.1.0", + "yargs": "~18.0.0" + }, + "bin": { + "vl2pdf": "bin/vl2pdf", + "vl2png": "bin/vl2png", + "vl2svg": "bin/vl2svg", + "vl2vg": "bin/vl2vg" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + }, + "peerDependencies": { + "vega": "^6.0.0" + } + }, + "node_modules/vega-loader": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-loader/-/vega-loader-5.1.0.tgz", + "integrity": "sha512-GaY3BdSPbPNdtrBz8SYUBNmNd8mdPc3mtdZfdkFazQ0RD9m+Toz5oR8fKnTamNSk9fRTJX0Lp3uEqxrAlQVreg==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-dsv": "^3.0.1", + "topojson-client": "^3.1.0", + "vega-format": "^2.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/vega-parser/-/vega-parser-7.1.0.tgz", + "integrity": "sha512-g0lrYxtmYVW8G6yXpIS4J3Uxt9OUSkc0bLu5afoYDo4rZmoOOdll3x3ebActp5LHPW+usZIE+p5nukRS2vEc7Q==", + "license": "BSD-3-Clause", + "dependencies": { + "vega-dataflow": "^6.1.0", + "vega-event-selector": "^4.0.0", + "vega-functions": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-projection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-projection/-/vega-projection-2.1.0.tgz", + "integrity": "sha512-EjRjVSoMR5ibrU7q8LaOQKP327NcOAM1+eZ+NO4ANvvAutwmbNVTmfA1VpPH+AD0AlBYc39ND/wnRk7SieDiXA==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-geo": "^3.1.1", + "d3-geo-projection": "^4.0.0", + "vega-scale": "^8.1.0" + } + }, + "node_modules/vega-regression": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-regression/-/vega-regression-2.1.0.tgz", + "integrity": "sha512-HzC7MuoEwG1rIxRaNTqgcaYF03z/ZxYkQR2D5BN0N45kLnHY1HJXiEcZkcffTsqXdspLjn47yLi44UoCwF5fxQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "vega-dataflow": "^6.1.0", + "vega-statistics": "^2.0.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-runtime": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/vega-runtime/-/vega-runtime-7.1.0.tgz", + "integrity": "sha512-mItI+WHimyEcZlZrQ/zYR3LwHVeyHCWwp7MKaBjkU8EwkSxEEGVceyGUY9X2YuJLiOgkLz/6juYDbMv60pfwYA==", + "license": "BSD-3-Clause", + "dependencies": { + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-scale": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vega-scale/-/vega-scale-8.1.0.tgz", + "integrity": "sha512-VEgDuEcOec8+C8+FzLcnAmcXrv2gAJKqQifCdQhkgnsLa978vYUgVfCut/mBSMMHbH8wlUV1D0fKZTjRukA1+A==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-scale-chromatic": "^3.1.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-scenegraph": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-scenegraph/-/vega-scenegraph-5.1.0.tgz", + "integrity": "sha512-4gA89CFIxkZX+4Nvl8SZF2MBOqnlj9J5zgdPh/HPx+JOwtzSlUqIhxFpFj7GWYfwzr/PyZnguBLPihPw1Og/cA==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "^3.1.0", + "d3-shape": "^3.2.0", + "vega-canvas": "^2.0.0", + "vega-loader": "^5.1.0", + "vega-scale": "^8.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-selections": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/vega-selections/-/vega-selections-6.1.2.tgz", + "integrity": "sha512-xJ+V4qdd46nk2RBdwIRrQm2iSTMHdlu/omhLz1pqRL3jZDrkqNBXimrisci2kIKpH2WBpA1YVagwuZEKBmF2Qw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "3.2.4", + "vega-expression": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-statistics": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vega-statistics/-/vega-statistics-2.0.0.tgz", + "integrity": "sha512-dGPfDXnBlgXbZF3oxtkb8JfeRXd5TYHx25Z/tIoaa9jWua4Vf/AoW2wwh8J1qmMy8J03/29aowkp1yk4DOPazQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4" + } + }, + "node_modules/vega-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vega-time/-/vega-time-3.1.0.tgz", + "integrity": "sha512-G93mWzPwNa6UYQRkr8Ujur9uqxbBDjDT/WpXjbDY0yygdSkRT+zXF+Sb4gjhW0nPaqdiwkn0R6kZcSPMj1bMNA==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-transforms/-/vega-transforms-5.1.0.tgz", + "integrity": "sha512-mj/sO2tSuzzpiXX8JSl4DDlhEmVwM/46MTAzTNQUQzJPMI/n4ChCjr/SdEbfEyzlD4DPm1bjohZGjLc010yuMg==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "vega-dataflow": "^6.1.0", + "vega-statistics": "^2.0.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-typings": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-typings/-/vega-typings-2.1.0.tgz", + "integrity": "sha512-zdis4Fg4gv37yEvTTSZEVMNhp8hwyEl7GZ4X4HHddRVRKxWFsbyKvZx/YW5Z9Ox4sjxVA2qHzEbod4Fdx+SEJA==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/geojson": "7946.0.16", + "vega-event-selector": "^4.0.0", + "vega-expression": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-util": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-2.1.0.tgz", + "integrity": "sha512-PGfp0m0QCufDmcxKJCWQy4Ov23FoF8DSXmoJwSezi3itQaa2hbxK0+xwsTMP2vy4PR16Pu25HMzgMwXVW1+33w==", + "license": "BSD-3-Clause" + }, + "node_modules/vega-view": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vega-view/-/vega-view-6.1.0.tgz", + "integrity": "sha512-hmHDm/zC65lb23mb9Tr9Gx0wkxP0TMS31LpMPYxIZpvInxvUn7TYitkOtz1elr63k2YZrgmF7ztdGyQ4iCQ5fQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-timer": "^3.0.1", + "vega-dataflow": "^6.1.0", + "vega-format": "^2.1.0", + "vega-functions": "^6.1.0", + "vega-runtime": "^7.1.0", + "vega-scenegraph": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-view-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-view-transforms/-/vega-view-transforms-5.1.0.tgz", + "integrity": "sha512-fpigh/xn/32t+An1ShoY3MLeGzNdlbAp2+HvFKzPpmpMTZqJEWkk/J/wHU7Swyc28Ta7W1z3fO+8dZkOYO5TWQ==", + "license": "BSD-3-Clause", + "dependencies": { + "vega-dataflow": "^6.1.0", + "vega-scenegraph": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-voronoi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-voronoi/-/vega-voronoi-5.1.0.tgz", + "integrity": "sha512-uKdsoR9x60mz7eYtVG+NhlkdQXeVdMr6jHNAHxs+W+i6kawkUp5S9jp1xf1FmW/uZvtO1eqinHQNwATcDRsiUg==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-delaunay": "^6.0.4", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-wordcloud": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vega-wordcloud/-/vega-wordcloud-5.1.0.tgz", + "integrity": "sha512-sSdNmT8y2D7xXhM2h76dKyaYn3PA4eV49WUUkfYfqHz/vpcu10GSAoFxLhQQTkbZXR+q5ZB63tFUow9W2IFo6g==", + "license": "BSD-3-Clause", + "dependencies": { + "vega-canvas": "^2.0.0", + "vega-dataflow": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-statistics": "^2.0.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vinyl": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.1.tgz", + "integrity": "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^2.1.2", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^5.0.0", + "vinyl": "^3.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-fs": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.2.tgz", + "integrity": "sha512-XRFwBLLTl8lRAOYiBqxY279wY46tVxLaRhSwo3GzKEuLz1giffsOquWWboD/haGf5lx+JyTigCFfe7DWHoARIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.3", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.1", + "vinyl-sourcemap": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite-plugin-static-copy": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.3.2.tgz", + "integrity": "sha512-iwrrf+JupY4b9stBttRWzGHzZbeMjAHBhkrn67MNACXJVjEMRpCI10Q3AkxdBkl45IHaTfw/CNVevzQhP7yTwg==", + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.3", + "fast-glob": "^3.2.11", + "fs-extra": "^11.1.0", + "p-map": "^7.0.3", + "picocolors": "^1.0.0" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/vite-plugin-static-copy/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/vite-plugin-static-copy/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/vite-plugin-static-copy/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/vite-plugin-static-copy/node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-static-copy/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vite-plugin-static-copy/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitefu": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", + "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/vitest/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/vitest/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/vitest/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "license": "MIT" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/walk-sync": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz", + "integrity": "sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^3.0.3", + "ensure-posix-path": "^1.1.0", + "matcher-collection": "^2.0.0", + "minimatch": "^3.0.4" + }, + "engines": { + "node": "8.* || >= 10.*" + } + }, + "node_modules/walk-sync/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/walk-sync/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/walk-sync/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/wheel": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wheel/-/wheel-1.0.0.tgz", + "integrity": "sha512-XiCMHibOiqalCQ+BaNSwRoZ9FDTAvOsXxGHXChBugewDj7HC8VBIER71dEOiRH1fSdLbRCQzngKTSiZ06ZQzeA==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y-prosemirror": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/y-prosemirror/-/y-prosemirror-1.3.7.tgz", + "integrity": "sha512-NpM99WSdD4Fx4if5xOMDpPtU3oAmTSjlzh5U4353ABbRHl1HtAFUx6HlebLZfyFxXN9jzKMDkVbcRjqOZVkYQg==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.109" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "prosemirror-model": "^1.7.1", + "prosemirror-state": "^1.2.3", + "prosemirror-view": "^1.9.10", + "y-protocols": "^1.0.1", + "yjs": "^13.5.38" + } + }, + "node_modules/y-protocols": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.7.tgz", + "integrity": "sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yjs": { + "version": "13.6.30", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.30.tgz", + "integrity": "sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.99" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..edd77762e6c12e2e6789bc31cc98913548f4b7fb --- /dev/null +++ b/package.json @@ -0,0 +1,162 @@ +{ + "name": "open-webui", + "version": "0.9.2", + "private": true, + "scripts": { + "dev": "npm run pyodide:fetch && vite dev --host", + "dev:5050": "npm run pyodide:fetch && vite dev --port 5050", + "build": "npm run pyodide:fetch && vite build", + "build:watch": "npm run pyodide:fetch && vite build --watch", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "npm run lint:frontend ; npm run lint:types ; npm run lint:backend", + "lint:frontend": "eslint . --fix", + "lint:types": "npm run check", + "lint:backend": "pylint backend/", + "format": "prettier --plugin-search-dir --write \"**/*.{js,ts,svelte,css,md,html,json}\"", + "format:backend": "ruff format . --exclude .venv --exclude venv", + "i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write \"src/lib/i18n/**/*.{js,json}\"", + "cy:open": "cypress open", + "test:frontend": "vitest --passWithNoTests", + "pyodide:fetch": "node scripts/prepare-pyodide.js" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "3.2.2", + "@sveltejs/adapter-static": "^3.0.2", + "@sveltejs/kit": "^2.5.27", + "@sveltejs/vite-plugin-svelte": "^4.0.0", + "@tailwindcss/container-queries": "^0.1.1", + "@tailwindcss/postcss": "^4.0.0", + "@tailwindcss/typography": "^0.5.13", + "@typescript-eslint/eslint-plugin": "^8.31.1", + "@typescript-eslint/parser": "^8.31.1", + "cypress": "^13.15.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-cypress": "^3.4.0", + "eslint-plugin-svelte": "^2.45.1", + "i18next-parser": "^9.0.1", + "postcss": "^8.4.31", + "prettier": "^3.3.3", + "prettier-plugin-svelte": "^3.2.6", + "sass-embedded": "^1.81.0", + "svelte": "^5.53.10", + "svelte-check": "^4.0.0", + "svelte-confetti": "^2.3.2", + "tailwindcss": "^4.0.0", + "tslib": "^2.4.1", + "typescript": "^5.5.4", + "vite": "^5.4.21", + "vitest": "^1.6.1" + }, + "type": "module", + "dependencies": { + "@azure/msal-browser": "^4.5.0", + "@codemirror/lang-javascript": "^6.2.2", + "@codemirror/lang-python": "^6.1.6", + "@codemirror/language-data": "^6.5.1", + "@codemirror/theme-one-dark": "^6.1.2", + "@floating-ui/dom": "^1.7.2", + "@huggingface/transformers": "^3.0.0", + "@joplin/turndown-plugin-gfm": "^1.0.62", + "@mediapipe/tasks-vision": "^0.10.17", + "@pyscript/core": "^0.4.32", + "@sveltejs/adapter-node": "^2.0.0", + "@sveltejs/svelte-virtual-list": "^3.0.1", + "@tiptap/core": "^3.0.7", + "@tiptap/extension-bubble-menu": "^3.0.7", + "@tiptap/extension-code": "^3.0.7", + "@tiptap/extension-code-block-lowlight": "^3.0.7", + "@tiptap/extension-drag-handle": "^3.4.5", + "@tiptap/extension-file-handler": "^3.0.7", + "@tiptap/extension-floating-menu": "^3.0.7", + "@tiptap/extension-highlight": "^3.3.0", + "@tiptap/extension-image": "^3.0.7", + "@tiptap/extension-link": "^3.0.7", + "@tiptap/extension-list": "^3.0.7", + "@tiptap/extension-mention": "^3.0.9", + "@tiptap/extension-table": "^3.0.7", + "@tiptap/extension-typography": "^3.0.7", + "@tiptap/extension-youtube": "^3.0.7", + "@tiptap/extensions": "^3.0.7", + "@tiptap/pm": "^3.0.7", + "@tiptap/starter-kit": "^3.0.7", + "@tiptap/suggestion": "^3.4.2", + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-web-links": "^0.12.0", + "@xterm/xterm": "^6.0.0", + "@xyflow/svelte": "^0.1.19", + "alpinejs": "^3.15.0", + "async": "^3.2.5", + "bits-ui": "^2.0.0", + "chart.js": "^4.5.0", + "codemirror": "^6.0.1", + "codemirror-lang-elixir": "^4.0.0", + "codemirror-lang-hcl": "^0.1.0", + "crc-32": "^1.2.2", + "dayjs": "^1.11.10", + "dompurify": "^3.2.6", + "eventsource-parser": "^1.1.2", + "fast-deep-equal": "^3.1.3", + "file-saver": "^2.0.5", + "focus-trap": "^7.6.4", + "fuse.js": "^7.0.0", + "heic2any": "^0.0.4", + "highlight.js": "^11.9.0", + "html-entities": "^2.5.3", + "html2canvas-pro": "^1.5.11", + "i18next": "^23.10.0", + "i18next-browser-languagedetector": "^7.2.0", + "i18next-resources-to-backend": "^1.2.0", + "idb": "^7.1.1", + "js-sha256": "^0.10.1", + "jspdf": "^4.0.0", + "jszip": "^3.10.1", + "katex": "^0.16.22", + "kokoro-js": "^1.1.1", + "leaflet": "^1.9.4", + "lowlight": "^3.3.0", + "mammoth": "^1.11.0", + "marked": "^9.1.0", + "mermaid": "^11.10.1", + "paneforge": "^0.0.6", + "panzoom": "^9.4.3", + "pdfjs-dist": "^5.4.149", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.6.0", + "prosemirror-example-setup": "^1.2.3", + "prosemirror-history": "^1.4.1", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.13.1", + "prosemirror-model": "^1.23.0", + "prosemirror-schema-basic": "^1.2.3", + "prosemirror-schema-list": "^1.5.1", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.7.1", + "prosemirror-view": "^1.34.3", + "pyodide": "^0.28.2", + "shiki": "^4.0.1", + "socket.io-client": "^4.2.0", + "sortablejs": "^1.15.6", + "sql.js": "^1.14.1", + "svelte-sonner": "^0.3.19", + "tippy.js": "^6.3.7", + "turndown": "^7.2.0", + "turndown-plugin-gfm": "^1.0.2", + "undici": "^7.3.0", + "uuid": "^9.0.1", + "vega": "^6.2.0", + "vega-lite": "^6.4.1", + "vite-plugin-static-copy": "^2.2.0", + "xlsx": "^0.18.5", + "y-prosemirror": "^1.3.7", + "y-protocols": "^1.0.7", + "yaml": "^2.7.1", + "yjs": "^13.6.27" + }, + "engines": { + "node": ">=18.13.0 <=22.x.x", + "npm": ">=6.0.0" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000000000000000000000000000000000000..85b958cb5961a220ce6f1139b7e8f6fa49baead1 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {} + } +}; diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..8d8fc8a755972593df33bba3535fe79cdc805acd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,250 @@ +[project] +name = "open-webui" +description = "Open WebUI" +authors = [ + { name = "Timothy Jaeryang Baek", email = "tim@openwebui.com" } +] +license = { file = "LICENSE" } +dependencies = [ + "fastapi==0.135.1", + "uvicorn[standard]==0.41.0", + "pydantic==2.12.5", + "python-multipart==0.0.22", + "itsdangerous==2.2.0", + + "python-socketio==5.16.1", + "python-jose==3.5.0", + "cryptography==46.0.5", + "bcrypt==5.0.0", + "argon2-cffi==25.1.0", + "PyJWT[crypto]==2.11.0", + "authlib==1.6.10", + + "requests==2.33.1", + "aiohttp==3.13.5", # do not update to 3.13.3 - broken + "async-timeout==5.0.1", + "aiocache==0.12.3", + "aiofiles==25.1.0", + "starlette-compress==1.7.0", + "Brotli==1.2.0", + "brotlicffi==1.2.0.1", + "httpx[socks,http2,zstd,cli,brotli]==0.28.1", + "starsessions[redis]==2.2.1", + "python-mimeparse==2.0.0", + + "sqlalchemy[asyncio]==2.0.48", + "aiosqlite==0.21.0", + "psycopg[binary]==3.2.9", + "alembic==1.18.4", + "peewee==3.19.0", + "peewee-migrate==1.14.3", + + "pycrdt==0.12.47", + "redis==7.4.0", + + "pytz==2026.1.post1", + "APScheduler==3.11.2", + "RestrictedPython==8.1", + + "loguru==0.7.3", + "asgiref==3.11.1", + + "tiktoken==0.12.0", + "mcp==1.26.0", + + "openai==2.29.0", + "anthropic==0.86.0", + "google-genai==1.66.0", + + "langchain==1.2.10", + "langchain-community==0.4.1", + "langchain-classic==1.0.1", + "langchain-text-splitters==1.1.1", + + "fake-useragent==2.2.0", + "chromadb==1.5.2", + "opensearch-py==3.1.0", + "PyMySQL==1.1.2", + "boto3==1.42.62", + + "transformers==5.5.4", + "sentence-transformers==5.4.0", + "accelerate==1.13.0", + "pyarrow==20.0.0", # fix: pin pyarrow version to 20 for rpi compatibility #15897 + "einops==0.8.2", + + "ftfy==6.3.1", + "chardet==5.2.0", + "pypdf==6.7.5", + "fpdf2==2.8.7", + "pymdown-extensions==10.21", + "docx2txt==0.9", + "python-pptx==1.0.2", + "msoffcrypto-tool==6.0.0", + "nltk==3.9.3", + "Markdown==3.10.2", + "beautifulsoup4==4.14.3", + "pypandoc==1.16.2", + "pandas==3.0.1", + "openpyxl==3.1.5", + "pyxlsb==1.0.10", + "xlrd==2.0.2", + "validators==0.35.0", + "psutil==7.2.2", + "sentencepiece==0.2.1", + "soundfile==0.13.1", + "azure-ai-documentintelligence==1.0.2", + + "pillow==12.1.1", + "opencv-python-headless==4.13.0.92", + "rapidocr-onnxruntime==1.4.4", + "rank-bm25==0.2.2", + + "onnxruntime==1.24.3", + "faster-whisper==1.2.1", + + "black==26.3.1", + "youtube-transcript-api==1.2.4", + "pytube==15.0.0", + + "pydub==0.25.1", + "ddgs==9.11.3", + + "google-api-python-client==2.193.0", + "google-auth-httplib2==0.3.0", + "google-auth-oauthlib==1.3.0", + + "googleapis-common-protos==1.72.0", + "google-cloud-storage==3.9.0", + + "azure-identity==1.25.2", + "azure-storage-blob==12.28.0", + + "ldap3==2.9.1", +] +readme = "README.md" +requires-python = ">= 3.11, < 3.13.0a1" +dynamic = ["version"] +classifiers = [ + "Development Status :: 4 - Beta", + "License :: Other/Proprietary License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Communications :: Chat", + "Topic :: Multimedia", +] + +[project.optional-dependencies] +postgres = [ + "psycopg2-binary==2.9.11", + "pgvector==0.4.2", +] +mariadb = [ + "mariadb==1.1.14", +] +unstructured = [ + "unstructured==0.18.31", +] + +all = [ + "pymongo==4.16.0", + "psycopg2-binary==2.9.11", + "pgvector==0.4.2", + "moto[s3]>=5.0.26", + "gcp-storage-emulator>=2024.8.3", + "docker~=7.1.0", + "pytest~=8.3.2", + "pytest-docker~=3.2.5", + "playwright==1.58.0", # Caution: version must match docker-compose.playwright.yaml - Update the docker-compose.yaml if necessary + "elasticsearch==9.3.0", + + "qdrant-client==1.17.0", + + "weaviate-client==4.20.3", + "pymilvus==2.6.9", + "pinecone==6.0.2", + "oracledb==3.4.2", + "colbert-ai==0.2.22", + + "azure-search-documents==11.6.0", + "unstructured==0.18.31", +] + +[project.scripts] +open-webui = "open_webui:app" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.rye] +managed = true +dev-dependencies = [] + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.version] +path = "package.json" +pattern = '"version":\s*"(?P[^"]+)"' + +[tool.hatch.build.hooks.custom] # keep this for reading hooks from `hatch_build.py` + +[tool.hatch.build.targets.wheel] +sources = ["backend"] +exclude = [ + ".dockerignore", + ".gitignore", + ".webui_secret_key", + "dev.sh", + "requirements.txt", + "start.sh", + "start_windows.bat", + "webui.db", + "chroma.sqlite3", +] +force-include = { "CHANGELOG.md" = "open_webui/CHANGELOG.md", build = "open_webui/frontend" } + +[tool.codespell] +# Ref: https://github.com/codespell-project/codespell#using-a-config-file +skip = '.git*,*.svg,package-lock.json,i18n,*.lock,*.css,*-bundle.js,locales,example-doc.txt,emoji-shortcodes.json' +check-hidden = true +# ignore-regex = '' +ignore-words-list = 'ans' + +[dependency-groups] +dev = [ + "pytest-asyncio>=1.0.0", + "ruff>=0.15.5", +] + +[tool.black] +line-length = 120 +skip-string-normalization = true + +[tool.ruff] +line-length = 120 + +[tool.ruff.format] +quote-style = "single" +docstring-code-format = false + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "F", # pyflakes + "W", # pycodestyle warnings + "I", # isort + "UP", # pyupgrade + "C90", # mccabe + "Q", # flake8-quotes + "ICN", # flake8-import-conventions +] + +# Plugin configs: +flake8-import-conventions.banned-from = [ "ast", "datetime" ] +flake8-import-conventions.aliases = { datetime = "dt" } +flake8-quotes.inline-quotes = "single" +mccabe.max-complexity = 10 +pydocstyle.convention = "google" diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000000000000000000000000000000000000..9352177bd8f58d50442d4a9a99561e9a90df7cb2 --- /dev/null +++ b/src/app.css @@ -0,0 +1,841 @@ +@reference "./tailwind.css"; + +@font-face { + font-family: 'Inter'; + src: url('/assets/fonts/Inter-Variable.ttf'); + font-display: swap; +} + +@font-face { + font-family: 'Archivo'; + src: url('/assets/fonts/Archivo-Variable.ttf'); + font-display: swap; +} + +@font-face { + font-family: 'Mona Sans'; + src: url('/assets/fonts/Mona-Sans.woff2'); + font-display: swap; +} + +@font-face { + font-family: 'InstrumentSerif'; + src: url('/assets/fonts/InstrumentSerif-Regular.ttf'); + font-display: swap; +} + +@font-face { + font-family: 'Vazirmatn'; + src: url('/assets/fonts/Vazirmatn-Variable.ttf'); + font-display: swap; +} + +/* --app-text-scale is updated via the UI Scale slider (Interface.svelte) */ +:root { + --app-text-scale: 1; +} + +html { + word-break: break-word; + /* font-size scales the entire document via the same UI control */ + font-size: calc(1rem * var(--app-text-scale, 1)); +} + +#sidebar-chat-item { + /* sidebar item sizing scales for the chat list entries */ + min-height: calc(32px * var(--app-text-scale, 1)); + padding-inline: calc(11px * var(--app-text-scale, 1)); + padding-block: calc(6px * var(--app-text-scale, 1)); +} + +#sidebar-chat-item div[dir='auto'] { + /* chat title line height follows the text scale */ + height: calc(20px * var(--app-text-scale, 1)); + line-height: calc(20px * var(--app-text-scale, 1)); +} + +#sidebar-chat-item input { + /* editing state input height is kept in sync */ + min-height: calc(20px * var(--app-text-scale, 1)); +} + +code { + /* white-space-collapse: preserve !important; */ + overflow-x: auto; + width: auto; +} + +.editor-selection { + background: rgba(180, 213, 255, 0.5); + border-radius: 2px; +} + +.font-secondary { + font-family: 'InstrumentSerif', sans-serif; +} + +.marked a { + @apply underline; +} + +math { + margin-top: 1rem; +} + +.hljs { + @apply rounded-lg; +} + +input::placeholder { + direction: auto; +} + +textarea::placeholder { + direction: auto; +} + +.input-prose { + @apply prose dark:prose-invert prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-50 prose-hr:dark:border-gray-850 prose-p:my-1 prose-img:my-1 prose-headings:my-2 prose-pre:my-0 prose-table:my-1 prose-blockquote:my-0 prose-ul:my-1 prose-ol:my-1 prose-li:my-0.5 whitespace-pre-line; +} + +.input-prose-sm { + @apply prose dark:prose-invert prose-headings:font-medium prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg prose-hr:my-4 prose-hr:border-gray-50 prose-hr:dark:border-gray-850 prose-p:my-1 prose-img:my-1 prose-headings:my-2 prose-pre:my-0 prose-table:my-1 prose-blockquote:my-0 prose-ul:my-1 prose-ol:my-1 prose-li:my-1 whitespace-pre-line text-sm; +} + +.markdown-prose { + @apply prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-50 prose-hr:dark:border-gray-850 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line; +} + +.markdown-prose-sm { + @apply text-sm prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-2 prose-hr:border-gray-50 prose-hr:dark:border-gray-850 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line; +} + +.markdown-prose-xs { + @apply text-xs prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-0.5 prose-hr:border-gray-50 prose-hr:dark:border-gray-850 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line; +} + +.markdown a { + @apply underline; +} + +.font-primary { + font-family: 'Archivo', 'Vazirmatn', sans-serif; +} + +.drag-region { + -webkit-app-region: drag; +} + +.drag-region a, +.drag-region button { + -webkit-app-region: no-drag; +} + +.no-drag-region { + -webkit-app-region: no-drag; +} + +li p { + display: inline; +} + +::-webkit-scrollbar-thumb { + --tw-border-opacity: 1; + background-color: rgba(215, 215, 215, 0.6); + border-color: rgba(255, 255, 255, var(--tw-border-opacity)); + border-radius: 9999px; + border-width: 1px; +} + +/* Dark theme scrollbar styles */ +.dark ::-webkit-scrollbar-thumb { + background-color: rgba(67, 67, 67, 0.6); /* Darker color for dark theme */ + border-color: rgba(0, 0, 0, var(--tw-border-opacity)); +} + +::-webkit-scrollbar { + height: 0.45rem; + width: 0.45rem; +} + +::-webkit-scrollbar-track { + background-color: transparent; + border-radius: 9999px; +} + +select { + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E"); + background-position: right 0rem center; + background-repeat: no-repeat; + background-size: 1.5em 1.5em; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + /* padding-right: 2.5rem; */ + /* for Firefox */ + -moz-appearance: none; + /* for Chrome */ + -webkit-appearance: none; +} + +.dark select:not([class*='bg-transparent']) { + @apply bg-gray-900 text-gray-300; +} + +.dark select option { + @apply bg-gray-850 text-white; +} + +@keyframes shimmer { + 0% { + background-position: 100% 0; + } + 100% { + background-position: -100% 0; + } +} + +.shimmer { + background: linear-gradient( + 110deg, + #b4b4b4 0%, + #b4b4b4 43%, + #e8e8e8 50%, + #b4b4b4 57%, + #b4b4b4 100% + ); + background-size: 200% 100%; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + animation: shimmer 1.5s cubic-bezier(0.7, 0, 1, 0.4) infinite; + color: #b4b4b4; +} + +:global(.dark) .shimmer { + background: linear-gradient( + 110deg, + #9a9a9a 0%, + #9a9a9a 43%, + #5e5e5e 50%, + #9a9a9a 57%, + #9a9a9a 100% + ); + background-size: 200% 100%; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + animation: shimmer 1.5s cubic-bezier(0.7, 0, 1, 0.4) infinite; + color: #9a9a9a; +} + +@keyframes smoothFadeIn { + 0% { + opacity: 0; + transform: translateY(-10px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +.status-description { + animation: smoothFadeIn 0.2s forwards; +} + +@keyframes fade-in-token { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.fade-in-token { + animation: fade-in-token 100ms ease-out; +} + +.katex-mathml { + display: none; +} + +/* Hide leaked Mermaid temp containers if render cleanup misses. + Use visibility:hidden (not display:none) so mermaid can still + measure the SVG layout before extracting its HTML. */ +body > div[id^='dmermaid-'], +body > iframe[id^='imermaid-'] { + position: fixed !important; + visibility: hidden !important; + height: 0 !important; + overflow: hidden !important; +} + +.scrollbar-hidden:active::-webkit-scrollbar-thumb, +.scrollbar-hidden:focus::-webkit-scrollbar-thumb, +.scrollbar-hidden:hover::-webkit-scrollbar-thumb { + visibility: visible; +} +.scrollbar-hidden::-webkit-scrollbar-thumb { + visibility: hidden; +} + +.scrollbar-hidden::-webkit-scrollbar-corner { + display: none; +} + +.scrollbar-none::-webkit-scrollbar { + display: none; /* for Chrome, Safari and Opera */ +} + +.scrollbar-none::-webkit-scrollbar-corner { + display: none; +} + +.scrollbar-none { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + /* display: none; <- Crashes Chrome on hover */ + -webkit-appearance: none; + margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ +} + +input[type='number'] { + -moz-appearance: textfield; /* Firefox */ +} + +.katex-display { + @apply overflow-y-hidden overflow-x-auto max-w-full; +} + +.katex-display::-webkit-scrollbar { + height: 0.4rem; + width: 0.4rem; +} + +.katex-display:active::-webkit-scrollbar-thumb, +.katex-display:focus::-webkit-scrollbar-thumb, +.katex-display:hover::-webkit-scrollbar-thumb { + visibility: visible; +} +.katex-display::-webkit-scrollbar-thumb { + visibility: hidden; +} + +.katex-display::-webkit-scrollbar-corner { + display: none; +} + +.cm-editor { + height: 100%; + width: 100%; +} + +.cm-scroller:active::-webkit-scrollbar-thumb, +.cm-scroller:focus::-webkit-scrollbar-thumb, +.cm-scroller:hover::-webkit-scrollbar-thumb { + visibility: visible; +} + +.cm-scroller::-webkit-scrollbar-thumb { + visibility: hidden; +} + +.cm-scroller::-webkit-scrollbar-corner { + display: none; +} + +.cm-editor.cm-focused { + outline: none; +} + +.cm-gutters { + @apply !bg-white dark:!bg-black !border-none; +} + +.cm-editor { + @apply bg-white dark:bg-black; +} + +.tippy-box[data-theme~='dark'] { + @apply rounded-lg bg-gray-950 text-xs border border-gray-900 shadow-xl; +} + +.password { + -webkit-text-security: disc; +} + +.codespan { + padding: 0.15rem 0.3rem; + font-size: 0.85em; + @apply font-mono rounded-md text-gray-800 bg-gray-100 dark:text-gray-200 dark:bg-gray-800 mx-0.5; +} + +.svelte-flow { + background-color: transparent !important; +} + +.svelte-flow__edge > path { + stroke-width: 0.5; +} + +.svelte-flow__edge.animated > path { + stroke-width: 2; + @apply stroke-gray-600 dark:stroke-gray-500; +} + +.bg-gray-950-90 { + background-color: rgba(var(--color-gray-950, #0d0d0d), 0.9); +} + +.ProseMirror { + @apply h-full min-h-fit max-h-full whitespace-pre-wrap; +} + +.ProseMirror:focus { + outline: none; +} + +.ProseMirror p.is-editor-empty:first-child::before { + content: attr(data-placeholder); + float: left; + /* Below color is from tailwind, and has the proper contrast + text-gray-600 from: https://tailwindcss.com/docs/color */ + color: #676767; + pointer-events: none; + + @apply line-clamp-1 absolute; +} + +.tiptap ul[data-type='taskList'] { + list-style: none; + margin-left: 0; + padding: 0; + + li { + align-items: start; + display: flex; + + > label { + flex: 0 0 auto; + margin-right: 0.5rem; + margin-top: 0.2rem; + user-select: none; + display: flex; + } + + > div { + flex: 1 1 auto; + + align-items: center; + } + } + + /* checked data-checked="true" */ + + li[data-checked='true'] { + > div { + opacity: 0.5; + text-decoration: line-through; + } + } + + input[type='checkbox'] { + cursor: pointer; + } + + ul[data-type='taskList'] { + margin: 0; + } + + /* Reset nested regular ul elements to default styling */ + ul:not([data-type='taskList']) { + list-style: disc; + padding-left: 1rem; + + li { + align-items: initial; + display: list-item; + + label { + flex: initial; + margin-right: initial; + margin-top: initial; + user-select: initial; + display: initial; + } + + div { + flex: initial; + align-items: initial; + } + } + } +} + +.mention { + border-radius: 0.4rem; + box-decoration-break: clone; + padding: 0.1rem 0.3rem; + @apply text-sky-800 dark:text-sky-200 bg-sky-300/15 dark:bg-sky-500/15; +} + +.mention::after { + content: '\200B'; +} + +.tiptap .suggestion { + border-radius: 0.4rem; + box-decoration-break: clone; + padding: 0.1rem 0.3rem; + @apply text-sky-800 dark:text-sky-200 bg-sky-300/15 dark:bg-sky-500/15; +} + +.tiptap .suggestion::after { + content: '\200B'; +} + +.tiptap .suggestion.is-empty::after { + content: '\00A0'; + border-bottom: 1px dotted rgba(31, 41, 55, 0.12); +} + +.input-prose .tiptap ul[data-type='taskList'] { + list-style: none; + margin-left: 0; + padding: 0; + + li { + align-items: start; + display: flex; + + > label { + flex: 0 0 auto; + margin-right: 0.5rem; + margin-top: 0.4rem; + user-select: none; + display: flex; + } + + > div { + flex: 1 1 auto; + + align-items: center; + } + } + + /* checked data-checked="true" */ + + li[data-checked='true'] { + > div { + opacity: 0.5; + text-decoration: line-through; + } + } + + input[type='checkbox'] { + cursor: pointer; + } + + ul[data-type='taskList'] { + margin: 0; + } + + /* Reset nested regular ul elements to default styling */ + ul:not([data-type='taskList']) { + list-style: disc; + padding-left: 1rem; + + li { + align-items: initial; + display: list-item; + + label { + flex: initial; + margin-right: initial; + margin-top: initial; + user-select: initial; + display: initial; + } + + div { + flex: initial; + align-items: initial; + } + } + } +} + +@media (prefers-color-scheme: dark) { + .ProseMirror p.is-editor-empty:first-child::before { + color: #757575; + } +} + +.ai-autocompletion::after { + color: #a0a0a0; + + content: attr(data-suggestion); + pointer-events: none; +} + +.tiptap pre > code { + border-radius: 0.4rem; + font-size: 0.85rem; + padding: 0.25em 0.3em; + + @apply dark:bg-gray-800 bg-gray-50; +} + +.tiptap pre { + border-radius: 0.5rem; + font-family: 'JetBrainsMono', monospace; + margin: 1.5rem 0; + padding: 0.75rem 1rem; + + @apply dark:bg-gray-800 bg-gray-50; +} + +.tiptap p code { + padding: 0.15rem 0.3rem; + font-size: 0.85em; + @apply font-mono rounded-md text-gray-800 bg-gray-50 dark:text-gray-200 dark:bg-gray-800 mx-0.5; +} + +/* Code styling */ +.hljs-comment, +.hljs-quote { + color: #616161; +} + +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #f98181; +} + +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #fbbc88; +} + +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #b9f18d; +} + +.hljs-title, +.hljs-section { + color: #faf594; +} + +.hljs-keyword, +.hljs-selector-tag { + color: #70cff8; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: 700; +} + +/* Table styling for tiptap editors */ +.tiptap table { + @apply w-full text-sm text-start text-gray-500 dark:text-gray-400 max-w-full; +} + +.tiptap thead { + @apply text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 border-none; +} + +.tiptap th, +.tiptap td { + @apply px-3 py-1.5 border border-gray-100/30 dark:border-gray-850/30; +} + +.tiptap th { + @apply cursor-pointer text-start text-xs text-gray-700 dark:text-gray-400 font-semibold uppercase bg-gray-50 dark:bg-gray-850; +} + +.tiptap td { + @apply text-gray-900 dark:text-white w-max; +} + +.tiptap tr { + @apply bg-white dark:bg-gray-900 dark:border-gray-850 text-xs; +} + +.tippy-box[data-theme~='transparent'] { + @apply bg-transparent p-0 m-0; +} + +/* this is a rough fix for the first cursor position when the first paragraph is empty */ +.ProseMirror > .ProseMirror-yjs-cursor:first-child { + margin-top: 16px; +} +/* This gives the remote user caret. The colors are automatically overwritten*/ +.ProseMirror-yjs-cursor { + position: relative; + margin-left: -1px; + margin-right: -1px; + border-left: 1px solid black; + border-right: 1px solid black; + border-color: orange; + word-break: normal; + pointer-events: none; +} +/* This renders the username above the caret */ +.ProseMirror-yjs-cursor > div { + position: absolute; + top: -1.05em; + left: -1px; + font-size: 13px; + background-color: rgb(250, 129, 0); + user-select: none; + color: white; + padding-left: 2px; + padding-right: 2px; + white-space: nowrap; +} + +body { + background: #fff; + color: #000; +} + +.dark body { + background: #171717; + color: #eee; +} + +/* Position the handle relative to each LI */ +.pm-li--with-handle { + position: relative; + margin-left: 12px; /* make space for the handle */ +} + +.tiptap ul[data-type='taskList'] .pm-list-drag-handle { + margin-left: 0px; +} + +/* The drag handle itself */ +.pm-list-drag-handle { + position: absolute; + left: -36px; /* pull into the left gutter */ + top: 1px; + width: 18px; + height: 18px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 12px; + line-height: 1; + border-radius: 4px; + cursor: grab; + user-select: none; + opacity: 0.35; + transition: + opacity 120ms ease, + background 120ms ease; +} + +.tiptap ul[data-type='taskList'] .pm-list-drag-handle { + left: -16px; /* pull into the left gutter more to avoid the checkbox */ +} + +.pm-list-drag-handle:active { + cursor: grabbing; +} +.pm-li--with-handle:hover > .pm-list-drag-handle { + opacity: 1; +} +.pm-list-drag-handle:hover { + background: rgba(0, 0, 0, 0.06); +} + +:root { + --pm-accent: color-mix(in oklab, Highlight 70%, transparent); + --pm-fill-target: color-mix(in oklab, Highlight 26%, transparent); + --pm-fill-ancestor: color-mix(in oklab, Highlight 16%, transparent); +} + +.pm-li-drop-before, +.pm-li-drop-after, +.pm-li-drop-into, +.pm-li-drop-outdent { + position: relative; +} + +/* BEFORE/AFTER lines */ +.pm-li-drop-before::before, +.pm-li-drop-after::after { + content: ''; + position: absolute; + left: 0; + right: 0; + height: 3px; + background: var(--pm-accent); + pointer-events: none; +} +.pm-li-drop-before::before { + top: -2px; +} +.pm-li-drop-after::after { + bottom: -2px; +} + +.pm-li-drop-before, +.pm-li-drop-after, +.pm-li-drop-into, +.pm-li-drop-outdent { + background: var(--pm-fill-target); + border-radius: 6px; +} + +.pm-li-drop-outdent::before { + content: ''; + position: absolute; + inset-block: 0; + inset-inline-start: 0; + width: 3px; + background: color-mix(in oklab, Highlight 35%, transparent); +} + +.pm-li--with-handle:has(.pm-li-drop-before), +.pm-li--with-handle:has(.pm-li-drop-after), +.pm-li--with-handle:has(.pm-li-drop-into), +.pm-li--with-handle:has(.pm-li-drop-outdent) { + background: var(--pm-fill-ancestor); + border-radius: 6px; +} + +.pm-li-drop-before, +.pm-li-drop-after, +.pm-li-drop-into, +.pm-li-drop-outdent { + position: relative; + z-index: 0; +} + +#note-content-container .ProseMirror { + padding-bottom: 2rem; /* space for the bottom toolbar */ +} diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..f59b884c51ed3c31fc0738fd38d0d75b580df5e4 --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,12 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface Platform {} + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000000000000000000000000000000000000..285164d02f67c19a93297069ad98b5667a3c447e --- /dev/null +++ b/src/app.html @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + + + + Open WebUI + + %sveltekit.head% + + + +
      %sveltekit.body%
      + +
      + + +
      + + +
      +
      + +
      +
      +
      + + +
      + + + + diff --git a/src/lib/apis/analytics/index.ts b/src/lib/apis/analytics/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..6bab2cbf818f956f80fae85befcbb26d6c5e36bf --- /dev/null +++ b/src/lib/apis/analytics/index.ts @@ -0,0 +1,319 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const getModelAnalytics = async ( + token: string = '', + startDate: number | null = null, + endDate: number | null = null, + groupId: string | null = null +) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (startDate) searchParams.append('start_date', startDate.toString()); + if (endDate) searchParams.append('end_date', endDate.toString()); + if (groupId) searchParams.append('group_id', groupId); + + const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/models?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUserAnalytics = async ( + token: string = '', + startDate: number | null = null, + endDate: number | null = null, + limit: number = 50, + groupId: string | null = null +) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (startDate) searchParams.append('start_date', startDate.toString()); + if (endDate) searchParams.append('end_date', endDate.toString()); + if (limit) searchParams.append('limit', limit.toString()); + if (groupId) searchParams.append('group_id', groupId); + + const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/users?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getMessages = async ( + token: string = '', + modelId: string | null = null, + userId: string | null = null, + chatId: string | null = null, + startDate: number | null = null, + endDate: number | null = null, + skip: number = 0, + limit: number = 50 +) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (modelId) searchParams.append('model_id', modelId); + if (userId) searchParams.append('user_id', userId); + if (chatId) searchParams.append('chat_id', chatId); + if (startDate) searchParams.append('start_date', startDate.toString()); + if (endDate) searchParams.append('end_date', endDate.toString()); + if (skip) searchParams.append('skip', skip.toString()); + if (limit) searchParams.append('limit', limit.toString()); + + const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/messages?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getSummary = async ( + token: string = '', + startDate: number | null = null, + endDate: number | null = null, + groupId: string | null = null +) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (startDate) searchParams.append('start_date', startDate.toString()); + if (endDate) searchParams.append('end_date', endDate.toString()); + if (groupId) searchParams.append('group_id', groupId); + + const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/summary?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getDailyStats = async ( + token: string = '', + startDate: number | null = null, + endDate: number | null = null, + granularity: 'hourly' | 'daily' = 'daily', + groupId: string | null = null +) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (startDate) searchParams.append('start_date', startDate.toString()); + if (endDate) searchParams.append('end_date', endDate.toString()); + searchParams.append('granularity', granularity); + if (groupId) searchParams.append('group_id', groupId); + + const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/daily?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getTokenUsage = async ( + token: string = '', + startDate: number | null = null, + endDate: number | null = null, + groupId: string | null = null +) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (startDate) searchParams.append('start_date', startDate.toString()); + if (endDate) searchParams.append('end_date', endDate.toString()); + if (groupId) searchParams.append('group_id', groupId); + + const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/tokens?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getModelChats = async ( + token: string = '', + modelId: string, + startDate: number | null = null, + endDate: number | null = null, + skip: number = 0, + limit: number = 50 +) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (startDate) searchParams.append('start_date', startDate.toString()); + if (endDate) searchParams.append('end_date', endDate.toString()); + if (skip) searchParams.append('skip', skip.toString()); + if (limit) searchParams.append('limit', limit.toString()); + + const res = await fetch( + `${WEBUI_API_BASE_URL}/analytics/models/${encodeURIComponent(modelId)}/chats?${searchParams.toString()}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getModelOverview = async (token: string = '', modelId: string, days: number = 30) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('days', days.toString()); + + const res = await fetch( + `${WEBUI_API_BASE_URL}/analytics/models/${encodeURIComponent(modelId)}/overview?${searchParams.toString()}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/audio/index.ts b/src/lib/apis/audio/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b2fed5739f2ab5c40e93d6c522529d28aaf2c448 --- /dev/null +++ b/src/lib/apis/audio/index.ts @@ -0,0 +1,196 @@ +import { AUDIO_API_BASE_URL } from '$lib/constants'; + +export const getAudioConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${AUDIO_API_BASE_URL}/config`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type OpenAIConfigForm = { + url: string; + key: string; + model: string; + speaker: string; +}; + +export const updateAudioConfig = async (token: string, payload: OpenAIConfigForm) => { + let error = null; + + const res = await fetch(`${AUDIO_API_BASE_URL}/config/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...payload + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const transcribeAudio = async (token: string, file: File, language?: string) => { + const data = new FormData(); + data.append('file', file); + if (language) { + data.append('language', language); + } + + let error = null; + const res = await fetch(`${AUDIO_API_BASE_URL}/transcriptions`, { + method: 'POST', + headers: { + Accept: 'application/json', + authorization: `Bearer ${token}` + }, + body: data + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const synthesizeOpenAISpeech = async ( + token: string = '', + speaker: string = 'alloy', + text: string = '', + model?: string +) => { + let error = null; + + const res = await fetch(`${AUDIO_API_BASE_URL}/speech`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + input: text, + voice: speaker, + ...(model && { model }) + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res; + }) + .catch((err) => { + error = err.detail; + console.error(err); + + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +interface AvailableModelsResponse { + models: { name: string; id: string }[] | { id: string }[]; +} + +export const getModels = async (token: string = ''): Promise => { + let error = null; + + const res = await fetch(`${AUDIO_API_BASE_URL}/models`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getVoices = async (token: string = '') => { + let error = null; + + const res = await fetch(`${AUDIO_API_BASE_URL}/voices`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/auths/index.ts b/src/lib/apis/auths/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b8494ceedf2e732a86ce4ca88ffe21699baeb3a7 --- /dev/null +++ b/src/lib/apis/auths/index.ts @@ -0,0 +1,744 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const getAdminDetails = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/details`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getAdminConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/config`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateAdminConfig = async (token: string, body: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/config`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify(body) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getSessionUser = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + credentials: 'include' + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const ldapUserSignIn = async (user: string, password: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/ldap`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include', + body: JSON.stringify({ + user: user, + password: password + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getLdapConfig = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/config/ldap`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateLdapConfig = async (token: string = '', enable_ldap: boolean) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/config/ldap`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + enable_ldap: enable_ldap + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getLdapServer = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/config/ldap/server`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateLdapServer = async (token: string = '', body: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/config/ldap/server`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify(body) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const userSignIn = async (email: string, password: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signin`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include', + body: JSON.stringify({ + email: email, + password: password + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const userSignUp = async ( + name: string, + email: string, + password: string, + profile_image_url: string +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include', + body: JSON.stringify({ + name: name, + email: email, + password: password, + profile_image_url: profile_image_url + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const userSignOut = async () => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signout`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include' + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + sessionStorage.clear(); + return res; +}; + +export const addUser = async ( + token: string, + name: string, + email: string, + password: string, + role: string = 'pending', + profile_image_url: null | string = null +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/add`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + name: name, + email: email, + password: password, + role: role, + ...(profile_image_url && { profile_image_url: profile_image_url }) + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateUserProfile = async (token: string, profile: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/update/profile`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + ...profile + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + if (Array.isArray(error)) { + error = error.map((e: { msg?: string }) => e.msg).join('; '); + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateUserTimezone = async (token: string, timezone: string) => { + await fetch(`${WEBUI_API_BASE_URL}/auths/update/timezone`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ timezone }) + }).catch((err) => { + console.error('Failed to update timezone:', err); + }); +}; + +export const updateUserPassword = async (token: string, password: string, newPassword: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/update/password`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + password: password, + new_password: newPassword + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getSignUpEnabledStatus = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup/enabled`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getDefaultUserRole = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup/user/role`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateDefaultUserRole = async (token: string, role: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup/user/role`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + role: role + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const toggleSignUpEnabledStatus = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup/enabled/toggle`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getJWTExpiresDuration = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/token/expires`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateJWTExpiresDuration = async (token: string, duration: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/token/expires/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + duration: duration + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const createAPIKey = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + if (error) { + throw error; + } + return res.api_key; +}; + +export const getAPIKey = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + if (error) { + throw error; + } + return res.api_key; +}; + +export const deleteAPIKey = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + if (error) { + throw error; + } + return res; +}; + +export const deleteOAuthSession = async (token: string, provider: string) => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/auths/oauth/sessions/${encodeURIComponent(provider)}`, + { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/automations/index.ts b/src/lib/apis/automations/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..a79fe1ddc8655f574f2d6dcb6db49c1b6201acb5 --- /dev/null +++ b/src/lib/apis/automations/index.ts @@ -0,0 +1,300 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export type AutomationTerminalConfig = { + server_id: string; + cwd?: string; +}; + +export type AutomationData = { + prompt: string; + model_id: string; + rrule: string; + terminal?: AutomationTerminalConfig; +}; + +export type AutomationForm = { + name: string; + data: AutomationData; + meta?: { + system_prompt?: string; + temperature?: number; + max_tokens?: number; + webhook?: string; + }; + is_active?: boolean; +}; + +export type AutomationRunModel = { + id: string; + automation_id: string; + chat_id: string | null; + status: string; + error: string | null; + created_at: number; +}; + +export type AutomationResponse = { + id: string; + user_id: string; + name: string; + data: AutomationData; + meta: Record | null; + is_active: boolean; + last_run_at: number | null; + next_run_at: number | null; + + created_at: number; + updated_at: number; + last_run: AutomationRunModel | null; + next_runs: number[] | null; +}; + +export const getAutomationItems = async ( + token: string, + query: string | null, + status: string | null, + page: number +): Promise<{ items: AutomationResponse[]; total: number }> => { + let error = null; + + const searchParams = new URLSearchParams(); + if (query) { + searchParams.append('query', query); + } + if (status && status !== 'all') { + searchParams.append('status', status); + } + if (page) { + searchParams.append('page', page.toString()); + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/automations/list?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const createAutomation = async (token: string, form: AutomationForm) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/automations/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify(form) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getAutomationById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/automations/${id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateAutomationById = async (token: string, id: string, form: AutomationForm) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/automations/${id}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify(form) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const toggleAutomationById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/automations/${id}/toggle`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const runAutomationById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/automations/${id}/run`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteAutomationById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/automations/${id}/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getAutomationRuns = async ( + token: string, + id: string, + skip: number = 0, + limit: number = 50 +) => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/automations/${id}/runs?skip=${skip}&limit=${limit}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/calendar/index.ts b/src/lib/apis/calendar/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b3148e6c186fc5acc599b370508081767f86bc68 --- /dev/null +++ b/src/lib/apis/calendar/index.ts @@ -0,0 +1,458 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export type CalendarModel = { + id: string; + user_id: string; + name: string; + color: string | null; + is_default: boolean; + is_system: boolean; + data: Record | null; + meta: Record | null; + access_grants: any[]; + created_at: number; + updated_at: number; +}; + +export type CalendarEventAttendeeModel = { + id: string; + event_id: string; + user_id: string; + status: string; + meta: Record | null; + created_at: number; + updated_at: number; +}; + +export type CalendarEventModel = { + id: string; + calendar_id: string; + user_id: string; + title: string; + description: string | null; + start_at: number; + end_at: number | null; + all_day: boolean; + rrule: string | null; + color: string | null; + location: string | null; + data: Record | null; + meta: Record | null; + is_cancelled: boolean; + attendees: CalendarEventAttendeeModel[]; + created_at: number; + updated_at: number; + // Set by expand_recurring_event for recurring instances + instance_id?: string; +}; + +export type CalendarEventForm = { + calendar_id: string; + title: string; + description?: string; + start_at: number; + end_at?: number; + all_day?: boolean; + rrule?: string; + color?: string; + location?: string; + data?: Record; + meta?: Record; + attendees?: { user_id: string; status?: string }[]; +}; + +export type CalendarForm = { + name: string; + color?: string; + data?: Record; + meta?: Record; + access_grants?: { target_type: string; target_id: string; permission: string }[]; +}; + +// ── Calendars ───────────────────────────────── + +export const getCalendars = async (token: string): Promise => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/calendars/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const createCalendar = async (token: string, form: CalendarForm): Promise => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/calendars/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify(form) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateCalendar = async ( + token: string, + calendarId: string, + form: Partial +): Promise => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/calendars/${calendarId}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify(form) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteCalendar = async (token: string, calendarId: string): Promise => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/calendars/${calendarId}/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res?.status ?? false; +}; + +export const setDefaultCalendar = async ( + token: string, + calendarId: string +): Promise => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/calendars/${calendarId}/default`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +// ── Events ───────────────────────────────── + +export const getCalendarEvents = async ( + token: string, + start: string, + end: string, + calendarIds?: string[] +): Promise => { + let error = null; + + const params = new URLSearchParams(); + params.append('start', start); + params.append('end', end); + if (calendarIds && calendarIds.length > 0) { + params.append('calendar_ids', calendarIds.join(',')); + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/calendars/events?${params.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const createCalendarEvent = async ( + token: string, + form: CalendarEventForm +): Promise => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/calendars/events/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify(form) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getCalendarEventById = async ( + token: string, + eventId: string +): Promise => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/calendars/events/${eventId}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateCalendarEvent = async ( + token: string, + eventId: string, + form: Partial +): Promise => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/calendars/events/${eventId}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify(form) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteCalendarEvent = async (token: string, eventId: string): Promise => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/calendars/events/${eventId}/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res?.status ?? false; +}; + +export const rsvpCalendarEvent = async ( + token: string, + eventId: string, + status: string +): Promise<{ status: boolean; rsvp: string }> => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/calendars/events/${eventId}/rsvp`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ status }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const searchCalendarEvents = async ( + token: string, + query: string | null, + skip: number = 0, + limit: number = 30 +): Promise<{ items: CalendarEventModel[]; total: number }> => { + let error = null; + + const params = new URLSearchParams(); + if (query) params.append('query', query); + params.append('skip', skip.toString()); + params.append('limit', limit.toString()); + + const res = await fetch(`${WEBUI_API_BASE_URL}/calendars/events/search?${params.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/channels/index.ts b/src/lib/apis/channels/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5715c64e89663040674f98ef5e5f5c9a23a4b56a --- /dev/null +++ b/src/lib/apis/channels/index.ts @@ -0,0 +1,918 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +type ChannelForm = { + type?: string; + name: string; + is_private?: boolean | null; + data?: object; + meta?: object; + access_grants?: object[]; + group_ids?: string[]; + user_ids?: string[]; +}; + +export const createNewChannel = async (token: string = '', channel: ChannelForm) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/channels/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ ...channel }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChannels = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/channels/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChannelById = async (token: string = '', channel_id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getDMChannelByUserId = async (token: string = '', user_id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/channels/users/${user_id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChannelMembersById = async ( + token: string, + channel_id: string, + query?: string, + orderBy?: string, + direction?: string, + page = 1 +) => { + let error = null; + let res = null; + + const searchParams = new URLSearchParams(); + + searchParams.set('page', `${page}`); + + if (query) { + searchParams.set('query', query); + } + + if (orderBy) { + searchParams.set('order_by', orderBy); + } + + if (direction) { + searchParams.set('direction', direction); + } + + res = await fetch( + `${WEBUI_API_BASE_URL}/channels/${channel_id}/members?${searchParams.toString()}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateChannelMemberActiveStatusById = async ( + token: string = '', + channel_id: string, + is_active: boolean +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/members/active`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ is_active }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type UpdateMembersForm = { + user_ids?: string[]; + group_ids?: string[]; +}; + +export const addMembersById = async ( + token: string = '', + channel_id: string, + formData: UpdateMembersForm +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/update/members/add`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ ...formData }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type RemoveMembersForm = { + user_ids?: string[]; + group_ids?: string[]; +}; + +export const removeMembersById = async ( + token: string = '', + channel_id: string, + formData: RemoveMembersForm +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/update/members/remove`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ ...formData }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateChannelById = async ( + token: string = '', + channel_id: string, + channel: ChannelForm +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ ...channel }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteChannelById = async (token: string = '', channel_id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChannelMessages = async ( + token: string = '', + channel_id: string, + skip: number = 0, + limit: number = 50 +) => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/channels/${channel_id}/messages?skip=${skip}&limit=${limit}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChannelPinnedMessages = async ( + token: string = '', + channel_id: string, + page: number = 1 +) => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/pinned?page=${page}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChannelThreadMessages = async ( + token: string = '', + channel_id: string, + message_id: string, + skip: number = 0, + limit: number = 50 +) => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/${message_id}/thread?skip=${skip}&limit=${limit}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getMessageData = async ( + token: string = '', + channel_id: string, + message_id: string +) => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/${message_id}/data`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type MessageForm = { + temp_id?: string; + reply_to_id?: string; + parent_id?: string; + content: string; + data?: object; + meta?: object; +}; + +export const sendMessage = async (token: string = '', channel_id: string, message: MessageForm) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/post`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ ...message }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const pinMessage = async ( + token: string = '', + channel_id: string, + message_id: string, + is_pinned: boolean +) => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/${message_id}/pin`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ is_pinned }) + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateMessage = async ( + token: string = '', + channel_id: string, + message_id: string, + message: MessageForm +) => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/${message_id}/update`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ ...message }) + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const addReaction = async ( + token: string = '', + channel_id: string, + message_id: string, + name: string +) => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/${message_id}/reactions/add`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ name }) + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const removeReaction = async ( + token: string = '', + channel_id: string, + message_id: string, + name: string +) => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/${message_id}/reactions/remove`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ name }) + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteMessage = async (token: string = '', channel_id: string, message_id: string) => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/channels/${channel_id}/messages/${message_id}/delete`, + { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +// Webhook API functions + +type WebhookForm = { + name: string; + profile_image_url?: string; +}; + +export const getChannelWebhooks = async (token: string = '', channel_id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/webhooks`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const createChannelWebhook = async ( + token: string = '', + channel_id: string, + formData: WebhookForm +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/webhooks/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ ...formData }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateChannelWebhook = async ( + token: string = '', + channel_id: string, + webhook_id: string, + formData: WebhookForm +) => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/channels/${channel_id}/webhooks/${webhook_id}/update`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ ...formData }) + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteChannelWebhook = async ( + token: string = '', + channel_id: string, + webhook_id: string +) => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/channels/${channel_id}/webhooks/${webhook_id}/delete`, + { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..028371386c89e48221fe7079f376e144d612ccbb --- /dev/null +++ b/src/lib/apis/chats/index.ts @@ -0,0 +1,1384 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; +import { getTimeRange } from '$lib/utils'; + +export const createNewChat = async (token: string, chat: object, folderId: string | null) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/new`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + chat: chat, + folder_id: folderId ?? null + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const unarchiveAllChats = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/unarchive/all`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const importChats = async (token: string, chats: object[]) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/import`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + chats + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChatList = async ( + token: string = '', + page: number | null = null, + include_pinned: boolean = false, + include_folders: boolean = false +) => { + let error = null; + const searchParams = new URLSearchParams(); + + if (page !== null) { + searchParams.append('page', `${page}`); + } + + if (include_folders) { + searchParams.append('include_folders', 'true'); + } + + if (include_pinned) { + searchParams.append('include_pinned', 'true'); + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + if (!res) { + return []; + } + + return res.map((chat) => ({ + ...chat, + time_range: getTimeRange(chat.updated_at) + })); +}; + +export const getChatListByUserId = async ( + token: string = '', + userId: string, + page: number = 1, + filter?: object +) => { + let error = null; + + const searchParams = new URLSearchParams(); + + searchParams.append('page', `${page}`); + + if (filter) { + Object.entries(filter).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + searchParams.append(key, value.toString()); + } + }); + } + + const res = await fetch( + `${WEBUI_API_BASE_URL}/chats/list/user/${userId}?${searchParams.toString()}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res.map((chat) => ({ + ...chat, + time_range: getTimeRange(chat.updated_at) + })); +}; + +export const getArchivedChatList = async ( + token: string = '', + page: number = 1, + filter?: object +) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('page', `${page}`); + + if (filter) { + Object.entries(filter).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + searchParams.append(key, value.toString()); + } + }); + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/archived?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res.map((chat) => ({ + ...chat, + time_range: getTimeRange(chat.updated_at) + })); +}; + +export const getSharedChatList = async (token: string = '', page: number = 1, filter?: object) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('page', `${page}`); + + if (filter) { + Object.entries(filter).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + searchParams.append(key, value.toString()); + } + }); + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/shared?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res.map((chat) => ({ + ...chat, + time_range: getTimeRange(chat.updated_at) + })); +}; + +export const getAllChats = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/all`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChatListBySearchText = async (token: string, text: string, page: number = 1) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('text', text); + searchParams.append('page', `${page}`); + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/search?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res.map((chat) => ({ + ...chat, + time_range: getTimeRange(chat.updated_at) + })); +}; + +export const getChatsByFolderId = async (token: string, folderId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/folder/${folderId}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChatListByFolderId = async (token: string, folderId: string, page: number = 1) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (page !== null) { + searchParams.append('page', `${page}`); + } + + const res = await fetch( + `${WEBUI_API_BASE_URL}/chats/folder/${folderId}/list?${searchParams.toString()}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getAllArchivedChats = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/all/archived`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getAllUserChats = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/all/db`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getAllTags = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/all/tags`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getPinnedChatList = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/pinned`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res.map((chat) => ({ + ...chat, + time_range: getTimeRange(chat.updated_at) + })); +}; + +export const getChatListByTagName = async (token: string = '', tagName: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/tags`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + name: tagName + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res.map((chat) => ({ + ...chat, + time_range: getTimeRange(chat.updated_at) + })); +}; + +export const getChatById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChatByShareId = async (token: string, share_id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/share/${share_id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChatPinnedStatusById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/pinned`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const toggleChatPinnedStatusById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/pin`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const cloneChatById = async (token: string, id: string, title?: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/clone`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + ...(title && { title: title }) + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const cloneSharedChatById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/clone/shared`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const shareChatById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateChatFolderIdById = async (token: string, id: string, folderId?: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/folder`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + folder_id: folderId + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const archiveChatById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/archive`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteSharedChatById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateChatAccessGrants = async (token: string, id: string, accessGrants: object[]) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/shared/${id}/access/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + access_grants: accessGrants + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChatAccessGrants = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/shared/${id}/access`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateChatById = async (token: string, id: string, chat: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + chat: chat + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteChatById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getTagsById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/tags`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const addTagById = async (token: string, id: string, tagName: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/tags`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + name: tagName + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteTagById = async (token: string, id: string, tagName: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/tags`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + name: tagName + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; +export const deleteTagsById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/tags/all`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteAllChats = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const archiveAllChats = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/archive/all`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; +export const exportChatStats = async (token: string, page: number = 1, params: object = {}) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('page', `${page}`); + + if (params) { + for (const [key, value] of Object.entries(params)) { + searchParams.append(key, `${value}`); + } + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/stats/export?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const exportSingleChatStats = async (token: string, chatId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/stats/export/${chatId}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const downloadChatStats = async ( + token: string = '', + updated_at: number | null = null +): Promise<[Response | null, AbortController]> => { + const controller = new AbortController(); + let error = null; + + let url = `${WEBUI_API_BASE_URL}/chats/stats/export?stream=true`; + if (updated_at) url += `&updated_at=${updated_at}`; + + const res = await fetch(url, { + signal: controller.signal, + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }).catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return [res, controller]; +}; diff --git a/src/lib/apis/configs/index.ts b/src/lib/apis/configs/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..6b7bf6f47bf44e2404f6bd4f9a2cf50ddc6fc22f --- /dev/null +++ b/src/lib/apis/configs/index.ts @@ -0,0 +1,649 @@ +import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; +import type { Banner } from '$lib/types'; + +export const importConfig = async (token: string, config) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/import`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + config: config + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const exportConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/export`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getConnectionsConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/connections`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const setConnectionsConfig = async (token: string, config: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/connections`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...config + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getToolServerConnections = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/tool_servers`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const setToolServerConnections = async (token: string, connections: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/tool_servers`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...connections + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getTerminalServerConnections = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/terminal_servers`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const setTerminalServerConnections = async (token: string, connections: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/terminal_servers`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...connections + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +/** + * Detect whether a terminal server URL points to an Orchestrator or a direct + * Open Terminal instance. + * + * - GET {url}/api/v1/policies → 200 → "orchestrator" + * - GET {url}/api/config → 200 → "terminal" + * - Neither → null + */ +export const detectTerminalServerType = async ( + url: string, + key: string +): Promise<'orchestrator' | 'terminal' | null> => { + const baseUrl = url.replace(/\/$/, ''); + const headers: Record = {}; + if (key) { + headers['Authorization'] = `Bearer ${key}`; + } + + // Orchestrators expose a policies API; plain terminals don't. + try { + const res = await fetch(`${baseUrl}/api/v1/policies`, { headers }); + if (res.ok) return 'orchestrator'; + } catch { + // ignore + } + + // Fall back to open-terminal config endpoint. + try { + const res = await fetch(`${baseUrl}/api/config`, { headers }); + if (res.ok) return 'terminal'; + } catch { + // ignore + } + + return null; +}; + +/** + * Create or update a policy on the orchestrator. + * Proxied through the Open WebUI backend to keep API keys server-side. + */ +export const putOrchestratorPolicy = async ( + token: string, + url: string, + key: string, + policyId: string, + policyData: object +): Promise => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/terminal_servers/policy`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + url: url.replace(/\/$/, ''), + key, + policy_id: policyId, + policy_data: policyData + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +/** + * Verify a terminal server connection via the backend proxy. + * Used for system/admin connections to avoid CORS issues and API key exposure. + */ +export const verifyTerminalServerConnection = async (token: string, connection: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/terminal_servers/verify`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...connection + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const verifyToolServerConnection = async (token: string, connection: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/tool_servers/verify`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...connection + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type RegisterOAuthClientForm = { + url: string; + client_id: string; + client_name?: string; + client_secret?: string; +}; + +export const registerOAuthClient = async ( + token: string, + formData: RegisterOAuthClientForm, + type: null | string = null +) => { + let error = null; + + const searchParams = type ? `?type=${type}` : ''; + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/oauth/clients/register${searchParams}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...formData + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getOAuthClientAuthorizationUrl = (clientId: string, type: null | string = null) => { + const oauthClientId = type ? `${type}:${clientId}` : clientId; + return `${WEBUI_BASE_URL}/oauth/clients/${oauthClientId}/authorize`; +}; + +export const getCodeExecutionConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_execution`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const setCodeExecutionConfig = async (token: string, config: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_execution`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...config + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getModelsDefaults = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/models/defaults`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getModelsConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/models`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const setModelsConfig = async (token: string, config: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/models`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...config + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const setDefaultPromptSuggestions = async (token: string, promptSuggestions: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/suggestions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + suggestions: promptSuggestions + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getBanners = async (token: string): Promise => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/banners`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const setBanners = async (token: string, banners: Banner[]) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/banners`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + banners: banners + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/evaluations/index.ts b/src/lib/apis/evaluations/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..bfb6955bcff708848841867db787e94eacc6a33e --- /dev/null +++ b/src/lib/apis/evaluations/index.ts @@ -0,0 +1,394 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const getConfig = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/config`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateConfig = async (token: string, config: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/config`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...config + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getAllFeedbacks = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/feedbacks/all`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getLeaderboard = async (token: string = '', query: string = '') => { + let error = null; + + const searchParams = new URLSearchParams(); + if (query) searchParams.append('query', query); + + const res = await fetch( + `${WEBUI_API_BASE_URL}/evaluations/leaderboard?${searchParams.toString()}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getModelHistory = async (token: string = '', modelId: string, days: number = 30) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('days', days.toString()); + + const res = await fetch( + `${WEBUI_API_BASE_URL}/evaluations/leaderboard/${encodeURIComponent(modelId)}/history?${searchParams.toString()}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFeedbackModelIds = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/feedbacks/models`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFeedbackItems = async ( + token: string = '', + orderBy, + direction, + page, + modelId: string = '' +) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (orderBy) searchParams.append('order_by', orderBy); + if (direction) searchParams.append('direction', direction); + if (page) searchParams.append('page', page.toString()); + if (modelId) searchParams.append('model_id', modelId); + + const res = await fetch( + `${WEBUI_API_BASE_URL}/evaluations/feedbacks/list?${searchParams.toString()}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const exportAllFeedbacks = async (token: string = '', modelId: string = '') => { + let error = null; + + const searchParams = new URLSearchParams(); + if (modelId) searchParams.append('model_id', modelId); + + const res = await fetch( + `${WEBUI_API_BASE_URL}/evaluations/feedbacks/all/export?${searchParams.toString()}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const createNewFeedback = async (token: string, feedback: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/feedback`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...feedback + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFeedbackById = async (token: string, feedbackId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/feedback/${feedbackId}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateFeedbackById = async (token: string, feedbackId: string, feedback: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/feedback/${feedbackId}`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...feedback + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteFeedbackById = async (token: string, feedbackId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/feedback/${feedbackId}`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/files/index.ts b/src/lib/apis/files/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..15785b354d6a6039867513a4df6d59d05f17d7fd --- /dev/null +++ b/src/lib/apis/files/index.ts @@ -0,0 +1,369 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; +import { splitStream } from '$lib/utils'; + +export const uploadFile = async ( + token: string, + file: File, + metadata?: object | null, + process?: boolean | null +) => { + const data = new FormData(); + data.append('file', file); + if (metadata) { + data.append('metadata', JSON.stringify(metadata)); + } + + const searchParams = new URLSearchParams(); + if (process !== undefined && process !== null) { + searchParams.append('process', String(process)); + } + + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/files/?${searchParams.toString()}`, { + method: 'POST', + headers: { + Accept: 'application/json', + authorization: `Bearer ${token}` + }, + body: data + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail || err.message; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + if (res) { + const status = await getFileProcessStatus(token, res.id); + + if (status && status.ok) { + const reader = status.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(splitStream('\n')) + .getReader(); + + while (true) { + const { value, done } = await reader.read(); + if (done) { + break; + } + + try { + let lines = value.split('\n'); + + for (const line of lines) { + if (line !== '') { + console.log(line); + if (line === 'data: [DONE]') { + console.log(line); + } else { + let data = JSON.parse(line.replace(/^data: /, '')); + console.log(data); + + if (data?.error) { + console.error(data.error); + res.error = data.error; + } + + if (res?.data) { + res.data = data; + } + } + } + } + } catch (error) { + console.log(error); + } + } + } + } + + if (error) { + throw error; + } + + return res; +}; + +export const getFileProcessStatus = async (token: string, id: string) => { + const queryParams = new URLSearchParams(); + queryParams.append('stream', 'true'); + + let error = null; + const res = await fetch(`${WEBUI_API_BASE_URL}/files/${id}/process/status?${queryParams}`, { + method: 'GET', + headers: { + Accept: 'application/json', + authorization: `Bearer ${token}` + } + }).catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const uploadDir = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/files/upload/dir`, { + method: 'POST', + headers: { + Accept: 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFiles = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/files/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const searchFiles = async ( + token: string, + filename: string = '*', + skip: number = 0, + limit: number = 50 +) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('filename', filename); + searchParams.append('skip', String(skip)); + searchParams.append('limit', String(limit)); + + const res = await fetch(`${WEBUI_API_BASE_URL}/files/search?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return []; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFileById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/files/${id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateFileDataContentById = async (token: string, id: string, content: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/files/${id}/data/content/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + content: content + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFileContentById = async (id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/files/${id}/content`, { + method: 'GET', + headers: { + Accept: 'application/json' + }, + credentials: 'include' + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return await res.arrayBuffer(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteFileById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/files/${id}`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteAllFiles = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/files/all`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/folders/index.ts b/src/lib/apis/folders/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..af5405de3c221ba2759fdceddca6546d009fd2ec --- /dev/null +++ b/src/lib/apis/folders/index.ts @@ -0,0 +1,275 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +type FolderForm = { + name?: string; + data?: Record; + meta?: Record; + parent_id?: string | null; +}; + +export const createNewFolder = async (token: string, folderForm: FolderForm) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify(folderForm) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFolders = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFolderById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateFolderById = async (token: string, id: string, folderForm: FolderForm) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify(folderForm) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateFolderIsExpandedById = async ( + token: string, + id: string, + isExpanded: boolean +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/update/expanded`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + is_expanded: isExpanded + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateFolderParentIdById = async (token: string, id: string, parentId?: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/update/parent`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + parent_id: parentId + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type FolderItems = { + chat_ids: string[]; + file_ids: string[]; +}; + +export const updateFolderItemsById = async (token: string, id: string, items: FolderItems) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}/update/items`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + items: items + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteFolderById = async (token: string, id: string, deleteContents: boolean) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('delete_contents', deleteContents ? 'true' : 'false'); + + const res = await fetch(`${WEBUI_API_BASE_URL}/folders/${id}?${searchParams.toString()}`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/functions/index.ts b/src/lib/apis/functions/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..47346b4a2078e11703a3a00094e2e6a325529fa6 --- /dev/null +++ b/src/lib/apis/functions/index.ts @@ -0,0 +1,520 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const createNewFunction = async (token: string, func: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...func + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFunctions = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFunctionList = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/list`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const loadFunctionByUrl = async (token: string = '', url: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/load/url`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + url + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const exportFunctions = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/export`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFunctionById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateFunctionById = async (token: string, id: string, func: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...func + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteFunctionById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const toggleFunctionById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/toggle`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const toggleGlobalById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/toggle/global`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFunctionValvesById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/valves`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFunctionValvesSpecById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/valves/spec`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateFunctionValvesById = async (token: string, id: string, valves: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/valves/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...valves + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUserValvesById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/valves/user`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUserValvesSpecById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/valves/user/spec`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateUserValvesById = async (token: string, id: string, valves: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/valves/user/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...valves + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/groups/index.ts b/src/lib/apis/groups/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..6089a6023fd9dbea4369238fc4acfca30da30e29 --- /dev/null +++ b/src/lib/apis/groups/index.ts @@ -0,0 +1,269 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const createNewGroup = async (token: string, group: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/groups/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...group + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getGroups = async (token: string = '', share?: boolean) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (share !== undefined) { + searchParams.append('share', String(share)); + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/groups/?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getGroupById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/groups/id/${id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getGroupInfoById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/groups/id/${id}/info`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateGroupById = async (token: string, id: string, group: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/groups/id/${id}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...group + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteGroupById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/groups/id/${id}/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const addUserToGroup = async (token: string, id: string, userIds: string[]) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/groups/id/${id}/users/add`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + user_ids: userIds + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const removeUserFromGroup = async (token: string, id: string, userIds: string[]) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/groups/id/${id}/users/remove`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + user_ids: userIds + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/images/index.ts b/src/lib/apis/images/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5d87c13a18f2f94e5b19943e13deaaaf9775bb89 --- /dev/null +++ b/src/lib/apis/images/index.ts @@ -0,0 +1,290 @@ +import { IMAGES_API_BASE_URL } from '$lib/constants'; + +export const getConfig = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/config`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateConfig = async (token: string = '', config: object) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/config/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + ...config + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const verifyConfigUrl = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/config/url/verify`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getImageGenerationConfig = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/image/config`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateImageGenerationConfig = async (token: string = '', config: object) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/image/config/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ ...config }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getImageGenerationModels = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/models`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const imageGenerations = async (token: string = '', prompt: string) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/generations`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + prompt: prompt + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + if (Array.isArray(err.detail)) { + error = err.detail.map((e: { msg?: string }) => e.msg || JSON.stringify(e)).join(', '); + } else { + error = err.detail; + } + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const imageEdits = async ( + token: string = '', + images: string | string[], + prompt: string, + model?: string, + size?: string, + n?: number, + background?: string +) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/edit`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + form_data: { + image: images, + prompt, + ...(model && { model }), + ...(size && { size }), + ...(n && { n }), + ...(background && { background }) + } + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + if (Array.isArray(err.detail)) { + error = err.detail.map((e: { msg?: string }) => e.msg || JSON.stringify(e)).join(', '); + } else { + error = err.detail; + } + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5faf56d4d41a957554e21883c8f6bfb9fc2be43d --- /dev/null +++ b/src/lib/apis/index.ts @@ -0,0 +1,1826 @@ +import { WEBUI_BASE_URL } from '$lib/constants'; +import { convertOpenApiToToolPayload } from '$lib/utils'; +import { getOpenAIModelsDirect } from './openai'; + +const TOOL_SERVER_FETCH_TIMEOUT = 10000; + +// Every request sent from here is a petition. May it reach +// the one for whom it was intended, and return answered. +export const getModels = async ( + token: string = '', + connections: object | null = null, + base: boolean = false, + refresh: boolean = false +) => { + const searchParams = new URLSearchParams(); + if (refresh) { + searchParams.append('refresh', 'true'); + } + + let error = null; + const res = await fetch( + `${WEBUI_BASE_URL}/api/models${base ? '/base' : ''}?${searchParams.toString()}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + let models = res?.data ?? []; + + if (connections && !base) { + let localModels = []; + + if (connections) { + const OPENAI_API_BASE_URLS = connections.OPENAI_API_BASE_URLS; + const OPENAI_API_KEYS = connections.OPENAI_API_KEYS; + const OPENAI_API_CONFIGS = connections.OPENAI_API_CONFIGS; + + const requests = []; + for (const idx in OPENAI_API_BASE_URLS) { + const url = OPENAI_API_BASE_URLS[idx]; + + if (idx.toString() in OPENAI_API_CONFIGS) { + const apiConfig = OPENAI_API_CONFIGS[idx.toString()] ?? {}; + + const enable = apiConfig?.enable ?? true; + const modelIds = apiConfig?.model_ids ?? []; + + if (enable) { + if (modelIds.length > 0) { + const modelList = { + object: 'list', + data: modelIds.map((modelId) => ({ + id: modelId, + name: modelId, + owned_by: 'openai', + openai: { id: modelId }, + urlIdx: idx + })) + }; + + requests.push( + (async () => { + return modelList; + })() + ); + } else { + requests.push( + (async () => { + return await getOpenAIModelsDirect(url, OPENAI_API_KEYS[idx]) + .then((res) => { + return res; + }) + .catch((err) => { + return { + object: 'list', + data: [], + urlIdx: idx + }; + }); + })() + ); + } + } else { + requests.push( + (async () => { + return { + object: 'list', + data: [], + urlIdx: idx + }; + })() + ); + } + } + } + + const responses = await Promise.all(requests); + + for (const idx in responses) { + const response = responses[idx]; + const apiConfig = OPENAI_API_CONFIGS[idx.toString()] ?? {}; + + let models = Array.isArray(response) ? response : (response?.data ?? []); + models = models.map((model) => ({ ...model, openai: { id: model.id }, urlIdx: idx })); + + const prefixId = apiConfig.prefix_id; + if (prefixId) { + for (const model of models) { + model.id = `${prefixId}.${model.id}`; + } + } + + const tags = apiConfig.tags; + if (tags) { + for (const model of models) { + model.tags = tags; + } + } + + localModels = localModels.concat(models); + } + } + + models = models.concat( + localModels.map((model) => ({ + ...model, + name: model?.name ?? model?.id, + direct: true + })) + ); + + // Remove duplicates + const modelsMap = {}; + for (const model of models) { + modelsMap[model.id] = model; + } + + models = Object.values(modelsMap); + } + + return models; +}; + +type ChatCompletedForm = { + model: string; + messages: Record[]; + chat_id: string; + session_id: string | undefined; + id: string; + filter_ids?: string[]; + model_item?: unknown; +}; + +export const chatCompleted = async (token: string, body: ChatCompletedForm) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/chat/completed`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify(body) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type ChatActionForm = { + model: string; + messages: string[]; + chat_id: string; +}; + +export const chatAction = async (token: string, action_id: string, body: ChatActionForm) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/chat/actions/${action_id}`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify(body) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const stopTask = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/tasks/stop/${id}`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const stopTasksByChatId = async (token: string, chat_id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/tasks/chat/${encodeURIComponent(chat_id)}/stop`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getTaskIdsByChatId = async (token: string, chat_id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/tasks/chat/${encodeURIComponent(chat_id)}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getToolServerData = async (token: string, url: string) => { + let error = null; + + const res = await fetch(`${url}`, { + signal: AbortSignal.timeout(TOOL_SERVER_FETCH_TIMEOUT), + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + // Check if URL ends with .yaml or .yml to determine format + if (url.toLowerCase().endsWith('.yaml') || url.toLowerCase().endsWith('.yml')) { + if (!res.ok) throw await res.text(); + const [text, { parse }] = await Promise.all([res.text(), import('yaml')]); + return parse(text); + } else { + if (!res.ok) throw await res.json(); + return res.json(); + } + }) + .catch((err) => { + console.error(err); + if (err?.name === 'TimeoutError') { + error = `Connection to ${url} timed out`; + } else if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + console.log(res); + return res; +}; + +export const getToolServersData = async (servers: object[]) => { + return ( + await Promise.all( + servers + .filter((server) => server?.config?.enable) + .map(async (server) => { + let error = null; + + let toolServerToken = null; + + const auth_type = server?.auth_type ?? 'bearer'; + if (auth_type === 'bearer') { + toolServerToken = server?.key; + } else if (auth_type === 'none') { + // No authentication + } else if (auth_type === 'session') { + toolServerToken = localStorage.token; + } + + let res = null; + const specType = server?.spec_type ?? 'url'; + + if (specType === 'url') { + res = await getToolServerData( + toolServerToken, + (server?.path ?? '').includes('://') + ? server?.path + : `${server?.url}${(server?.path ?? '').startsWith('/') ? '' : '/'}${server?.path}` + ).catch((err) => { + error = err; + return null; + }); + } else if ((specType === 'json' && server?.spec) ?? null) { + try { + res = JSON.parse(server?.spec); + } catch (e) { + error = 'Failed to parse JSON spec'; + } + } + + if (res) { + if (!res.paths) { + return { + error: 'Invalid OpenAPI spec', + url: server?.url + }; + } + + const { openapi, info, specs } = { + openapi: res, + info: res.info, + specs: convertOpenApiToToolPayload(res) + }; + + const result: Record = { + url: server?.url, + openapi: openapi, + info: info, + specs: specs + }; + + // Fetch system prompt if the server supports it + try { + const baseUrl = (server?.url ?? '').replace(/\/$/, ''); + const configRes = await fetch(`${baseUrl}/api/config`, { + signal: AbortSignal.timeout(TOOL_SERVER_FETCH_TIMEOUT) + }); + if (configRes.ok) { + const config = await configRes.json(); + if (config?.features?.system) { + const headers: Record = {}; + if (toolServerToken) { + headers['Authorization'] = `Bearer ${toolServerToken}`; + } + const systemRes = await fetch(`${baseUrl}/system`, { + signal: AbortSignal.timeout(TOOL_SERVER_FETCH_TIMEOUT), + headers + }); + if (systemRes.ok) { + const systemData = await systemRes.json(); + if (systemData?.prompt) { + result.system_prompt = systemData.prompt; + } + } + } + } + } catch (e) { + // Server doesn't support /system — that's fine + } + + return result; + } else if (error) { + return { + error, + url: server?.url + }; + } else { + return null; + } + }) + ) + ).filter((server) => server); +}; + +export const executeToolServer = async ( + token: string, + url: string, + name: string, + params: Record, + serverData: { openapi: any; info: any; specs: any }, + sessionId?: string +) => { + let error = null; + + try { + // Find the matching operationId in the OpenAPI spec + const matchingRoute = Object.entries(serverData.openapi.paths).find(([_, methods]) => + Object.entries(methods as any).some(([__, operation]: any) => operation.operationId === name) + ); + + if (!matchingRoute) { + throw new Error(`No matching route found for operationId: ${name}`); + } + + const [routePath, methods] = matchingRoute; + + const methodEntry = Object.entries(methods as any).find( + ([_, operation]: any) => operation.operationId === name + ); + + if (!methodEntry) { + throw new Error(`No matching method found for operationId: ${name}`); + } + + const [httpMethod, operation]: [string, any] = methodEntry; + + // Split parameters by type + const pathParams: Record = {}; + const queryParams: Record = {}; + let bodyParams: any = {}; + + if (operation.parameters) { + operation.parameters.forEach((param: any) => { + const paramName = param?.name; + if (!paramName) return; + const paramIn = param?.in; + if (params.hasOwnProperty(paramName)) { + if (paramIn === 'path') { + pathParams[paramName] = params[paramName]; + } else if (paramIn === 'query') { + queryParams[paramName] = params[paramName]; + } + } + }); + } + + let finalUrl = `${url}${routePath}`; + + // Replace path parameters (`{param}`) + Object.entries(pathParams).forEach(([key, value]) => { + finalUrl = finalUrl.replace(new RegExp(`{${key}}`, 'g'), encodeURIComponent(value)); + }); + + // Append query parameters to URL if any + if (Object.keys(queryParams).length > 0) { + const queryString = new URLSearchParams( + Object.entries(queryParams).map(([k, v]) => [k, String(v)]) + ).toString(); + finalUrl += `?${queryString}`; + } + + // Handle requestBody composite + if (operation.requestBody && operation.requestBody.content) { + const contentType = Object.keys(operation.requestBody.content)[0]; + if (params !== undefined) { + bodyParams = params; + } else { + // Optional: Fallback or explicit error if body is expected but not provided + throw new Error(`Request body expected for operation '${name}' but none found.`); + } + } + + // Prepare headers and request options + const headers: Record = { + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }; + if (sessionId) headers['X-Session-Id'] = sessionId; + + const requestOptions: RequestInit = { + method: httpMethod.toUpperCase(), + headers + }; + + if ( + ['post', 'put', 'patch', 'delete'].includes(httpMethod.toLowerCase()) && + operation.requestBody + ) { + requestOptions.body = JSON.stringify(bodyParams); + } + + const res = await fetch(finalUrl, requestOptions); + if (!res.ok) { + const resText = await res.text(); + throw new Error(`HTTP error! Status: ${res.status}. Message: ${resText}`); + } + + // make a clone of res and extract headers + const responseHeaders = {}; + res.headers.forEach((value, key) => { + responseHeaders[key] = value; + }); + + let responseData; + const contentType = res.headers.get('Content-Type')?.split(';')[0]?.trim() ?? ''; + + try { + responseData = await res.clone().json(); + } catch { + if (contentType.startsWith('text/') || !contentType) { + responseData = await res.text(); + } else { + const buf = await res.arrayBuffer(); + const bytes = new Uint8Array(buf); + let binary = ''; + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + const b64 = btoa(binary); + responseData = `data:${contentType};base64,${b64}`; + } + } + return [responseData, responseHeaders]; + } catch (err: any) { + error = err.message; + console.error('API Request Error:', error); + return [{ error }, null]; + } +}; + +export const getTaskConfig = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/v1/tasks/config`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateTaskConfig = async (token: string, config: object) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/v1/tasks/config/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify(config) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const generateTitle = async ( + token: string = '', + model: string, + messages: object[], + chat_id?: string +) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/v1/tasks/title/completions`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + messages: messages, + ...(chat_id && { chat_id: chat_id }) + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } + return null; + }); + + if (error) { + throw error; + } + + try { + // Step 1: Safely extract the response string + const response = res?.choices[0]?.message?.content ?? ''; + + // Step 2: Attempt to fix common JSON format issues like single quotes + const sanitizedResponse = response.replace(/['‘’`]/g, '"'); // Convert single quotes to double quotes for valid JSON + + // Step 3: Find the relevant JSON block within the response + const jsonStartIndex = sanitizedResponse.indexOf('{'); + const jsonEndIndex = sanitizedResponse.lastIndexOf('}'); + + // Step 4: Check if we found a valid JSON block (with both `{` and `}`) + if (jsonStartIndex !== -1 && jsonEndIndex !== -1) { + const jsonResponse = sanitizedResponse.substring(jsonStartIndex, jsonEndIndex + 1); + + // Step 5: Parse the JSON block + const parsed = JSON.parse(jsonResponse); + + // Step 6: If there's a "tags" key, return the tags array; otherwise, return an empty array + if (parsed && parsed.title) { + return parsed.title; + } else { + return null; + } + } + + // If no valid JSON block found, return an empty array + return null; + } catch (e) { + // Catch and safely return empty array on any parsing errors + console.error('Failed to parse response: ', e); + return null; + } +}; + +export const generateFollowUps = async ( + token: string = '', + model: string, + messages: string, + chat_id?: string +) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/v1/tasks/follow_ups/completions`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + messages: messages, + ...(chat_id && { chat_id: chat_id }) + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } + return null; + }); + + if (error) { + throw error; + } + + try { + // Step 1: Safely extract the response string + const response = res?.choices[0]?.message?.content ?? ''; + + // Step 2: Attempt to fix common JSON format issues like single quotes + const sanitizedResponse = response.replace(/['‘’`]/g, '"'); // Convert single quotes to double quotes for valid JSON + + // Step 3: Find the relevant JSON block within the response + const jsonStartIndex = sanitizedResponse.indexOf('{'); + const jsonEndIndex = sanitizedResponse.lastIndexOf('}'); + + // Step 4: Check if we found a valid JSON block (with both `{` and `}`) + if (jsonStartIndex !== -1 && jsonEndIndex !== -1) { + const jsonResponse = sanitizedResponse.substring(jsonStartIndex, jsonEndIndex + 1); + + // Step 5: Parse the JSON block + const parsed = JSON.parse(jsonResponse); + + // Step 6: If there's a "follow_ups" key, return the follow_ups array; otherwise, return an empty array + if (parsed && parsed.follow_ups) { + return Array.isArray(parsed.follow_ups) ? parsed.follow_ups : []; + } else { + return []; + } + } + + // If no valid JSON block found, return an empty array + return []; + } catch (e) { + // Catch and safely return empty array on any parsing errors + console.error('Failed to parse response: ', e); + return []; + } +}; + +export const generateTags = async ( + token: string = '', + model: string, + messages: string, + chat_id?: string +) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/v1/tasks/tags/completions`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + messages: messages, + ...(chat_id && { chat_id: chat_id }) + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } + return null; + }); + + if (error) { + throw error; + } + + try { + // Step 1: Safely extract the response string + const response = res?.choices[0]?.message?.content ?? ''; + + // Step 2: Attempt to fix common JSON format issues like single quotes + const sanitizedResponse = response.replace(/['‘’`]/g, '"'); // Convert single quotes to double quotes for valid JSON + + // Step 3: Find the relevant JSON block within the response + const jsonStartIndex = sanitizedResponse.indexOf('{'); + const jsonEndIndex = sanitizedResponse.lastIndexOf('}'); + + // Step 4: Check if we found a valid JSON block (with both `{` and `}`) + if (jsonStartIndex !== -1 && jsonEndIndex !== -1) { + const jsonResponse = sanitizedResponse.substring(jsonStartIndex, jsonEndIndex + 1); + + // Step 5: Parse the JSON block + const parsed = JSON.parse(jsonResponse); + + // Step 6: If there's a "tags" key, return the tags array; otherwise, return an empty array + if (parsed && parsed.tags) { + return Array.isArray(parsed.tags) ? parsed.tags : []; + } else { + return []; + } + } + + // If no valid JSON block found, return an empty array + return []; + } catch (e) { + // Catch and safely return empty array on any parsing errors + console.error('Failed to parse response: ', e); + return []; + } +}; + +export const generateEmoji = async ( + token: string = '', + model: string, + prompt: string, + chat_id?: string +) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/v1/tasks/emoji/completions`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + prompt: prompt, + ...(chat_id && { chat_id: chat_id }) + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } + return null; + }); + + if (error) { + throw error; + } + + const response = res?.choices[0]?.message?.content.replace(/["']/g, '') ?? null; + + if (response) { + if (/\p{Extended_Pictographic}/u.test(response)) { + return response.match(/\p{Extended_Pictographic}/gu)[0]; + } + } + + return null; +}; + +export const generateQueries = async ( + token: string = '', + model: string, + messages: object[], + prompt: string, + type: string = 'web_search', + chat_id?: string +) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/v1/tasks/queries/completions`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + messages: messages, + prompt: prompt, + type: type, + ...(chat_id && { chat_id: chat_id }) + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } + return null; + }); + + if (error) { + throw error; + } + + // Step 1: Safely extract the response string + const response = res?.choices[0]?.message?.content ?? ''; + + try { + const jsonStartIndex = response.indexOf('{'); + const jsonEndIndex = response.lastIndexOf('}'); + + if (jsonStartIndex !== -1 && jsonEndIndex !== -1) { + const jsonResponse = response.substring(jsonStartIndex, jsonEndIndex + 1); + + // Step 5: Parse the JSON block + const parsed = JSON.parse(jsonResponse); + + // Step 6: If there's a "queries" key, return the queries array; otherwise, return an empty array + if (parsed && parsed.queries) { + return Array.isArray(parsed.queries) ? parsed.queries : []; + } else { + return []; + } + } + + // If no valid JSON block found, return response as is + return [response]; + } catch (e) { + // Catch and safely return empty array on any parsing errors + console.error('Failed to parse response: ', e); + return [response]; + } +}; + +export const generateAutoCompletion = async ( + token: string = '', + model: string, + prompt: string, + messages?: object[], + type: string = 'search query', + chat_id?: string +) => { + const controller = new AbortController(); + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/v1/tasks/auto/completions`, { + signal: controller.signal, + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + prompt: prompt, + ...(messages && { messages: messages }), + type: type, + stream: false, + ...(chat_id && { chat_id: chat_id }) + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } + return null; + }); + + if (error) { + throw error; + } + + const response = res?.choices[0]?.message?.content ?? ''; + + try { + const jsonStartIndex = response.indexOf('{'); + const jsonEndIndex = response.lastIndexOf('}'); + + if (jsonStartIndex !== -1 && jsonEndIndex !== -1) { + const jsonResponse = response.substring(jsonStartIndex, jsonEndIndex + 1); + + // Step 5: Parse the JSON block + const parsed = JSON.parse(jsonResponse); + + // Step 6: If there's a "queries" key, return the queries array; otherwise, return an empty array + if (parsed && parsed.text) { + return parsed.text; + } else { + return ''; + } + } + + // If no valid JSON block found, return response as is + return response; + } catch (e) { + // Catch and safely return empty array on any parsing errors + console.error('Failed to parse response: ', e); + return response; + } +}; + +export const generateMoACompletion = async ( + token: string = '', + model: string, + prompt: string, + responses: string[] +) => { + const controller = new AbortController(); + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/v1/tasks/moa/completions`, { + signal: controller.signal, + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + prompt: prompt, + responses: responses, + stream: true + }) + }).catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return [res, controller]; +}; + +export const getPipelinesList = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/v1/pipelines/list`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + const pipelines = res?.data ?? []; + return pipelines; +}; + +export const uploadPipeline = async (token: string, file: File, urlIdx: string) => { + let error = null; + + // Create a new FormData object to handle the file upload + const formData = new FormData(); + formData.append('file', file); + formData.append('urlIdx', urlIdx); + + const res = await fetch(`${WEBUI_BASE_URL}/api/v1/pipelines/upload`, { + method: 'POST', + headers: { + ...(token && { authorization: `Bearer ${token}` }) + // 'Content-Type': 'multipart/form-data' is not needed as Fetch API will set it automatically + }, + body: formData + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const downloadPipeline = async (token: string, url: string, urlIdx: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/v1/pipelines/add`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + url: url, + urlIdx: urlIdx + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deletePipeline = async (token: string, id: string, urlIdx: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/v1/pipelines/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + id: id, + urlIdx: urlIdx + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getPipelines = async (token: string, urlIdx?: string) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (urlIdx !== undefined) { + searchParams.append('urlIdx', urlIdx); + } + + const res = await fetch(`${WEBUI_BASE_URL}/api/v1/pipelines/?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + const pipelines = res?.data ?? []; + return pipelines; +}; + +export const getPipelineValves = async (token: string, pipeline_id: string, urlIdx: string) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (urlIdx !== undefined) { + searchParams.append('urlIdx', urlIdx); + } + + const res = await fetch( + `${WEBUI_BASE_URL}/api/v1/pipelines/${pipeline_id}/valves?${searchParams.toString()}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getPipelineValvesSpec = async (token: string, pipeline_id: string, urlIdx: string) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (urlIdx !== undefined) { + searchParams.append('urlIdx', urlIdx); + } + + const res = await fetch( + `${WEBUI_BASE_URL}/api/v1/pipelines/${pipeline_id}/valves/spec?${searchParams.toString()}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updatePipelineValves = async ( + token: string = '', + pipeline_id: string, + valves: object, + urlIdx: string +) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (urlIdx !== undefined) { + searchParams.append('urlIdx', urlIdx); + } + + const res = await fetch( + `${WEBUI_BASE_URL}/api/v1/pipelines/${pipeline_id}/valves/update?${searchParams.toString()}`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify(valves) + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUsage = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/usage`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getBackendConfig = async () => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/config`, { + method: 'GET', + credentials: 'include', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + // When a forward-auth proxy (e.g. Authentik/Traefik) intercepts the + // request and redirects to an external login page, the browser blocks + // the cross-origin redirect for fetch() and throws a TypeError. + // Detect this by re-fetching with redirect:"manual" — if the server + // responded with a redirect, the probe returns an opaque redirect + // response instead of throwing, confirming the backend is alive but + // an auth proxy is intercepting. + if (error instanceof TypeError) { + try { + const probeRes = await fetch(`${WEBUI_BASE_URL}/api/config`, { + method: 'GET', + credentials: 'include', + redirect: 'manual', + headers: { 'Content-Type': 'application/json' } + }); + if ( + probeRes.type === 'opaqueredirect' || + (probeRes.status >= 300 && probeRes.status < 400) + ) { + throw { authRedirect: true }; + } + } catch (probeErr: any) { + if (probeErr?.authRedirect) throw probeErr; + // Probe also failed — genuine network/backend issue + } + } + throw error; + } + + return res; +}; + +export const getChangelog = async () => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/changelog`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getVersion = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/version`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getVersionUpdates = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/version/updates`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getModelFilterConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/config/model/filter`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateModelFilterConfig = async ( + token: string, + enabled: boolean, + models: string[] +) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/config/model/filter`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + enabled: enabled, + models: models + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getWebhookUrl = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/webhook`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res.url; +}; + +export const updateWebhookUrl = async (token: string, url: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/webhook`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + url: url + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res.url; +}; + +export const getCommunitySharingEnabledStatus = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/community_sharing`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const toggleCommunitySharingEnabledStatus = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/community_sharing/toggle`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getModelConfig = async (token: string): Promise => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/config/models`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res.models; +}; + +export interface ModelConfig { + id: string; + name: string; + meta: ModelMeta; + base_model_id?: string; + params: ModelParams; +} + +export interface ModelMeta { + toolIds: never[]; + description?: string; + capabilities?: object; + profile_image_url?: string; +} + +export interface ModelParams {} + +export type GlobalModelConfig = ModelConfig[]; + +export const updateModelConfig = async (token: string, config: GlobalModelConfig) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/config/models`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + models: config + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/knowledge/index.ts b/src/lib/apis/knowledge/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f314bae634db47de1445f54118ac526ba3e24722 --- /dev/null +++ b/src/lib/apis/knowledge/index.ts @@ -0,0 +1,546 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const createNewKnowledge = async ( + token: string, + name: string, + description: string, + accessGrants: object[] +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: name, + description: description, + access_grants: accessGrants + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getKnowledgeBases = async (token: string = '', page: number | null = null) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (page) searchParams.append('page', page.toString()); + + const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const searchKnowledgeBases = async ( + token: string = '', + query: string | null = null, + viewOption: string | null = null, + page: number | null = null +) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (query) searchParams.append('query', query); + if (viewOption) searchParams.append('view_option', viewOption); + if (page) searchParams.append('page', page.toString()); + + const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/search?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const searchKnowledgeFiles = async ( + token: string, + query?: string | null = null, + viewOption?: string | null = null, + orderBy?: string | null = null, + direction?: string | null = null, + page: number = 1 +) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (query) searchParams.append('query', query); + if (viewOption) searchParams.append('view_option', viewOption); + if (orderBy) searchParams.append('order_by', orderBy); + if (direction) searchParams.append('direction', direction); + searchParams.append('page', page.toString()); + + const res = await fetch( + `${WEBUI_API_BASE_URL}/knowledge/search/files?${searchParams.toString()}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getKnowledgeById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const searchKnowledgeFilesById = async ( + token: string, + id: string, + query?: string | null = null, + viewOption?: string | null = null, + orderBy?: string | null = null, + direction?: string | null = null, + page: number = 1 +) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (query) searchParams.append('query', query); + if (viewOption) searchParams.append('view_option', viewOption); + if (orderBy) searchParams.append('order_by', orderBy); + if (direction) searchParams.append('direction', direction); + searchParams.append('page', page.toString()); + + const res = await fetch( + `${WEBUI_API_BASE_URL}/knowledge/${id}/files?${searchParams.toString()}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type KnowledgeUpdateForm = { + name?: string; + description?: string; + data?: object; + access_grants?: object[]; +}; + +export const updateKnowledgeById = async (token: string, id: string, form: KnowledgeUpdateForm) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: form?.name ? form.name : undefined, + description: form?.description ? form.description : undefined, + data: form?.data ? form.data : undefined, + access_grants: form.access_grants + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateKnowledgeAccessGrants = async ( + token: string, + id: string, + accessGrants: any[] +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/access/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ access_grants: accessGrants }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const addFileToKnowledgeById = async (token: string, id: string, fileId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/file/add`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + file_id: fileId + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateFileFromKnowledgeById = async (token: string, id: string, fileId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/file/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + file_id: fileId + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const removeFileFromKnowledgeById = async (token: string, id: string, fileId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/file/remove`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + file_id: fileId + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const resetKnowledgeById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/reset`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteKnowledgeById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const reindexKnowledgeFiles = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/reindex`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const exportKnowledgeById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/export`, { + method: 'GET', + headers: { + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.blob(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/memories/index.ts b/src/lib/apis/memories/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..d8fdc638fa61f6a13aa3ce6822d102fd9e2ca452 --- /dev/null +++ b/src/lib/apis/memories/index.ts @@ -0,0 +1,186 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const getMemories = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/memories/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const addNewMemory = async (token: string, content: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/memories/add`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + content: content + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateMemoryById = async (token: string, id: string, content: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/memories/${id}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + content: content + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const queryMemory = async (token: string, content: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/memories/query`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + content: content + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteMemoryById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/memories/${id}`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteMemoriesByUserId = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/memories/delete/user`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/models/index.ts b/src/lib/apis/models/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e7abaa309e4353397a6ff4ca874f35500060a910 --- /dev/null +++ b/src/lib/apis/models/index.ts @@ -0,0 +1,387 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const getModelItems = async ( + token: string = '', + query, + viewOption, + selectedTag, + orderBy, + direction, + page +) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (query) { + searchParams.append('query', query); + } + if (viewOption) { + searchParams.append('view_option', viewOption); + } + if (selectedTag) { + searchParams.append('tag', selectedTag); + } + if (orderBy) { + searchParams.append('order_by', orderBy); + } + if (direction) { + searchParams.append('direction', direction); + } + if (page) { + searchParams.append('page', page.toString()); + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/models/list?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getModelTags = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/models/tags`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const importModels = async (token: string, models: object[]) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/models/import`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ models: models }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getBaseModels = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/models/base`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const createNewModel = async (token: string, model: object) => { + let error = null; + + const { id, base_model_id, name, meta, params, access_grants, is_active } = model as any; + const payload = { id, base_model_id, name, meta, params, access_grants, is_active }; + + const res = await fetch(`${WEBUI_API_BASE_URL}/models/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify(payload) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getModelById = async (token: string, id: string) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('id', id); + + const res = await fetch(`${WEBUI_API_BASE_URL}/models/model?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const toggleModelById = async (token: string, id: string) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('id', id); + + const res = await fetch(`${WEBUI_API_BASE_URL}/models/model/toggle?${searchParams.toString()}`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateModelById = async (token: string, id: string, model: object) => { + let error = null; + + const { base_model_id, name, meta, params, access_grants, is_active } = model as any; + const payload = { id, base_model_id, name, meta, params, access_grants, is_active }; + + const res = await fetch(`${WEBUI_API_BASE_URL}/models/model/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify(payload) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateModelAccessGrants = async ( + token: string, + id: string, + name: string, + accessGrants: any[] +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/models/model/access/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ id, name, access_grants: accessGrants }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteModelById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/models/model/delete`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ id }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteAllModels = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/models/delete/all`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/notes/index.ts b/src/lib/apis/notes/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..80f0413bbca653983f551320d3405d63e2fa99ae --- /dev/null +++ b/src/lib/apis/notes/index.ts @@ -0,0 +1,377 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; +import { getTimeRange } from '$lib/utils'; + +type NoteItem = { + title: string; + data: object; + meta?: null | object; + access_grants?: object[]; +}; + +export const createNewNote = async (token: string, note: NoteItem) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...note + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getNotes = async (token: string = '', raw: boolean = false) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + if (raw) { + return res; // Return raw response if requested + } + + if (!Array.isArray(res)) { + return {}; // or throw new Error("Notes response is not an array") + } + + // Build the grouped object + const grouped: Record = {}; + for (const note of res) { + const timeRange = getTimeRange(note.updated_at / 1000000000); + if (!grouped[timeRange]) { + grouped[timeRange] = []; + } + grouped[timeRange].push({ + ...note, + timeRange + }); + } + + return grouped; +}; + +export const searchNotes = async ( + token: string = '', + query: string | null = null, + viewOption: string | null = null, + permission: string | null = null, + sortKey: string | null = null, + page: number | null = null +) => { + let error = null; + const searchParams = new URLSearchParams(); + + if (query !== null) { + searchParams.append('query', query); + } + + if (viewOption !== null) { + searchParams.append('view_option', viewOption); + } + + if (permission !== null) { + searchParams.append('permission', permission); + } + + if (sortKey !== null) { + searchParams.append('order_by', sortKey); + } + + if (page !== null) { + searchParams.append('page', `${page}`); + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/search?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getNoteList = async (token: string = '', page: number | null = null) => { + let error = null; + const searchParams = new URLSearchParams(); + + if (page !== null) { + searchParams.append('page', `${page}`); + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getNoteById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/${id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateNoteById = async (token: string, id: string, note: NoteItem) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/${id}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...note + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateNoteAccessGrants = async (token: string, id: string, accessGrants: any[]) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/${id}/access/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ access_grants: accessGrants }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteNoteById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/${id}/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getPinnedNoteList = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/pinned`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res ?? []; +}; + +export const toggleNotePinnedStatusById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/${id}/pin`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/ollama/index.ts b/src/lib/apis/ollama/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..a0f670d5e3fc025e549986e9019334de8258c1be --- /dev/null +++ b/src/lib/apis/ollama/index.ts @@ -0,0 +1,561 @@ +import { OLLAMA_API_BASE_URL } from '$lib/constants'; + +export const verifyOllamaConnection = async (token: string = '', connection: dict = {}) => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/verify`, { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + ...connection + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = `Ollama: ${err?.error?.message ?? 'Network Problem'}`; + return []; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getOllamaConfig = async (token: string = '') => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/config`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type OllamaConfig = { + ENABLE_OLLAMA_API: boolean; + OLLAMA_BASE_URLS: string[]; + OLLAMA_API_CONFIGS: object; +}; + +export const updateOllamaConfig = async (token: string = '', config: OllamaConfig) => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/config/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + ...config + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getOllamaUrls = async (token: string = '') => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/urls`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.OLLAMA_BASE_URLS; +}; + +export const updateOllamaUrls = async (token: string = '', urls: string[]) => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/urls/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + urls: urls + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.OLLAMA_BASE_URLS; +}; + +export const getOllamaVersion = async (token: string, urlIdx?: number) => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/version${urlIdx ? `/${urlIdx}` : ''}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res?.version ?? false; +}; + +export const getOllamaModels = async (token: string = '', urlIdx: null | number = null) => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/tags${urlIdx !== null ? `/${urlIdx}` : ''}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return (res?.models ?? []) + .map((model) => ({ id: model.model, name: model.name ?? model.model, ...model })) + .sort((a, b) => { + return (a?.name ?? a?.id ?? '').localeCompare(b?.name ?? b?.id ?? ''); + }); +}; + +export const generatePrompt = async (token: string = '', model: string, conversation: string) => { + let error = null; + + if (conversation === '') { + conversation = '[no existing conversation]'; + } + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + prompt: `Conversation: + ${conversation} + + As USER in the conversation above, your task is to continue the conversation. Remember, Your responses should be crafted as if you're a human conversing in a natural, realistic manner, keeping in mind the context and flow of the dialogue. Please generate a fitting response to the last message in the conversation, or if there is no existing conversation, initiate one as a normal person would. + + Response: + ` + }) + }).catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const generateEmbeddings = async (token: string = '', model: string, text: string) => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/embeddings`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + prompt: text + }) + }).catch((err) => { + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const generateTextCompletion = async (token: string = '', model: string, text: string) => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + prompt: text, + stream: true + }) + }).catch((err) => { + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const generateChatCompletion = async (token: string = '', body: object) => { + const controller = new AbortController(); + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/chat`, { + signal: controller.signal, + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify(body) + }).catch((err) => { + error = err; + return null; + }); + + if (error) { + throw error; + } + + return [res, controller]; +}; + +export const unloadModel = async (token: string, tagName: string) => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/unload`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: tagName + }) + }).catch((err) => { + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const createModel = async (token: string, payload: object, urlIdx: string | null = null) => { + let error = null; + + const res = await fetch( + `${OLLAMA_API_BASE_URL}/api/create${urlIdx !== null ? `/${urlIdx}` : ''}`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify(payload) + } + ).catch((err) => { + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteModel = async (token: string, tagName: string, urlIdx: string | null = null) => { + let error = null; + + const res = await fetch( + `${OLLAMA_API_BASE_URL}/api/delete${urlIdx !== null ? `/${urlIdx}` : ''}`, + { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: tagName + }) + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + console.debug(json); + return true; + }) + .catch((err) => { + console.error(err); + error = err; + + if ('detail' in err) { + error = err.detail; + } + + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const pullModel = async (token: string, tagName: string, urlIdx: number | null = null) => { + let error = null; + const controller = new AbortController(); + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/pull${urlIdx !== null ? `/${urlIdx}` : ''}`, { + signal: controller.signal, + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: tagName + }) + }).catch((err) => { + console.error(err); + error = err; + + if ('detail' in err) { + error = err.detail; + } + + return null; + }); + if (error) { + throw error; + } + return [res, controller]; +}; + +export const downloadModel = async ( + token: string, + download_url: string, + urlIdx: string | null = null +) => { + let error = null; + + const res = await fetch( + `${OLLAMA_API_BASE_URL}/models/download${urlIdx !== null ? `/${urlIdx}` : ''}`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + url: download_url + }) + } + ).catch((err) => { + console.error(err); + error = err; + + if ('detail' in err) { + error = err.detail; + } + + return null; + }); + if (error) { + throw error; + } + return res; +}; + +export const uploadModel = async (token: string, file: File, urlIdx: string | null = null) => { + let error = null; + + const formData = new FormData(); + formData.append('file', file); + + const res = await fetch( + `${OLLAMA_API_BASE_URL}/models/upload${urlIdx !== null ? `/${urlIdx}` : ''}`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${token}` + }, + body: formData + } + ).catch((err) => { + console.error(err); + error = err; + + if ('detail' in err) { + error = err.detail; + } + + return null; + }); + if (error) { + throw error; + } + return res; +}; + +// export const pullModel = async (token: string, tagName: string) => { +// return await fetch(`${OLLAMA_API_BASE_URL}/pull`, { +// method: 'POST', +// headers: { +// 'Content-Type': 'text/event-stream', +// Authorization: `Bearer ${token}` +// }, +// body: JSON.stringify({ +// name: tagName +// }) +// }); +// }; diff --git a/src/lib/apis/openai/index.ts b/src/lib/apis/openai/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..8cf7438ad31cfec9459a684c70371d1cf54d5571 --- /dev/null +++ b/src/lib/apis/openai/index.ts @@ -0,0 +1,424 @@ +import { OPENAI_API_BASE_URL, WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; + +export const getOpenAIConfig = async (token: string = '') => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/config`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type OpenAIConfig = { + ENABLE_OPENAI_API: boolean; + OPENAI_API_BASE_URLS: string[]; + OPENAI_API_KEYS: string[]; + OPENAI_API_CONFIGS: object; +}; + +export const updateOpenAIConfig = async (token: string = '', config: OpenAIConfig) => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/config/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + ...config + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getOpenAIUrls = async (token: string = '') => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/urls`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res?.OPENAI_API_BASE_URLS ?? []; +}; + +export const updateOpenAIUrls = async (token: string = '', urls: string[]) => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/urls/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + urls: urls + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.OPENAI_API_BASE_URLS; +}; + +export const getOpenAIKeys = async (token: string = '') => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/keys`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res?.OPENAI_API_KEYS ?? []; +}; + +export const updateOpenAIKeys = async (token: string = '', keys: string[]) => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/keys/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + keys: keys + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.OPENAI_API_KEYS; +}; + +export const getOpenAIModelsDirect = async (url: string, key: string) => { + let error = null; + + const res = await fetch(`${url}/models`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(key && { authorization: `Bearer ${key}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`; + return []; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getOpenAIModels = async (token: string, urlIdx?: number) => { + let error = null; + + const res = await fetch( + `${OPENAI_API_BASE_URL}/models${typeof urlIdx === 'number' ? `/${urlIdx}` : ''}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`; + return []; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const verifyOpenAIConnection = async ( + token: string = '', + connection: dict = {}, + direct: boolean = false +) => { + const { url, key, config } = connection; + if (!url) { + throw 'OpenAI: URL is required'; + } + + let error = null; + let res = null; + + if (direct) { + res = await fetch(`${url}/models`, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${key}`, + 'Content-Type': 'application/json' + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`; + return []; + }); + + if (error) { + throw error; + } + } else { + res = await fetch(`${OPENAI_API_BASE_URL}/verify`, { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + url, + key, + config + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`; + return []; + }); + + if (error) { + throw error; + } + } + + return res; +}; + +export const chatCompletion = async ( + token: string = '', + body: object, + url: string = `${WEBUI_BASE_URL}/api` +): Promise<[Response | null, AbortController]> => { + const controller = new AbortController(); + let error = null; + + const res = await fetch(`${url}/chat/completions`, { + signal: controller.signal, + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }).catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return [res, controller]; +}; + +export const generateOpenAIChatCompletion = async ( + token: string = '', + body: object, + url: string = `${WEBUI_BASE_URL}/api` +) => { + let error = null; + + const res = await fetch(`${url}/chat/completions`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + credentials: 'include', + body: JSON.stringify(body) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err?.detail ?? err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const synthesizeOpenAISpeech = async ( + token: string = '', + speaker: string = 'alloy', + text: string = '', + model: string = 'tts-1' +) => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/audio/speech`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: model, + input: text, + voice: speaker + }) + }).catch((err) => { + console.error(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/prompts/index.ts b/src/lib/apis/prompts/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5db7dc3540d74140402f2b1befe7201f4662c93f --- /dev/null +++ b/src/lib/apis/prompts/index.ts @@ -0,0 +1,664 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +type PromptItem = { + id?: string; // Prompt ID + command: string; + name: string; // Changed from title + content: string; + data?: object | null; + meta?: object | null; + access_grants?: object[]; + version_id?: string | null; // Active version + commit_message?: string | null; // For history tracking + is_production?: boolean; // Whether to set new version as production +}; + +type PromptHistoryItem = { + id: string; + prompt_id: string; + parent_id: string | null; + snapshot: { + name: string; + content: string; + command: string; + data: object; + meta: object; + access_grants: object[]; + }; + user_id: string; + commit_message: string | null; + created_at: number; + user?: { + id: string; + name: string; + email: string; + }; +}; + +type PromptDiff = { + from_id: string; + to_id: string; + from_snapshot: object; + to_snapshot: object; + content_diff: string[]; + name_changed: boolean; + access_grants_changed: boolean; +}; + +export const createNewPrompt = async (token: string, prompt: PromptItem) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...prompt, + command: prompt.command.startsWith('/') ? prompt.command.slice(1) : prompt.command + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getPrompts = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getPromptTags = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/tags`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getPromptItems = async ( + token: string = '', + query: string | null, + viewOption: string | null, + selectedTag: string | null, + orderBy: string | null, + direction: string | null, + page: number +) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (query) { + searchParams.append('query', query); + } + if (viewOption) { + searchParams.append('view_option', viewOption); + } + if (selectedTag) { + searchParams.append('tag', selectedTag); + } + if (orderBy) { + searchParams.append('order_by', orderBy); + } + if (direction) { + searchParams.append('direction', direction); + } + if (page) { + searchParams.append('page', page.toString()); + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/list?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getPromptList = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/list`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getPromptByCommand = async (token: string, command: string) => { + let error = null; + + command = command.charAt(0) === '/' ? command.slice(1) : command; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/command/${command}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getPromptById = async (token: string, promptId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updatePromptById = async (token: string, prompt: PromptItem) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${prompt.id}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify(prompt) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updatePromptMetadata = async ( + token: string, + promptId: string, + name: string, + command: string, + tags: string[] = [] +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/update/meta`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ name, command, tags }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const setProductionPromptVersion = async ( + token: string, + promptId: string, + version_id: string +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/update/version`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + version_id: version_id + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const togglePromptById = async (token: string, promptId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/toggle`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deletePromptById = async (token: string, promptId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updatePromptAccessGrants = async ( + token: string, + promptId: string, + accessGrants: any[] +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/access/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ access_grants: accessGrants }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +//////////////////////////// +// Prompt History APIs +//////////////////////////// + +export const getPromptHistory = async ( + token: string, + promptId: string, + page: number = 0 +): Promise => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/history?page=${page}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deletePromptHistoryVersion = async ( + token: string, + promptId: string, + historyId: string +): Promise => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/history/${historyId}`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return false; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getPromptHistoryEntry = async ( + token: string, + promptId: string, + historyId: string +): Promise => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/history/${historyId}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const restorePromptFromHistory = async ( + token: string, + promptId: string, + historyId: string, + commitMessage?: string +) => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/prompts/id/${promptId}/history/${historyId}/restore`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + commit_message: commitMessage + }) + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getPromptDiff = async ( + token: string, + promptId: string, + fromId: string, + toId: string +): Promise => { + let error = null; + + const res = await fetch( + `${WEBUI_API_BASE_URL}/prompts/id/${promptId}/history/diff?from_id=${fromId}&to_id=${toId}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/retrieval/index.ts b/src/lib/apis/retrieval/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..a84e7b68225cbd509ace55a17a3b9baed93efb1f --- /dev/null +++ b/src/lib/apis/retrieval/index.ts @@ -0,0 +1,532 @@ +import { RETRIEVAL_API_BASE_URL } from '$lib/constants'; + +export const getRAGConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${RETRIEVAL_API_BASE_URL}/config`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type ChunkConfigForm = { + chunk_size: number; + chunk_overlap: number; +}; + +type DocumentIntelligenceConfigForm = { + key: string; + endpoint: string; + model: string; +}; + +type ContentExtractConfigForm = { + engine: string; + tika_server_url: string | null; + document_intelligence_config: DocumentIntelligenceConfigForm | null; +}; + +type YoutubeConfigForm = { + language: string[]; + translation?: string | null; + proxy_url: string; +}; + +type RAGConfigForm = { + PDF_EXTRACT_IMAGES?: boolean; + ENABLE_GOOGLE_DRIVE_INTEGRATION?: boolean; + ENABLE_ONEDRIVE_INTEGRATION?: boolean; + chunk?: ChunkConfigForm; + content_extraction?: ContentExtractConfigForm; + web_loader_ssl_verification?: boolean; + youtube?: YoutubeConfigForm; +}; + +export const updateRAGConfig = async (token: string, payload: RAGConfigForm) => { + let error = null; + + const res = await fetch(`${RETRIEVAL_API_BASE_URL}/config/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...payload + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getQuerySettings = async (token: string) => { + let error = null; + + const res = await fetch(`${RETRIEVAL_API_BASE_URL}/query/settings`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type QuerySettings = { + k: number | null; + r: number | null; + template: string | null; +}; + +export const updateQuerySettings = async (token: string, settings: QuerySettings) => { + let error = null; + + const res = await fetch(`${RETRIEVAL_API_BASE_URL}/query/settings/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...settings + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getEmbeddingConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${RETRIEVAL_API_BASE_URL}/embedding`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type OpenAIConfigForm = { + key: string; + url: string; +}; + +type AzureOpenAIConfigForm = { + key: string; + url: string; + version: string; +}; + +type EmbeddingModelUpdateForm = { + openai_config?: OpenAIConfigForm; + azure_openai_config?: AzureOpenAIConfigForm; + embedding_engine: string; + embedding_model: string; + embedding_batch_size?: number; +}; + +export const updateEmbeddingConfig = async (token: string, payload: EmbeddingModelUpdateForm) => { + let error = null; + + const res = await fetch(`${RETRIEVAL_API_BASE_URL}/embedding/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...payload + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getRerankingConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${RETRIEVAL_API_BASE_URL}/reranking`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type RerankingModelUpdateForm = { + reranking_model: string; +}; + +export const updateRerankingConfig = async (token: string, payload: RerankingModelUpdateForm) => { + let error = null; + + const res = await fetch(`${RETRIEVAL_API_BASE_URL}/reranking/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...payload + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export interface SearchDocument { + status: boolean; + collection_name: string; + filenames: string[]; +} + +export const processYoutubeVideo = async (token: string, url: string) => { + let error = null; + + const res = await fetch(`${RETRIEVAL_API_BASE_URL}/process/youtube`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + url: url + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const processWeb = async ( + token: string, + collection_name: string, + url: string, + process: boolean = true +) => { + let error = null; + + const searchParams = new URLSearchParams(); + + if (!process) { + searchParams.append('process', 'false'); + } + + const res = await fetch(`${RETRIEVAL_API_BASE_URL}/process/web?${searchParams.toString()}`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + url: url, + collection_name: collection_name + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const processWebSearch = async ( + token: string, + query: string, + collection_name?: string +): Promise => { + let error = null; + + const res = await fetch(`${RETRIEVAL_API_BASE_URL}/process/web/search`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + query, + collection_name: collection_name ?? '' + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const queryDoc = async ( + token: string, + collection_name: string, + query: string, + k: number | null = null +) => { + let error = null; + + const res = await fetch(`${RETRIEVAL_API_BASE_URL}/query/doc`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + collection_name: collection_name, + query: query, + k: k + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const queryCollection = async ( + token: string, + collection_names: string, + query: string, + k: number | null = null +) => { + let error = null; + + const res = await fetch(`${RETRIEVAL_API_BASE_URL}/query/collection`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + collection_names: collection_names, + query: query, + k: k + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const resetUploadDir = async (token: string) => { + let error = null; + + const res = await fetch(`${RETRIEVAL_API_BASE_URL}/reset/uploads`, { + method: 'POST', + headers: { + Accept: 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const resetVectorDB = async (token: string) => { + let error = null; + + const res = await fetch(`${RETRIEVAL_API_BASE_URL}/reset/db`, { + method: 'POST', + headers: { + Accept: 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/skills/index.ts b/src/lib/apis/skills/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..24139fa042988d1e353df76ada22e62d7ba7db18 --- /dev/null +++ b/src/lib/apis/skills/index.ts @@ -0,0 +1,321 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const createNewSkill = async (token: string, skill: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/skills/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...skill + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getSkills = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/skills/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getSkillList = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/skills/list`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getSkillItems = async ( + token: string = '', + query: string | null = null, + viewOption: string | null = null, + page: number | null = null +) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (query) searchParams.append('query', query); + if (viewOption) searchParams.append('view_option', viewOption); + if (page) searchParams.append('page', page.toString()); + + const res = await fetch(`${WEBUI_API_BASE_URL}/skills/list?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const exportSkills = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/skills/export`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getSkillById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/skills/id/${id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateSkillById = async (token: string, id: string, skill: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/skills/id/${id}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...skill + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateSkillAccessGrants = async (token: string, id: string, accessGrants: any[]) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/skills/id/${id}/access/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + access_grants: accessGrants + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const toggleSkillById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/skills/id/${id}/toggle`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteSkillById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/skills/id/${id}/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/streaming/index.ts b/src/lib/apis/streaming/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..6f3677aaf9f9c2ebe259bdde736587f5709d351a --- /dev/null +++ b/src/lib/apis/streaming/index.ts @@ -0,0 +1,142 @@ +import { EventSourceParserStream } from 'eventsource-parser/stream'; +import type { ParsedEvent } from 'eventsource-parser'; + +type TextStreamUpdate = { + done: boolean; + value: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sources?: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + selectedModelId?: any; + error?: any; + usage?: ResponseUsage; +}; + +type ResponseUsage = { + /** Including images and tools if any */ + prompt_tokens: number; + /** The tokens generated */ + completion_tokens: number; + /** Sum of the above two fields */ + total_tokens: number; + /** Any other fields that aren't part of the base OpenAI spec */ + [other: string]: unknown; +}; + +// createOpenAITextStream takes a responseBody with a SSE response, +// and returns an async generator that emits delta updates with large deltas chunked into random sized chunks +export async function createOpenAITextStream( + responseBody: ReadableStream, + splitLargeDeltas: boolean +): Promise> { + const eventStream = responseBody + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new EventSourceParserStream()) + .getReader(); + let iterator = openAIStreamToIterator(eventStream); + if (splitLargeDeltas) { + iterator = streamLargeDeltasAsRandomChunks(iterator); + } + return iterator; +} + +async function* openAIStreamToIterator( + reader: ReadableStreamDefaultReader +): AsyncGenerator { + while (true) { + const { value, done } = await reader.read(); + if (done) { + yield { done: true, value: '' }; + break; + } + if (!value) { + continue; + } + const data = value.data; + if (data.startsWith('[DONE]')) { + yield { done: true, value: '' }; + break; + } + + try { + const parsedData = JSON.parse(data); + console.log(parsedData); + + if (parsedData.error) { + yield { done: true, value: '', error: parsedData.error }; + break; + } + + if (parsedData.sources) { + yield { done: false, value: '', sources: parsedData.sources }; + continue; + } + + if (parsedData.selected_model_id) { + yield { done: false, value: '', selectedModelId: parsedData.selected_model_id }; + continue; + } + + if (parsedData.usage) { + yield { done: false, value: '', usage: parsedData.usage }; + continue; + } + + yield { + done: false, + value: parsedData.choices?.[0]?.delta?.content ?? '' + }; + } catch (e) { + console.error('Error extracting delta from SSE event:', e); + } + } +} + +// streamLargeDeltasAsRandomChunks will chunk large deltas (length > 5) into random sized chunks between 1-3 characters +// This is to simulate a more fluid streaming, even though some providers may send large chunks of text at once +async function* streamLargeDeltasAsRandomChunks( + iterator: AsyncGenerator +): AsyncGenerator { + for await (const textStreamUpdate of iterator) { + if (textStreamUpdate.done) { + yield textStreamUpdate; + return; + } + + if (textStreamUpdate.error) { + yield textStreamUpdate; + continue; + } + if (textStreamUpdate.sources) { + yield textStreamUpdate; + continue; + } + if (textStreamUpdate.selectedModelId) { + yield textStreamUpdate; + continue; + } + if (textStreamUpdate.usage) { + yield textStreamUpdate; + continue; + } + + let content = textStreamUpdate.value; + if (content.length < 5) { + yield { done: false, value: content }; + continue; + } + while (content != '') { + const chunkSize = Math.min(Math.floor(Math.random() * 3) + 1, content.length); + const chunk = content.slice(0, chunkSize); + yield { done: false, value: chunk }; + // Do not sleep if the tab is hidden + // Timers are throttled to 1s in hidden tabs + if (document?.visibilityState !== 'hidden') { + await sleep(5); + } + content = content.slice(chunkSize); + } + } +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/src/lib/apis/tasks/index.ts b/src/lib/apis/tasks/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..dab6090fdec05d435a6bc666b756dbb397ffe9f0 --- /dev/null +++ b/src/lib/apis/tasks/index.ts @@ -0,0 +1,14 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const checkActiveChats = async (token: string, chatIds: string[]) => { + const res = await fetch(`${WEBUI_API_BASE_URL}/tasks/active/chats`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ chat_ids: chatIds }) + }); + if (!res.ok) throw await res.json(); + return res.json(); +}; diff --git a/src/lib/apis/terminal/index.ts b/src/lib/apis/terminal/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..69ee2c5a0a652f91c212207039633cb1a834856a --- /dev/null +++ b/src/lib/apis/terminal/index.ts @@ -0,0 +1,387 @@ +export type FileEntry = { + name: string; + type: 'file' | 'directory'; + size?: number; + modified?: number; +}; + +export type ListeningPort = { + port: number; + pid: number | null; + process: string | null; +}; + +export type TerminalFeatures = { + terminal?: boolean; +}; + +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export type TerminalServer = { + id: string; + url: string; + name: string; +}; + +export const getTerminalServers = async (token: string): Promise => { + const res = await fetch(`${WEBUI_API_BASE_URL}/terminals/`, { + headers: { + Authorization: `Bearer ${token}` + } + }).catch(() => null); + if (!res || !res.ok) return []; + return res.json().catch(() => []); +}; + +export const getTerminalConfig = async ( + baseUrl: string, + apiKey: string +): Promise<{ features: TerminalFeatures } | null> => { + const url = `${baseUrl.replace(/\/$/, '')}/api/config`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${apiKey}` } + }).catch(() => null); + if (!res || !res.ok) return null; + return res.json().catch(() => null); +}; + +export const getCwd = async ( + baseUrl: string, + apiKey: string, + sessionId?: string +): Promise => { + const url = `${baseUrl.replace(/\/$/, '')}/files/cwd`; + const headers: Record = { Authorization: `Bearer ${apiKey}` }; + if (sessionId) headers['X-Session-Id'] = sessionId; + const res = await fetch(url, { headers }).catch(() => null); + if (!res || !res.ok) return null; + const json = await res.json().catch(() => null); + return json?.cwd ?? null; +}; + +export const listFiles = async ( + baseUrl: string, + apiKey: string, + path: string = '/', + sessionId?: string +): Promise => { + // The endpoint uses `directory` as the query param name + const url = `${baseUrl.replace(/\/$/, '')}/files/list?directory=${encodeURIComponent(path)}`; + const headers: Record = { Authorization: `Bearer ${apiKey}` }; + if (sessionId) headers['X-Session-Id'] = sessionId; + const res = await fetch(url, { headers }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error('open-terminal listFiles error:', err); + return null; + }); + return res?.entries ?? null; +}; + +export const readFile = async ( + baseUrl: string, + apiKey: string, + path: string, + sessionId?: string +): Promise => { + const url = `${baseUrl.replace(/\/$/, '')}/files/read?path=${encodeURIComponent(path)}`; + const headers: Record = { Authorization: `Bearer ${apiKey}` }; + if (sessionId) headers['X-Session-Id'] = sessionId; + const res = await fetch(url, { headers }).catch((err) => { + console.error('open-terminal readFile error:', err); + return null; + }); + + if (!res || !res.ok) return null; + + const contentType = res.headers.get('content-type') ?? ''; + if (contentType.startsWith('image/') || contentType.startsWith('application/octet')) { + // Binary — return a placeholder + return `[Binary file: ${contentType}]`; + } + + // Text files: endpoint returns JSON { path, total_lines, content } + // Binary image files: endpoint returns raw bytes (handled above) + const json = await res.json().catch(() => null); + return json?.content ?? null; +}; + +export const downloadFileBlob = async ( + baseUrl: string, + apiKey: string, + path: string, + sessionId?: string +): Promise<{ blob: Blob; filename: string } | null> => { + const url = `${baseUrl.replace(/\/$/, '')}/files/view?path=${encodeURIComponent(path)}`; + const headers: Record = { Authorization: `Bearer ${apiKey}` }; + if (sessionId) headers['X-Session-Id'] = sessionId; + const res = await fetch(url, { headers }).catch(() => null); + + if (!res || !res.ok) return null; + + const filename = path.split('/').pop() ?? 'file'; + const blob = await res.blob(); + return { blob, filename }; +}; + +export const archiveFromTerminal = async ( + baseUrl: string, + apiKey: string, + paths: string[], + sessionId?: string +): Promise<{ blob: Blob; filename: string } | null> => { + const url = `${baseUrl.replace(/\/$/, '')}/files/archive`; + const headers: Record = { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }; + if (sessionId) headers['X-Session-Id'] = sessionId; + const res = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify({ paths }) + }).catch(() => null); + + if (!res || !res.ok) return null; + + const disposition = res.headers.get('content-disposition') ?? ''; + const match = disposition.match(/filename="?([^"]+)"?/); + const filename = match?.[1] ?? 'download.zip'; + const blob = await res.blob(); + return { blob, filename }; +}; + +export const uploadToTerminal = async ( + baseUrl: string, + apiKey: string, + directory: string, + file: File, + sessionId?: string +): Promise<{ path: string; size: number } | null> => { + const url = `${baseUrl.replace(/\/$/, '')}/files/upload?directory=${encodeURIComponent(directory)}`; + const body = new FormData(); + body.append('file', file); + const headers: Record = { Authorization: `Bearer ${apiKey}` }; + if (sessionId) headers['X-Session-Id'] = sessionId; + const res = await fetch(url, { + method: 'POST', + headers, + body + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error('open-terminal uploadToTerminal error:', err); + return null; + }); + return res; +}; + +export const createDirectory = async ( + baseUrl: string, + apiKey: string, + path: string, + sessionId?: string +): Promise<{ path: string } | null> => { + const url = `${baseUrl.replace(/\/$/, '')}/files/mkdir`; + const headers: Record = { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }; + if (sessionId) headers['X-Session-Id'] = sessionId; + const res = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify({ path }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error('open-terminal createDirectory error:', err); + return null; + }); + return res; +}; + +export const deleteEntry = async ( + baseUrl: string, + apiKey: string, + path: string, + sessionId?: string +): Promise<{ path: string; type: string } | null> => { + const url = `${baseUrl.replace(/\/$/, '')}/files/delete?path=${encodeURIComponent(path)}`; + const headers: Record = { Authorization: `Bearer ${apiKey}` }; + if (sessionId) headers['X-Session-Id'] = sessionId; + const res = await fetch(url, { + method: 'DELETE', + headers + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error('open-terminal deleteEntry error:', err); + return null; + }); + return res; +}; + +export const setCwd = async ( + baseUrl: string, + apiKey: string, + path: string, + sessionId?: string +): Promise<{ cwd: string } | null> => { + const url = `${baseUrl.replace(/\/$/, '')}/files/cwd`; + const headers: Record = { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }; + if (sessionId) headers['X-Session-Id'] = sessionId; + const res = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify({ path }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error('open-terminal setCwd error:', err); + return null; + }); + return res; +}; + +export const moveEntry = async ( + baseUrl: string, + apiKey: string, + source: string, + destination: string, + sessionId?: string +): Promise<{ source: string; destination: string } | { error: string }> => { + const url = `${baseUrl.replace(/\/$/, '')}/files/move`; + const headers: Record = { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }; + if (sessionId) headers['X-Session-Id'] = sessionId; + const res = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify({ source, destination }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error('open-terminal moveEntry error:', err); + return { error: err?.detail ?? 'Move failed' }; + }); + return res; +}; + +export const getListeningPorts = async ( + baseUrl: string, + apiKey: string +): Promise => { + const url = `${baseUrl.replace(/\/$/, '')}/ports`; + const res = await fetch(url, { + headers: { Authorization: `Bearer ${apiKey}` } + }).catch(() => null); + if (!res || !res.ok) return []; + const json = await res.json().catch(() => null); + return json?.ports ?? []; +}; + +export const getPortProxyUrl = (baseUrl: string, port: number, path: string = ''): string => { + return `${baseUrl.replace(/\/$/, '')}/proxy/${port}/${path}`; +}; + +// --------------------------------------------------------------------------- +// Notebook execution +// --------------------------------------------------------------------------- + +export const createNotebookSession = async ( + baseUrl: string, + apiKey: string, + path: string +): Promise<{ id: string; kernel: string; status: string } | { error: string }> => { + const url = `${baseUrl.replace(/\/$/, '')}/notebooks`; + const res = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ path }) + }) + .then(async (res) => { + if (!res.ok) { + const body = await res.json().catch(() => ({})); + return { error: body?.detail ?? `HTTP ${res.status}` }; + } + return res.json(); + }) + .catch((err) => { + console.error('open-terminal createNotebookSession error:', err); + return { error: 'Connection failed' }; + }); + return res; +}; + +export const executeNotebookCell = async ( + baseUrl: string, + apiKey: string, + sessionId: string, + cellIndex: number, + source?: string +): Promise<{ status: string; execution_count?: number; outputs: any[] } | { error: string }> => { + const url = `${baseUrl.replace(/\/$/, '')}/notebooks/${sessionId}/execute`; + const body: Record = { cell_index: cellIndex }; + if (source !== undefined) body.source = source; + + const res = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }) + .then(async (res) => { + if (!res.ok) { + const body = await res.json().catch(() => ({})); + return { error: body?.detail ?? `HTTP ${res.status}` }; + } + return res.json(); + }) + .catch((err) => { + console.error('open-terminal executeNotebookCell error:', err); + return { error: 'Connection failed' }; + }); + return res; +}; + +export const stopNotebookSession = async ( + baseUrl: string, + apiKey: string, + sessionId: string +): Promise => { + const url = `${baseUrl.replace(/\/$/, '')}/notebooks/${sessionId}`; + const res = await fetch(url, { + method: 'DELETE', + headers: { Authorization: `Bearer ${apiKey}` } + }).catch(() => null); + return res?.ok ?? false; +}; diff --git a/src/lib/apis/tools/index.ts b/src/lib/apis/tools/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5d26e50fee46760f50aca74c910c44c7fe270868 --- /dev/null +++ b/src/lib/apis/tools/index.ts @@ -0,0 +1,485 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const createNewTool = async (token: string, tool: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...tool + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const loadToolByUrl = async (token: string = '', url: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/load/url`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + url + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getTools = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getToolList = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/list`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const exportTools = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/export`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getToolById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateToolById = async (token: string, id: string, tool: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...tool + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateToolAccessGrants = async (token: string, id: string, accessGrants: any[]) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/access/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ access_grants: accessGrants }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteToolById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getToolValvesById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/valves`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getToolValvesSpecById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/valves/spec`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateToolValvesById = async (token: string, id: string, valves: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/valves/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...valves + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUserValvesById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/valves/user`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUserValvesSpecById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/valves/user/spec`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateUserValvesById = async (token: string, id: string, valves: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/valves/user/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...valves + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/users/index.ts b/src/lib/apis/users/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..91b63338deadde85236eabcf306ba1e397f81080 --- /dev/null +++ b/src/lib/apis/users/index.ts @@ -0,0 +1,552 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; +import { getUserPosition } from '$lib/utils'; + +export const getUserGroups = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/groups`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUserDefaultPermissions = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/default/permissions`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateUserDefaultPermissions = async (token: string, permissions: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/default/permissions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...permissions + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateUserRole = async (token: string, id: string, role: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/update/role`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + id: id, + role: role + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUsers = async ( + token: string, + query?: string, + orderBy?: string, + direction?: string, + page = 1 +) => { + let error = null; + let res = null; + + const searchParams = new URLSearchParams(); + + searchParams.set('page', `${page}`); + + if (query) { + searchParams.set('query', query); + } + + if (orderBy) { + searchParams.set('order_by', orderBy); + } + + if (direction) { + searchParams.set('direction', direction); + } + + res = await fetch(`${WEBUI_API_BASE_URL}/users/?${searchParams.toString()}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const searchUsers = async ( + token: string, + query?: string, + orderBy?: string, + direction?: string, + page = 1 +) => { + let error = null; + let res = null; + + const searchParams = new URLSearchParams(); + + searchParams.set('page', `${page}`); + + if (query) { + searchParams.set('query', query); + } + + if (orderBy) { + searchParams.set('order_by', orderBy); + } + + if (direction) { + searchParams.set('direction', direction); + } + + res = await fetch(`${WEBUI_API_BASE_URL}/users/search?${searchParams.toString()}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getAllUsers = async (token: string) => { + let error = null; + let res = null; + + res = await fetch(`${WEBUI_API_BASE_URL}/users/all`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUserSettings = async (token: string) => { + let error = null; + const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/settings`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateUserSettings = async (token: string, settings: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/settings/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...settings + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUserInfoById = async (token: string, userId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}/info`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateUserStatus = async (token: string, formData: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/status/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...formData + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUserInfo = async (token: string) => { + let error = null; + const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/info`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateUserInfo = async (token: string, info: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/info/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...info + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getAndUpdateUserLocation = async (token: string) => { + const location = await getUserPosition().catch((err) => { + console.error(err); + return null; + }); + + if (location) { + await updateUserInfo(token, { location: location }); + return location; + } else { + console.info('Failed to get user location'); + return null; + } +}; + +export const getUserActiveStatusById = async (token: string, userId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}/active`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteUserById = async (token: string, userId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type UserUpdateForm = { + role: string; + profile_image_url: string; + email: string; + name: string; + password: string; +}; + +export const updateUserById = async (token: string, userId: string, user: UserUpdateForm) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + profile_image_url: user.profile_image_url, + role: user.role, + email: user.email, + name: user.name, + password: user.password !== '' ? user.password : undefined + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUserGroupsById = async (token: string, userId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}/groups`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/utils/index.ts b/src/lib/apis/utils/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e5fea091eb938aab966a71afbaf1aa94c4c365e6 --- /dev/null +++ b/src/lib/apis/utils/index.ts @@ -0,0 +1,185 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const getGravatarUrl = async (token: string, email: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/utils/gravatar?email=${email}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail ?? err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const executeCode = async (token: string, code: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/execute`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + code: code + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + + error = err; + if (err.detail) { + error = err.detail; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const formatPythonCode = async (token: string, code: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/format`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + code: code + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + + error = err; + if (err.detail) { + error = err.detail; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const downloadChatAsPDF = async (token: string, title: string, messages: object[]) => { + let error = null; + + const blob = await fetch(`${WEBUI_API_BASE_URL}/utils/pdf`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + title: title, + messages: messages + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.blob(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + return blob; +}; + +export const getHTMLFromMarkdown = async (token: string, md: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/utils/markdown`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + md: md + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err; + return null; + }); + + return res.html; +}; + +export const downloadDatabase = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/utils/db/download`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (response) => { + if (!response.ok) { + throw await response.json(); + } + return response.blob(); + }) + .then((blob) => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'webui.db'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } +}; diff --git a/src/lib/components/AddConnectionModal.svelte b/src/lib/components/AddConnectionModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ae1d35364252274ab12ed2383e6b58a3f86aef16 --- /dev/null +++ b/src/lib/components/AddConnectionModal.svelte @@ -0,0 +1,736 @@ + + + +
      +
      +

      + {#if edit} + {$i18n.t('Edit Connection')} + {:else} + {$i18n.t('Add Connection')} + {/if} +

      + +
      + +
      +
      +
      { + e.preventDefault(); + submitHandler(); + }} + > +
      + {#if !direct} +
      +
      +
      {$i18n.t('Connection Type')}
      + +
      + +
      +
      +
      + {/if} + +
      +
      + + +
      + + + {#if !ollama} + + + {/if} +
      +
      + + + + + +
      + + + + +
      +
      + +
      +
      + + +
      +
      + +
      + +
      + {#if auth_type === 'bearer'} + + {:else if auth_type === 'none'} +
      + {$i18n.t('No authentication')} +
      + {:else if auth_type === 'session'} +
      + {$i18n.t('Forwards system user session credentials to authenticate')} +
      + {:else if auth_type === 'system_oauth'} +
      + {$i18n.t('Forwards system user OAuth access token to authenticate')} +
      + {:else if ['azure_ad', 'microsoft_entra_id'].includes(auth_type)} +
      + {$i18n.t('Uses DefaultAzureCredential to authenticate')} +
      + {/if} +
      +
      +
      +
      + + {#if !ollama && !direct} +
      +
      + + +
      + + +
      +
      + + +
      +
      + {#if event && !event.meta?.automation_id} + + {/if} +
      + +
      + + +
      +
      +
      + + + diff --git a/src/lib/components/calendar/CalendarSidebar.svelte b/src/lib/components/calendar/CalendarSidebar.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d3ea51a47247760c296d18f695f2aafdae6ff910 --- /dev/null +++ b/src/lib/components/calendar/CalendarSidebar.svelte @@ -0,0 +1,233 @@ + + + + +
      + +
      +
      +
      {miniMonthNames[miniMonth]} {miniYear}
      +
      + + +
      +
      + +
      + {#each ['S', 'M', 'T', 'W', 'T', 'F', 'S'] as d} +
      {d}
      + {/each} +
      + +
      + {#each miniDays as day} + + {/each} +
      +
      + + +
      +
      +
      + {$i18n.t('Calendars')} +
      +
      + + {#each calendars as cal (cal.id)} +
      + +
      + {/each} +
      +
      diff --git a/src/lib/components/calendar/CalendarView.svelte b/src/lib/components/calendar/CalendarView.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c78de56d8f027a0b45b9d468ba1a8696f18393bb --- /dev/null +++ b/src/lib/components/calendar/CalendarView.svelte @@ -0,0 +1,323 @@ + + +
      + + {#if view === 'month'} +
      +
      + {#each DAY_NAMES as day} +
      + {$i18n.t(day)} +
      + {/each} +
      + +
      + {#each monthDays as day, i} + {@const dayKey = new Date(day.getFullYear(), day.getMonth(), day.getDate()) + .getTime() + .toString()} + {@const dayEvents = eventsByDay[dayKey] || []} + {@const col = i % 7} + {@const row = Math.floor(i / 7)} + + {/each} +
      +
      + + + {:else if view === 'week'} +
      +
      +
      +
      +
      +
      + {#each weekDays as day} +
      +
      + {DAY_NAMES[day.getDay()]} +
      +
      + {day.getDate()} +
      +
      + {/each} +
      + +
      + {#each hours as hour} +
      +
      + {hour > 0 ? formatHour(hour) : ''} +
      + {#each weekDays as day} + {@const hourEvents = getEventsForHour(day, hour, filteredEvents)} + + {/each} +
      + {/each} +
      +
      +
      +
      +
      + + + {:else} +
      +
      + {#each hours as hour} + {@const hourEvents = getEventsForHour(currentDate, hour, filteredEvents)} +
      +
      + {formatHour(hour)} +
      + +
      + {/each} +
      +
      + {/if} +
      diff --git a/src/lib/components/channel/Channel.svelte b/src/lib/components/channel/Channel.svelte new file mode 100644 index 0000000000000000000000000000000000000000..77626a1016c241ce108f96c33ccea67e3ba467cd --- /dev/null +++ b/src/lib/components/channel/Channel.svelte @@ -0,0 +1,439 @@ + + + + {#if channel?.type === 'dm'} + {channel?.name.trim() || + channel?.users.reduce((a, e, i, arr) => { + if (e.id === $user?.id) { + return a; + } + + if (a) { + return `${a}, ${e.name}`; + } else { + return e.name; + } + }, '')} • Open WebUI + {:else} + #{channel?.name ?? 'Channel'} • Open WebUI + {/if} + + +
      + + + { + messages = messages.map((message) => { + if (message.id === messageId) { + return { + ...message, + is_pinned: pinned + }; + } + return message; + }); + }} + onUpdate={async () => { + channel = await getChannelById(localStorage.token, id).catch((error) => { + return null; + }); + }} + /> + + {#if channel && messages !== null} +
      +
      { + scrollEnd = Math.abs(messagesContainerElement.scrollTop) <= 50; + }} + > + {#key id} + { + replyToMessage = message; + await tick(); + chatInputElement?.focus(); + }} + onThread={(id) => { + threadId = id; + }} + onLoad={async () => { + const newMessages = await getChannelMessages( + localStorage.token, + id, + messages.length + ); + + messages = [...messages, ...newMessages]; + + if (newMessages.length < 50) { + top = true; + return; + } + }} + /> + {/key} +
      +
      + +
      + +
      + {:else} +
      +
      + +
      +
      + {/if} +
      + + {#if !largeScreen} + {#if threadId !== null} + { + threadId = null; + }} + > +
      + { + threadId = null; + }} + /> +
      +
      + {/if} + {:else if threadId !== null} + +
      + + + +
      + { + threadId = null; + }} + /> +
      +
      + {/if} + +
      diff --git a/src/lib/components/channel/ChannelInfoModal.svelte b/src/lib/components/channel/ChannelInfoModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..cd1ee352438a9b19c72f84f3457fd6b5e00969e0 --- /dev/null +++ b/src/lib/components/channel/ChannelInfoModal.svelte @@ -0,0 +1,138 @@ + + +{#if channel} + + +
      +
      +
      +
      + {#if channel?.type === 'dm'} +
      + {$i18n.t('Direct Message')} +
      + {:else} +
      + {#if isPublicChannel(channel)} + + {:else} + + {/if} +
      + +
      + {channel.name} +
      + {/if} +
      +
      + +
      + +
      +
      + { + e.preventDefault(); + submitHandler(); + }} + > +
      + { + showAddMembersModal = true; + } + : null} + onRemove={channel?.type === 'group' && channel?.is_manager + ? (userId) => { + removeMemberHandler(userId); + } + : null} + search={channel?.type !== 'dm'} + sort={channel?.type !== 'dm'} + /> +
      + +
      +
      +
      +
      +{/if} diff --git a/src/lib/components/channel/ChannelInfoModal/AddMembersModal.svelte b/src/lib/components/channel/ChannelInfoModal/AddMembersModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6b70ad820f734ff3970dc02bee190df65b4a11e8 --- /dev/null +++ b/src/lib/components/channel/ChannelInfoModal/AddMembersModal.svelte @@ -0,0 +1,106 @@ + + +{#if channel} + +
      +
      +
      +
      + {$i18n.t('Add Members')} +
      +
      + +
      + +
      +
      +
      { + e.preventDefault(); + submitHandler(); + }} + > +
      + +
      + +
      + +
      +
      +
      +
      +
      +
      +{/if} diff --git a/src/lib/components/channel/ChannelInfoModal/UserList.svelte b/src/lib/components/channel/ChannelInfoModal/UserList.svelte new file mode 100644 index 0000000000000000000000000000000000000000..354d1002df31df6b254474b5129ddad50bb023a6 --- /dev/null +++ b/src/lib/components/channel/ChannelInfoModal/UserList.svelte @@ -0,0 +1,283 @@ + + +
      + {#if users === null || total === null} +
      + +
      + {:else} +
      +
      + + {$i18n.t('Members')} + + {total} +
      + + {#if onAdd} +
      + +
      + {/if} +
      + + + {#if search} +
      +
      +
      +
      + + + +
      + +
      +
      +
      + {/if} + + {#if users.length > 0} +
      +
      + +
      + {#each users as user, userIdx (user.id)} +
      +
      +
      + + user + + +
      {user.name}
      +
      + + {#if user?.is_active} +
      + + + + +
      + {/if} +
      +
      + +
      +
      + +
      + + {#if onRemove} +
      + +
      + {/if} +
      +
      + {/each} +
      +
      +
      + + {#if total > 30} + + {/if} + {:else} +
      + {$i18n.t('No users were found.')} +
      + {/if} + {/if} +
      diff --git a/src/lib/components/channel/MessageInput.svelte b/src/lib/components/channel/MessageInput.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ec00a944d0b2f4b522dd9a214e1b9bc4266c6c3e --- /dev/null +++ b/src/lib/components/channel/MessageInput.svelte @@ -0,0 +1,1126 @@ + + +{#if loaded} + + + {#if acceptFiles} + { + if (inputFiles && inputFiles.length > 0) { + inputFilesHandler(Array.from(inputFiles)); + } else { + toast.error($i18n.t(`File not found.`)); + } + + filesInputElement.value = ''; + }} + /> + {/if} + + + +
      +
      +
      +
      +
      + {#if scrollEnd === false} +
      + +
      + {/if} +
      + + {#if typingUsers.length > 0} +
      +
      + + +
      + + {typingUsers.map((user) => user.name).join(', ')} + + {$i18n.t('is typing...')} +
      +
      +
      + {/if} +
      +
      + +
      + {#if recording} + { + recording = false; + + await tick(); + + if (chatInputElement) { + chatInputElement.focus(); + } + }} + onConfirm={async (data) => { + const { text, filename } = data; + recording = false; + + await tick(); + insertTextAtCursor(text); + + await tick(); + + if (chatInputElement) { + chatInputElement.focus(); + } + }} + /> + {:else} +
      { + submitHandler(); + }} + > +
      + {#if replyToMessage !== null} +
      +
      +
      +
      + {$i18n.t('Replying to {{NAME}}', { + NAME: replyToMessage?.meta?.model_name ?? replyToMessage.user.name + })} +
      +
      +
      + +
      +
      +
      + {/if} + + {#if files.length > 0} +
      + {#each files as file, fileIdx} + {#if file.type === 'image' || (file?.content_type ?? '').startsWith('image/')} + {@const fileUrl = + file.url.startsWith('data') || file.url.startsWith('http') + ? file.url + : `${WEBUI_API_BASE_URL}/files/${file.url}${file?.content_type ? '/content' : ''}`} +
      +
      + +
      +
      + +
      +
      + {:else} + { + files.splice(fileIdx, 1); + files = files; + }} + on:click={() => { + console.log(file); + }} + /> + {/if} + {/each} +
      + {/if} + +
      +
      + {#key $settings?.richTextInput && $settings?.showFormattingToolbar} + 0 || + navigator.msMaxTouchPoints > 0 + )} + largeTextAsFile={$settings?.largeTextAsFile ?? false} + floatingMenuPlacement={'top-start'} + {suggestions} + onChange={(e) => { + const { md } = e; + content = md; + command = getCommand(); + }} + on:keydown={async (e) => { + e = e.detail.event; + const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac + + const suggestionsContainerElement = + document.getElementById('suggestions-container'); + + if (!suggestionsContainerElement) { + if ( + !$mobile || + !( + 'ontouchstart' in window || + navigator.maxTouchPoints > 0 || + navigator.msMaxTouchPoints > 0 + ) + ) { + // Prevent Enter key from creating a new line + // Uses keyCode '13' for Enter key for chinese/japanese keyboards + if (e.keyCode === 13 && !e.shiftKey) { + e.preventDefault(); + } + + // Submit the content when Enter key is pressed + if ( + (content !== '' || files.length > 0) && + e.keyCode === 13 && + !e.shiftKey + ) { + submitHandler(); + } + } + } + + if (e.key === 'Escape') { + console.info('Escape'); + replyToMessage = null; + } + }} + on:paste={async (e) => { + e = e.detail.event; + console.log(e); + + const clipboardData = e.clipboardData || window.clipboardData; + + if (clipboardData && clipboardData.items) { + for (const item of clipboardData.items) { + const file = item.getAsFile(); + if (file) { + await inputFilesHandler([file]); + e.preventDefault(); + } + } + } + }} + /> + {/key} +
      +
      + +
      +
      + + {#if acceptFiles} + { + filesInputElement.click(); + }} + > + + + {/if} + +
      + +
      + {#if content === ''} + + + + {/if} + +
      + {#if inputLoading && onStop} +
      + + + +
      + {:else} +
      + + + +
      + {/if} +
      +
      +
      +
      +
      + {/if} +
      +
      +
      +{/if} diff --git a/src/lib/components/channel/MessageInput/InputMenu.svelte b/src/lib/components/channel/MessageInput/InputMenu.svelte new file mode 100644 index 0000000000000000000000000000000000000000..09ad17f59f8747e467decd991aabe410bc88d478 --- /dev/null +++ b/src/lib/components/channel/MessageInput/InputMenu.svelte @@ -0,0 +1,74 @@ + + + { + if (e.detail === false) { + onClose(); + } + }} +> + + + + +
      +
      + + + +
      +
      +
      diff --git a/src/lib/components/channel/MessageInput/MentionList.svelte b/src/lib/components/channel/MessageInput/MentionList.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0d8ce881970b7f36828ba22203c17c08186b7561 --- /dev/null +++ b/src/lib/components/channel/MessageInput/MentionList.svelte @@ -0,0 +1,236 @@ + + +{#if filteredItems.length} +
      +
      + {#each filteredItems as item, i} + {#if i === 0 || item?.type !== filteredItems[i - 1]?.type} +
      + {#if item?.type === 'user'} + {$i18n.t('Users')} + {:else if item?.type === 'model'} + {$i18n.t('Models')} + {:else if item?.type === 'channel'} + {$i18n.t('Channels')} + {/if} +
      + {/if} + + + + + {/each} +
      +
      +{/if} diff --git a/src/lib/components/channel/Messages.svelte b/src/lib/components/channel/Messages.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f15a37be4b35ef36547e9b83cbcc14d56b2ab1ee --- /dev/null +++ b/src/lib/components/channel/Messages.svelte @@ -0,0 +1,256 @@ + + +{#if messages} + {@const messageList = messages.slice().reverse()} +
      + {#if !top} + { + console.info('visible'); + if (!messagesLoading) { + loadMoreMessages(); + } + }} + > +
      + +
      {$i18n.t('Loading...')}
      +
      +
      + {:else if !thread} +
      + {#if channel} +
      + {#if channel?.type === 'dm'} +
      + {#each channel.users.filter((u) => u.id !== $user?.id).slice(0, 2) as u, index} + {u.name} + {/each} +
      + {/if} + +
      + {#if channel?.name} + {channel.name} + {:else} + {channel?.users + ?.filter((u) => u.id !== $user?.id) + .map((u) => u.name) + .join(', ')} + {/if} +
      + +
      + {$i18n.t( + 'This channel was created on {{createdAt}}. This is the very beginning of the {{channelName}} channel.', + { + createdAt: dayjs(channel.created_at / 1000000).format('MMMM D, YYYY'), + channelName: channel.name + } + )} +
      +
      + {:else} +
      +
      {$i18n.t('Start of the channel')}
      +
      + {/if} + + {#if messageList.length > 0} +
      + {/if} +
      + {/if} + + {#each messageList as message, messageIdx (id ? `${id}-${message.id}` : message.id)} + { + messages = messages.filter((m) => m.id !== message.id); + + const res = deleteMessage(localStorage.token, message.channel_id, message.id).catch( + (error) => { + toast.error(`${error}`); + return null; + } + ); + }} + onEdit={(content) => { + messages = messages.map((m) => { + if (m.id === message.id) { + m.content = content; + } + return m; + }); + + const res = updateMessage(localStorage.token, message.channel_id, message.id, { + content: content + }).catch((error) => { + toast.error(`${error}`); + return null; + }); + }} + onReply={(message) => { + onReply(message); + }} + onPin={async (message) => { + messages = messages.map((m) => { + if (m.id === message.id) { + m.is_pinned = !m.is_pinned; + m.pinned_by = !m.is_pinned ? null : $user?.id; + m.pinned_at = !m.is_pinned ? null : Date.now() * 1000000; + } + return m; + }); + + const updatedMessage = await pinMessage( + localStorage.token, + message.channel_id, + message.id, + message.is_pinned + ).catch((error) => { + toast.error(`${error}`); + return null; + }); + }} + onThread={(id) => { + onThread(id); + }} + onReaction={(name) => { + if ( + (message?.reactions ?? []) + .find((reaction) => reaction.name === name) + ?.users?.some((u) => u.id === $user?.id) ?? + false + ) { + messages = messages.map((m) => { + if (m.id === message.id) { + const reaction = m.reactions.find((reaction) => reaction.name === name); + + if (reaction) { + reaction.users = reaction.users.filter((u) => u.id !== $user?.id); + reaction.count = reaction.users.length; + + if (reaction.count === 0) { + m.reactions = m.reactions.filter((r) => r.name !== name); + } + } + } + return m; + }); + + const res = removeReaction( + localStorage.token, + message.channel_id, + message.id, + name + ).catch((error) => { + toast.error(`${error}`); + return null; + }); + } else { + messages = messages.map((m) => { + if (m.id === message.id) { + if (m.reactions) { + const reaction = m.reactions.find((reaction) => reaction.name === name); + + if (reaction) { + reaction.users.push({ id: $user?.id, name: $user?.name }); + reaction.count = reaction.users.length; + } else { + m.reactions.push({ + name: name, + users: [{ id: $user?.id, name: $user?.name }], + count: 1 + }); + } + } + } + return m; + }); + + const res = addReaction(localStorage.token, message.channel_id, message.id, name).catch( + (error) => { + toast.error(`${error}`); + return null; + } + ); + } + }} + /> + {/each} + +
      +
      +{/if} diff --git a/src/lib/components/channel/Messages/Message.svelte b/src/lib/components/channel/Messages/Message.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6168eea3fde1823a73075e1fe333515d118cb659 --- /dev/null +++ b/src/lib/components/channel/Messages/Message.svelte @@ -0,0 +1,704 @@ + + + { + await onDelete(); + }} +/> + +{#if message} +
      + + {#if swipeOffsetX > 0} +
      +
      = SWIPE_THRESHOLD} + > + +
      +
      + {/if} + +
      + {#if !edit && !disabled} +
      +
      + {#if onReaction} + (showButtons = false)} + onSubmit={(name) => { + showButtons = false; + onReaction(name); + }} + > + + + + + {/if} + + {#if onReply} + + + + {/if} + + + + + + {#if !thread && onThread} + + + + {/if} + + {#if message.user_id === $user?.id || $user?.role === 'admin'} + {#if onEdit} + + + + {/if} + + {#if onDelete} + + + + {/if} + {/if} +
      +
      + {/if} + + {#if message?.is_pinned} +
      +
      + + {$i18n.t('Pinned')} +
      +
      + {/if} + + {#if message?.reply_to_message?.user} +
      +
      + + +
      + {/if} + +
      +
      + {#if showUserProfile} + {#if message?.meta?.model_id} + {message.meta.model_name { + e.currentTarget.src = '/favicon.png'; + }} + /> + {:else if message.user?.role === 'webhook'} + + {:else} + + + + {/if} + {:else} + + + {#if message.created_at} + + {/if} + {/if} +
      + +
      + {#if showUserProfile} + +
      + {#if message?.meta?.model_id} + {message?.meta?.model_name ?? message?.meta?.model_id} + {:else} + {message?.user?.name} + {/if} +
      + + {#if message.created_at} +
      + + + {#if dayjs(message.created_at / 1000000).isToday()} + {dayjs(message.created_at / 1000000).format('LT')} + {:else} + {$i18n.t(formatDate(message.created_at / 1000000), { + LOCALIZED_TIME: dayjs(message.created_at / 1000000).format('LT'), + LOCALIZED_DATE: dayjs(message.created_at / 1000000).format('L') + })} + {/if} + + +
      + {/if} +
      + {/if} + + {#if message?.data === true} + +
      + +
      + {:else if (message?.data?.files ?? []).length > 0} +
      + {#each message?.data?.files as file} + {@const fileUrl = + file.url.startsWith('data') || file.url.startsWith('http') + ? file.url + : `${WEBUI_API_BASE_URL}/files/${file.url}${file?.content_type ? '/content' : ''}`} +
      + {#if file.type === 'image' || (file?.content_type ?? '').startsWith('image/')} + {file.name} + {:else if file.type === 'video' || (file?.content_type ?? '').startsWith('video/')} + + {:else} + + {/if} +
      + {/each} +
      + {/if} + + {#if edit} +
      + + {:else} + +
      startEditing(i)} + > + {@html renderMarkdown(toStr(cell.source))} +
      + {/if} + {:else if cell.cell_type === 'code'} +
      +
      + {#if runningCell === i} +
      + {:else if baseUrl && apiKey && filePath} + + {/if} +
      + {#if cell.execution_count !== undefined && cell.execution_count !== null} + [{cell.execution_count}] + {:else} + [ ] + {/if} +
      +
      +
      + {#if editingCell[i]} + { + editedSources[i] = e.detail; + }} + on:run={() => runCell(i)} + on:cancel={() => cancelEditing(i)} + /> + {:else} + +
      startEditing(i)} + > + {#if highlightedCells[i]} +
      + {@html highlightedCells[i]} +
      + {:else} +
      {toStr(cell.source)}
      + {/if} +
      + {/if} + + {#if cell.outputs && cell.outputs.length > 0} +
      + {#each cell.outputs as output} + {#if output.output_type === 'error'} +
      {stripAnsi(
      +												(output.traceback ?? []).join('\n') || `${output.ename}: ${output.evalue}`
      +											)}
      + {:else} + {@const html = getOutputHtml(output)} + {@const images = getOutputImages(output)} + {@const text = getOutputText(output)} + {#if html} +
      {@html html}
      + {/if} + {#each images as src} + Output + {/each} + {#if text} +
      {text}
      + {/if} + {/if} + {/each} +
      + {/if} +
      +
      + {:else if cell.cell_type === 'raw'} +
      {toStr(cell.source)}
      + {/if} +
      + {/each} +
      + + diff --git a/src/lib/components/chat/FileNav/PortList.svelte b/src/lib/components/chat/FileNav/PortList.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9e85951126e2f26c2de24979bcfef454eb0f01a3 --- /dev/null +++ b/src/lib/components/chat/FileNav/PortList.svelte @@ -0,0 +1,150 @@ + + +
      + + + + + + {#if expanded} +
      + {#if ports.length === 0} +
      + {$i18n.t('No servers detected')} +
      + {:else} + {#each ports as port} + + {/each} + {/if} +
      + {/if} +
      diff --git a/src/lib/components/chat/FileNav/PortPreview.svelte b/src/lib/components/chat/FileNav/PortPreview.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fdefb2d49f890b1b8d40e49df44d3eb97809ae55 --- /dev/null +++ b/src/lib/components/chat/FileNav/PortPreview.svelte @@ -0,0 +1,296 @@ + + +
      + +
      + + + + + + + + + + + + + + + + +
      + +
      + + + + + + + + + + +
      + + + {#if isLoading} +
      +
      +
      + {/if} + + +
      + {#if overlay} +
      + {/if} + {#key iframeKey} + + {:else} + {@const rawContent = document.document.trim().replace(/\n\n+/g, '\n\n')} + {@const isTruncated = + ($settings?.renderMarkdownInPreviews ?? true) && + rawContent.length > CONTENT_PREVIEW_LIMIT && + !expandedDocs.has(documentIdx)} + {#if $settings?.renderMarkdownInPreviews ?? true} +
      + +
      + {#if isTruncated} + + {/if} + {:else} +
      {rawContent}
      + {/if} + {/if} +
      +
      + {/each} +
      +
      +
      + diff --git a/src/lib/components/chat/Messages/Citations/CitationsModal.svelte b/src/lib/components/chat/Messages/Citations/CitationsModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..435e2735cdd18f5bdd2264487bf626b4d8551fd6 --- /dev/null +++ b/src/lib/components/chat/Messages/Citations/CitationsModal.svelte @@ -0,0 +1,82 @@ + + + + + +
      +
      +
      + {$i18n.t('Citations')} +
      + +
      + +
      +
      + {#each citations as citation, idx} + + {/each} +
      +
      +
      +
      diff --git a/src/lib/components/chat/Messages/CodeBlock.svelte b/src/lib/components/chat/Messages/CodeBlock.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7e385925337f51c83aebe87032d70b5d79e3b6a5 --- /dev/null +++ b/src/lib/components/chat/Messages/CodeBlock.svelte @@ -0,0 +1,634 @@ + + +
      +
      + {#if ['mermaid', 'vega', 'vega-lite'].includes(lang)} + {#if renderHTML} + + {:else} +
      + {#if renderError} +
      + {renderError} +
      + {/if} +
      {code}
      +
      + {/if} + {:else} +
      +
      + + + {lang} + + +
      + +
      + + + {#if ($config?.features?.enable_code_execution ?? true) && (lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code)))} + {#if executing} +
      + {$i18n.t('Running')} +
      + {:else if run} + + {/if} + {/if} + + {#if save} + + {/if} + + + + {#if preview && ['html', 'svg'].includes(lang)} + + {/if} +
      +
      + +
      +
      + + {#if !collapsed} + {#if edit} + { + saveCode(); + }} + onChange={(value) => { + _code = value; + }} + /> + {:else} +
      {@html hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value ||
      +									code}
      + {/if} + {:else} +
      + + {$i18n.t('{{COUNT}} hidden lines', { + COUNT: code.split('\n').length + })} + +
      + {/if} +
      + + {#if !collapsed} +
      + + {#if executing || stdout || stderr || result || files} +
      + {#if executing} +
      +
      {$i18n.t('STDOUT/STDERR')}
      +
      {$i18n.t('Running...')}
      +
      + {:else} + {#if stdout || stderr} +
      +
      {$i18n.t('STDOUT/STDERR')}
      +
      + {stdout || stderr} +
      +
      + {/if} + {#if result || files} +
      +
      {$i18n.t('RESULT')}
      + {#if result} +
      {`${JSON.stringify(result)}`}
      + {/if} + {#if files} +
      + {#each files as file} + {#if file.type.startsWith('image')} + Output + {/if} + {/each} +
      + {/if} +
      + {/if} + {/if} +
      + {/if} + {/if} + {/if} +
      +
      diff --git a/src/lib/components/chat/Messages/CodeExecutionModal.svelte b/src/lib/components/chat/Messages/CodeExecutionModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9284eb220c18c85fccfe3b6ab830e1b693b9e807 --- /dev/null +++ b/src/lib/components/chat/Messages/CodeExecutionModal.svelte @@ -0,0 +1,109 @@ + + + +
      +
      +
      + {#if codeExecution?.result} +
      + {#if codeExecution.result?.error} + + {:else if codeExecution.result?.output} + + {:else} + + {/if} +
      + {/if} + +
      + {#if !codeExecution?.result} +
      + +
      + {/if} + +
      + {#if codeExecution?.name} + {$i18n.t('Code execution')}: {codeExecution?.name} + {:else} + {$i18n.t('Code execution')} + {/if} +
      +
      +
      + +
      + +
      +
      +
      + +
      + + {#if codeExecution?.result && (codeExecution?.result?.error || codeExecution?.result?.output)} +
      + {#if codeExecution?.result?.error} +
      +
      {$i18n.t('ERROR')}
      +
      {codeExecution?.result?.error}
      +
      + {/if} + {#if codeExecution?.result?.output} +
      +
      {$i18n.t('OUTPUT')}
      +
      {codeExecution?.result?.output}
      +
      + {/if} +
      + {/if} + {#if codeExecution?.result?.files && codeExecution?.result?.files.length > 0} +
      +
      +
      + {$i18n.t('Files')} +
      +
        + {#each codeExecution?.result?.files as file} +
      • + {file.name} +
      • + {/each} +
      +
      + {/if} +
      +
      +
      +
      diff --git a/src/lib/components/chat/Messages/CodeExecutions.svelte b/src/lib/components/chat/Messages/CodeExecutions.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2c53eaeb3898576cf083c865d356203f61b6f2ce --- /dev/null +++ b/src/lib/components/chat/Messages/CodeExecutions.svelte @@ -0,0 +1,80 @@ + + + + +{#if codeExecutions.length > 0} +
      + {#each codeExecutions as execution (execution.id)} +
      + +
      + {/each} +
      +{/if} + + diff --git a/src/lib/components/chat/Messages/ContentRenderer.svelte b/src/lib/components/chat/Messages/ContentRenderer.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ec1454a32d182850435e06be035068577378c3e1 --- /dev/null +++ b/src/lib/components/chat/Messages/ContentRenderer.svelte @@ -0,0 +1,226 @@ + + +
      + { + const { lang, text: code } = token; + + if ( + ($settings?.detectArtifacts ?? true) && + (['html', 'svg'].includes(lang) || (lang === 'xml' && code.includes('svg'))) && + !$mobile && + $chatId + ) { + await tick(); + showArtifacts.set(true); + showControls.set(true); + } + }} + onPreview={async (value) => { + console.log('Preview', value); + await artifactCode.set(value); + await showControls.set(true); + await showArtifacts.set(true); + await showEmbeds.set(false); + }} + /> +
      + +{#if floatingButtons} + { + onSetInputText(text); + closeFloatingButtons(); + }} + /> +{/if} diff --git a/src/lib/components/chat/Messages/Error.svelte b/src/lib/components/chat/Messages/Error.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2c046485c2c96294877ae495aa58d7408cf41f6d --- /dev/null +++ b/src/lib/components/chat/Messages/Error.svelte @@ -0,0 +1,29 @@ + + +
      +
      + +
      + +
      + {#if typeof content === 'string'} + {content} + {:else if typeof content === 'object' && content !== null} + {#if content?.error && content?.error?.message} + {content.error.message} + {:else if content?.detail} + {content.detail} + {:else if content?.message} + {content.message} + {:else} + {JSON.stringify(content)} + {/if} + {:else} + {JSON.stringify(content)} + {/if} +
      +
      diff --git a/src/lib/components/chat/Messages/Markdown.svelte b/src/lib/components/chat/Messages/Markdown.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3cbef944e4a50f025bb55d2ab7b191db86fadfa9 --- /dev/null +++ b/src/lib/components/chat/Messages/Markdown.svelte @@ -0,0 +1,107 @@ + + +{#key id} + +{/key} diff --git a/src/lib/components/chat/Messages/Markdown/AlertRenderer.svelte b/src/lib/components/chat/Messages/Markdown/AlertRenderer.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ae00acb60bec7c7ac00e37d6fb4ed6c71348859d --- /dev/null +++ b/src/lib/components/chat/Messages/Markdown/AlertRenderer.svelte @@ -0,0 +1,110 @@ + + + + + +
      +
      + + {alert.type} +
      +
      + +
      +
      diff --git a/src/lib/components/chat/Messages/Markdown/ColonFenceBlock.svelte b/src/lib/components/chat/Messages/Markdown/ColonFenceBlock.svelte new file mode 100644 index 0000000000000000000000000000000000000000..734f251b294475c9e2e66e1e3c7ab60e72f4c454 --- /dev/null +++ b/src/lib/components/chat/Messages/Markdown/ColonFenceBlock.svelte @@ -0,0 +1,83 @@ + + +
      + +
      + + {label} + + + +
      + + +
      + +
      +
      diff --git a/src/lib/components/chat/Messages/Markdown/ConsecutiveDetailsGroup.svelte b/src/lib/components/chat/Messages/Markdown/ConsecutiveDetailsGroup.svelte new file mode 100644 index 0000000000000000000000000000000000000000..598511eb25bafcd940e62063d3b9665dabaf95c3 --- /dev/null +++ b/src/lib/components/chat/Messages/Markdown/ConsecutiveDetailsGroup.svelte @@ -0,0 +1,178 @@ + + +
      + + + + {#if open} +
      +
      + +
      +
      + {/if} + + {#if allEmbeds.length > 0} + {#each allEmbeds as embedItem, idx} +
      + +
      + {/each} + {/if} +
      diff --git a/src/lib/components/chat/Messages/Markdown/HTMLToken.svelte b/src/lib/components/chat/Messages/Markdown/HTMLToken.svelte new file mode 100644 index 0000000000000000000000000000000000000000..da9690c631a7141204af8e16033323e165256005 --- /dev/null +++ b/src/lib/components/chat/Messages/Markdown/HTMLToken.svelte @@ -0,0 +1,134 @@ + + +{#if token.type === 'html'} + {#if html && html.includes(']*>([\s\S]*?)<\/video>/)} + {@const videoSrc = video && video[1]} + {#if videoSrc} + + + {:else} + {token.text} + {/if} + {:else if html && html.includes(']*>([\s\S]*?)<\/audio>/)} + {@const audioSrc = audio && audio[1]} + {#if audioSrc} + + + {:else} + {token.text} + {/if} + {:else if token.text && token.text.match(/]*src="https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})(?:\?[^"]*)?"[^>]*><\/iframe>/)} + {@const match = token.text.match( + /]*src="https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})(?:\?[^"]*)?"[^>]*><\/iframe>/ + )} + {@const ytId = match && match[1]} + {#if ytId} + + {/if} + {:else if token.text && token.text.includes(']*src="([^"]+)"[^>]*><\/iframe>/)} + {@const iframeSrc = match && match[1]} + {#if iframeSrc} + + {:else} + {token.text} + {/if} + {:else if token.text && token.text.includes('/)} + {@const statusTitle = match && match[1]} + {@const statusDone = match && match[2] === 'true'} + {#if statusTitle} +
      +
      + {statusTitle} +
      +
      + {:else} + {token.text} + {/if} + {:else if token.text.includes(` { + try { + e.currentTarget.style.height = + e.currentTarget.contentWindow.document.body.scrollHeight + 20 + 'px'; + } catch {} + }} + > + {/if} + {:else if token.text.trim().match(/^$/i)} +
      + {:else} + {token.text} + {/if} +{/if} diff --git a/src/lib/components/chat/Messages/Markdown/KatexRenderer.svelte b/src/lib/components/chat/Messages/Markdown/KatexRenderer.svelte new file mode 100644 index 0000000000000000000000000000000000000000..efbb46df45925a3ede05710ce40921d24d48e7d5 --- /dev/null +++ b/src/lib/components/chat/Messages/Markdown/KatexRenderer.svelte @@ -0,0 +1,33 @@ + + + + +{#if renderToString} + {@html renderToString(content, { displayMode, throwOnError: false })} +{/if} diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0daf389eecedc115939b4e787c3d943da6ed4bb8 --- /dev/null +++ b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte @@ -0,0 +1,142 @@ + + +{#each tokens as token, tokenIdx (tokenIdx)} + {#if token.type === 'escape'} + {unescapeHtml(token.text)} + {:else if token.type === 'html'} + + {:else if token.type === 'link'} + {@const noteId = getNoteIdFromHref(token.href)} + {#if noteId} + + {:else if token.tokens} + handleLinkClick(e, token.href)} + > + + + {:else} + handleLinkClick(e, token.href)}>{token.text} + {/if} + {:else if token.type === 'image'} + {token.text} + {:else if token.type === 'strong'} + + {:else if token.type === 'em'} + + {:else if token.type === 'codespan'} + + {:else if token.type === 'br'} +
      + {:else if token.type === 'del'} + + {:else if token.type === 'inlineKatex'} + {#if token.text} + + {/if} + {:else if token.type === 'iframe'} + + {:else if token.type === 'mention'} + + {:else if token.type === 'footnote'} + {@html DOMPurify.sanitize( + `${token.escapedText}` + ) || ''} + {:else if token.type === 'citation'} + {#if (sourceIds ?? []).length > 0} + + {:else} + + {/if} + {:else if token.type === 'text'} + + {/if} +{/each} diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/CodespanToken.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/CodespanToken.svelte new file mode 100644 index 0000000000000000000000000000000000000000..48144f9a0d2d70dc38b6c7c02ff56fdbff11d25d --- /dev/null +++ b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/CodespanToken.svelte @@ -0,0 +1,21 @@ + + + + + { + copyToClipboard(unescapeHtml(token.text)); + toast.success($i18n.t('Copied to clipboard')); + }}>{unescapeHtml(token.text)} diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/MentionToken.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/MentionToken.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9497444be678afa20c5eb2c8cd3be8392666580b --- /dev/null +++ b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/MentionToken.svelte @@ -0,0 +1,122 @@ + + + + + + + + { + if (triggerChar === '@') { + if (idType === 'U') { + // Open user profile + console.log('Clicked user mention', id); + } else if (idType === 'M') { + console.log('Clicked model mention', id); + await goto(`/?model=${id}`); + } + } else if (triggerChar === '#') { + if (idType === 'C') { + // Open channel + if ($channels.find((c) => c.id === id)) { + await goto(`/channels/${id}`); + } + } else if (idType === 'T') { + // Open thread + } + } else { + // Unknown trigger char, just log + console.log('Clicked mention', id); + } + }} + > + {triggerChar}{label} + + + + {#if triggerChar === '@' && idType === 'U'} + + {/if} + diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/NoteLinkToken.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/NoteLinkToken.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ec6e863d7538bba44d26c2b2883ec75074f7622e --- /dev/null +++ b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/NoteLinkToken.svelte @@ -0,0 +1,73 @@ + + + + + diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/TextToken.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/TextToken.svelte new file mode 100644 index 0000000000000000000000000000000000000000..8ba5de2233f54aeafc6bf48c2608d2d3fc122979 --- /dev/null +++ b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/TextToken.svelte @@ -0,0 +1,14 @@ + + +{#if done} + {token?.raw} +{:else} + {#each (token?.raw ?? '').split(' ') as text} + + {text}{' '} + + {/each} +{/if} diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte new file mode 100644 index 0000000000000000000000000000000000000000..da4deaaa12dcdab51d3a75fe3c0b803774d5c54c --- /dev/null +++ b/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte @@ -0,0 +1,556 @@ + + + +{#each displayTokens as token, tokenIdx (tokenIdx)} + {#if token.type === 'hr'} +
      + {:else if token.type === 'heading'} + + + + {:else if token.type === 'code'} + {#if token.raw.includes('```')} + { + onSave({ + raw: token.raw, + oldContent: token.text, + newContent: value + }); + }} + {onUpdate} + {onPreview} + /> + {:else} + {token.text} + {/if} + {:else if token.type === 'table'} +
      +
      + + + + {#each token.header as header, headerIdx} + + {/each} + + + + {#each token.rows as row, rowIdx} + + {#each row ?? [] as cell, cellIdx} + + {/each} + + {/each} + +
      +
      +
      + +
      +
      +
      +
      + +
      +
      +
      + + +
      + {:else if token.type === 'blockquote'} + {@const alert = alertComponent(token)} + {#if alert} + + {:else} +
      + +
      + {/if} + {:else if token.type === 'list'} + {#if token.ordered} +
        + {#each token.items as item, itemIdx} +
      1. + {#if item?.task} + { + onTaskClick({ + id: id, + token: token, + tokenIdx: tokenIdx, + item: item, + itemIdx: itemIdx, + checked: e.target.checked + }); + }} + /> + {/if} + + +
      2. + {/each} +
      + {:else} +
        + {#each token.items as item, itemIdx} +
      • + {#if item?.task} + { + onTaskClick({ + id: id, + token: token, + tokenIdx: tokenIdx, + item: item, + itemIdx: itemIdx, + checked: e.target.checked + }); + }} + /> + +
        + +
        + {:else} + + {/if} +
      • + {/each} +
      + {/if} + {:else if token.type === 'detail_group'} + +
      + {#each token.items as detailToken, detailIdx} + {@const textContent = getDetailTextContent(detailToken)} + + {#if detailToken?.attributes?.type === 'tool_calls'} + + {:else if textContent.length > 0} + +
      + +
      +
      + {:else} + + {/if} + {/each} +
      +
      + {:else if token.type === 'details'} + {@const textContent = getDetailTextContent(token)} + + {#if token?.attributes?.type === 'tool_calls'} + + + {:else if textContent.length > 0} + +
      + +
      +
      + {:else} + + {/if} + {:else if token.type === 'html'} + + {:else if token.type === 'iframe'} + + {:else if token.type === 'paragraph'} + {#if paragraphTag == 'span'} + + + + {:else} +

      + +

      + {/if} + {:else if token.type === 'text'} + {#if top} +

      + {#if token.tokens} + + {:else} + {unescapeHtml(token.text)} + {/if} +

      + {:else if token.tokens} + + {:else} + {unescapeHtml(token.text)} + {/if} + {:else if token.type === 'inlineKatex'} + {#if token.text} + + {/if} + {:else if token.type === 'blockKatex'} + {#if token.text} + + {/if} + {:else if token.type === 'colonFence'} + + {:else if token.type === 'space'} +
      + {:else} + {console.log('Unknown token', token)} + {/if} +{/each} diff --git a/src/lib/components/chat/Messages/Markdown/Source.svelte b/src/lib/components/chat/Messages/Markdown/Source.svelte new file mode 100644 index 0000000000000000000000000000000000000000..cbe8670383d79ebbef590708f3fcb317f26777fe --- /dev/null +++ b/src/lib/components/chat/Messages/Markdown/Source.svelte @@ -0,0 +1,53 @@ + + +{#if title !== 'N/A'} + +{/if} diff --git a/src/lib/components/chat/Messages/Markdown/SourceToken.svelte b/src/lib/components/chat/Messages/Markdown/SourceToken.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d8111576ddd8446115ab88ccf2a74c8284aa53c5 --- /dev/null +++ b/src/lib/components/chat/Messages/Markdown/SourceToken.svelte @@ -0,0 +1,80 @@ + + +{#if sourceIds} + {#if (token?.ids ?? []).length == 1} + {@const id = token.ids[0]} + {@const identifier = token.citationIdentifiers ? token.citationIdentifiers[0] : id - 1} + + {:else} + + + + + + +
      + {#each token.citationIdentifiers ?? token.ids as identifier} + {@const id = + typeof identifier === 'string' ? parseInt(identifier.split('#')[0]) : identifier} +
      + +
      + {/each} +
      +
      +
      +
      + {/if} +{:else} + {token.raw} +{/if} diff --git a/src/lib/components/chat/Messages/Message.svelte b/src/lib/components/chat/Messages/Message.svelte new file mode 100644 index 0000000000000000000000000000000000000000..242e84f4590a9d92edab2b147e02099d6e306507 --- /dev/null +++ b/src/lib/components/chat/Messages/Message.svelte @@ -0,0 +1,140 @@ + + +
      + {#if history.messages[messageId]} + {#if history.messages[messageId].role === 'user'} + message.parentId === null) + .map((message) => message.id) ?? [])} + {gotoMessage} + {showPreviousMessage} + {showNextMessage} + {editMessage} + {deleteMessage} + {readOnly} + {editCodeBlock} + {topPadding} + /> + {:else if (history.messages[history.messages[messageId].parentId]?.models?.length ?? 1) === 1} + + {:else} + {#key messageId} + + {/key} + {/if} + {/if} +
      + + diff --git a/src/lib/components/chat/Messages/MultiResponseMessages.svelte b/src/lib/components/chat/Messages/MultiResponseMessages.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a6d5a9b8a71f8361b58e5ee059f0becfc3521e1a --- /dev/null +++ b/src/lib/components/chat/Messages/MultiResponseMessages.svelte @@ -0,0 +1,448 @@ + + +{#if parentMessage} +
      +
      + {#if $settings?.displayMultiModelResponsesInTabs ?? false} +
      +
      +
      { + e.currentTarget.scrollLeft += e.deltaY; + }} + > + {#each Object.keys(groupedMessageIds) as modelIdx} + {#if groupedMessageIdsIdx[modelIdx] !== undefined && (groupedMessageIds[modelIdx]?.messageIds ?? []).length > 0} + + + + {@const _messageId = + groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]]} + + {@const model = $models.find((m) => m.id === history.messages[_messageId]?.model)} + + + {/if} + {/each} +
      +
      + + {#if selectedModelIdx !== null} + {#key history.currentId} + {#if message} + gotoMessage(selectedModelIdx, messageIdx)} + showPreviousMessage={() => showPreviousMessage(selectedModelIdx)} + showNextMessage={() => showNextMessage(selectedModelIdx)} + {setInputText} + {updateChat} + {editMessage} + {saveMessage} + {rateMessage} + {deleteMessage} + {actionMessage} + {submitMessage} + {continueResponse} + regenerateResponse={async (message, prompt = null) => { + regenerateResponse(message, prompt); + await tick(); + groupedMessageIdsIdx[selectedModelIdx] = + groupedMessageIds[selectedModelIdx].messageIds.length - 1; + }} + {addMessages} + {readOnly} + {topPadding} + /> + {/if} + {/key} + {/if} +
      + {:else} + {#each Object.keys(groupedMessageIds) as modelIdx} + {#if groupedMessageIdsIdx[modelIdx] !== undefined && groupedMessageIds[modelIdx].messageIds.length > 0} + + + {@const _messageId = + groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]]} + +
      { + onGroupClick(_messageId, modelIdx); + }} + > + {#key history.currentId} + {#if message} + gotoMessage(modelIdx, messageIdx)} + showPreviousMessage={() => showPreviousMessage(modelIdx)} + showNextMessage={() => showNextMessage(modelIdx)} + {setInputText} + {updateChat} + {editMessage} + {saveMessage} + {rateMessage} + {deleteMessage} + {actionMessage} + {submitMessage} + {continueResponse} + regenerateResponse={async (message, prompt = null) => { + regenerateResponse(message, prompt); + await tick(); + groupedMessageIdsIdx[modelIdx] = + groupedMessageIds[modelIdx].messageIds.length - 1; + }} + {addMessages} + {readOnly} + {editCodeBlock} + {topPadding} + /> + {/if} + {/key} +
      + {/if} + {/each} + {/if} +
      + + {#if !readOnly} + {#if !Object.keys(groupedMessageIds).find((modelIdx) => { + const { messageIds } = groupedMessageIds[modelIdx]; + const _messageId = messageIds[groupedMessageIdsIdx[modelIdx]]; + return !history.messages[_messageId]?.done ?? false; + })} +
      +
      + {#if history.messages[messageId]?.merged?.status} + {@const message = history.messages[messageId]?.merged} + +
      + + {$i18n.t('Merged Response')} + + {#if message.timestamp} + + {/if} + + +
      + {#if (message?.content ?? '') === ''} + + {:else} + + {/if} +
      +
      + {/if} +
      + + {#if isLastMessage} +
      + + + +
      + {/if} +
      + {/if} + {/if} +
      +{/if} diff --git a/src/lib/components/chat/Messages/Name.svelte b/src/lib/components/chat/Messages/Name.svelte new file mode 100644 index 0000000000000000000000000000000000000000..82f2c5b21b257b0b52a9fc486937db00a42c644e --- /dev/null +++ b/src/lib/components/chat/Messages/Name.svelte @@ -0,0 +1,3 @@ +
      + +
      diff --git a/src/lib/components/chat/Messages/ProfileImage.svelte b/src/lib/components/chat/Messages/ProfileImage.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d837ab05ab4fcc744c37f634895f86015a6ca140 --- /dev/null +++ b/src/lib/components/chat/Messages/ProfileImage.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/chat/Messages/RateComment.svelte b/src/lib/components/chat/Messages/RateComment.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fad599f35f941bba176d8d85367c6ecbacae0595 --- /dev/null +++ b/src/lib/components/chat/Messages/RateComment.svelte @@ -0,0 +1,275 @@ + + +{#if message?.arena} +
      + {$i18n.t('This response was generated by "{{model}}"', { + model: selectedModel ? (selectedModel?.name ?? selectedModel.id) : message.selectedModelId + })} +
      +{/if} + +
      +
      +
      {$i18n.t('How would you rate this response?')}
      + + + + +
      + +
      +
      +
      + + {#each Array.from({ length: 10 }).map((_, i) => i + 1) as rating} + + {/each} +
      + +
      +
      + 1 - {$i18n.t('Awful')} +
      + +
      + 10 - {$i18n.t('Amazing')} +
      +
      +
      +
      + +
      + {#if reasons.length > 0} +
      {$i18n.t('Why?')}
      + +
      + {#each reasons as reason} + + {/each} +
      + {/if} +
      + +
      + + {/if} +
      +
      + {/if} + + {#if (valvesSpec.properties[property]?.description ?? null) !== null} +
      + {valvesSpec.properties[property].description} +
      + {/if} +
      + {/each} +{:else} +
      {$i18n.t('No valves')}
      +{/if} diff --git a/src/lib/components/common/Valves/MapSelector.svelte b/src/lib/components/common/Valves/MapSelector.svelte new file mode 100644 index 0000000000000000000000000000000000000000..cf4de4b445f816b5297472f4bd15d6031d58bc73 --- /dev/null +++ b/src/lib/components/common/Valves/MapSelector.svelte @@ -0,0 +1,84 @@ + + +
      +
      +
      diff --git a/src/lib/components/icons/AdjustmentsHorizontal.svelte b/src/lib/components/icons/AdjustmentsHorizontal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..31071fb1603ab34292451c4cbc8714f7ce328ab8 --- /dev/null +++ b/src/lib/components/icons/AdjustmentsHorizontal.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/AdjustmentsHorizontalOutline.svelte b/src/lib/components/icons/AdjustmentsHorizontalOutline.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5a99d126d53bfa16895a63887838b1e05807b29d --- /dev/null +++ b/src/lib/components/icons/AdjustmentsHorizontalOutline.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Agile.svelte b/src/lib/components/icons/Agile.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e43811bd7e248c6f446b8683dd600477c638e0e9 --- /dev/null +++ b/src/lib/components/icons/Agile.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/lib/components/icons/AlignHorizontal.svelte b/src/lib/components/icons/AlignHorizontal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2cd28d5578188351a973341a5936e50ced86e159 --- /dev/null +++ b/src/lib/components/icons/AlignHorizontal.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/icons/AlignVertical.svelte b/src/lib/components/icons/AlignVertical.svelte new file mode 100644 index 0000000000000000000000000000000000000000..92db5b83e8bcf586de61ecce8c0b5d6559748040 --- /dev/null +++ b/src/lib/components/icons/AlignVertical.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/icons/AppNotification.svelte b/src/lib/components/icons/AppNotification.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fa24a1f8fc4060f10db40cb0d5e33109dddcfe68 --- /dev/null +++ b/src/lib/components/icons/AppNotification.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/ArchiveBox.svelte b/src/lib/components/icons/ArchiveBox.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6f60c7b68dfe880a23e3de5f5599dc0fb957f78f --- /dev/null +++ b/src/lib/components/icons/ArchiveBox.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/ArrowDownTray.svelte b/src/lib/components/icons/ArrowDownTray.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b59c520e77af3b77d9e2bd5af37ed79ef94b20ea --- /dev/null +++ b/src/lib/components/icons/ArrowDownTray.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/ArrowForward.svelte b/src/lib/components/icons/ArrowForward.svelte new file mode 100644 index 0000000000000000000000000000000000000000..264089367ce5fa58fd3a4163b546e01d34765808 --- /dev/null +++ b/src/lib/components/icons/ArrowForward.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/icons/ArrowLeft.svelte b/src/lib/components/icons/ArrowLeft.svelte new file mode 100644 index 0000000000000000000000000000000000000000..bc4ac70b5b4ce49f6f48cd475b9a66bdaca33421 --- /dev/null +++ b/src/lib/components/icons/ArrowLeft.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/icons/ArrowLeftTag.svelte b/src/lib/components/icons/ArrowLeftTag.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d66cf5ff81bcfef00468dcaa2bef17203e761e04 --- /dev/null +++ b/src/lib/components/icons/ArrowLeftTag.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/icons/ArrowPath.svelte b/src/lib/components/icons/ArrowPath.svelte new file mode 100644 index 0000000000000000000000000000000000000000..df545cf37d87ee289ab84a161aeca21dc81d140a --- /dev/null +++ b/src/lib/components/icons/ArrowPath.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/ArrowRight.svelte b/src/lib/components/icons/ArrowRight.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e52947d402dbb628619aa29e33463c3fe75e2171 --- /dev/null +++ b/src/lib/components/icons/ArrowRight.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/icons/ArrowRightCircle.svelte b/src/lib/components/icons/ArrowRightCircle.svelte new file mode 100644 index 0000000000000000000000000000000000000000..273a5884bcd24e218f3d0685d49e4258427ba4c1 --- /dev/null +++ b/src/lib/components/icons/ArrowRightCircle.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/ArrowRightTag.svelte b/src/lib/components/icons/ArrowRightTag.svelte new file mode 100644 index 0000000000000000000000000000000000000000..34553da678989024ea3db63dc5310f7db6e72ae8 --- /dev/null +++ b/src/lib/components/icons/ArrowRightTag.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/icons/ArrowTurnDownRight.svelte b/src/lib/components/icons/ArrowTurnDownRight.svelte new file mode 100644 index 0000000000000000000000000000000000000000..40b6e9010fb204470ad7df63cfbb0d894cce1bd4 --- /dev/null +++ b/src/lib/components/icons/ArrowTurnDownRight.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/icons/ArrowUpCircle.svelte b/src/lib/components/icons/ArrowUpCircle.svelte new file mode 100644 index 0000000000000000000000000000000000000000..eb34a67bee553c17ab568a771a81c1e1b3020b33 --- /dev/null +++ b/src/lib/components/icons/ArrowUpCircle.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/ArrowUpLeft.svelte b/src/lib/components/icons/ArrowUpLeft.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5bcc5ef9c471fd142a6370251c34c73821814edc --- /dev/null +++ b/src/lib/components/icons/ArrowUpLeft.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/ArrowUpLeftAlt.svelte b/src/lib/components/icons/ArrowUpLeftAlt.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ed8100742afeb952a534093e3d9820b556486747 --- /dev/null +++ b/src/lib/components/icons/ArrowUpLeftAlt.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/ArrowUpTray.svelte b/src/lib/components/icons/ArrowUpTray.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4a896bd7fa868bc36c79c845bf6d3fdfd2c6775b --- /dev/null +++ b/src/lib/components/icons/ArrowUpTray.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/ArrowUturnLeft.svelte b/src/lib/components/icons/ArrowUturnLeft.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b8faac77297af21c5aa32845eb05e0aca028fad5 --- /dev/null +++ b/src/lib/components/icons/ArrowUturnLeft.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/ArrowUturnRight.svelte b/src/lib/components/icons/ArrowUturnRight.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ac71cdc1ca273803258e7c62343dd66084b3992b --- /dev/null +++ b/src/lib/components/icons/ArrowUturnRight.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/ArrowsPointingOut.svelte b/src/lib/components/icons/ArrowsPointingOut.svelte new file mode 100644 index 0000000000000000000000000000000000000000..8fde76dbb929f794579cd2b5bb9ff927741c8c40 --- /dev/null +++ b/src/lib/components/icons/ArrowsPointingOut.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Bars3BottomLeft.svelte b/src/lib/components/icons/Bars3BottomLeft.svelte new file mode 100644 index 0000000000000000000000000000000000000000..132078f098d63ffaf359e5357dd031faffced71b --- /dev/null +++ b/src/lib/components/icons/Bars3BottomLeft.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/icons/BarsArrowUp.svelte b/src/lib/components/icons/BarsArrowUp.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9beddd8f6a934f0876c5914bacbc5063839edece --- /dev/null +++ b/src/lib/components/icons/BarsArrowUp.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Bold.svelte b/src/lib/components/icons/Bold.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ffb8cbce531094e8125db0ff574d8303f1b310a8 --- /dev/null +++ b/src/lib/components/icons/Bold.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/icons/Bolt.svelte b/src/lib/components/icons/Bolt.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7f4dc999f595c93f1595ef75d92c82e74bea58d0 --- /dev/null +++ b/src/lib/components/icons/Bolt.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/BookOpen.svelte b/src/lib/components/icons/BookOpen.svelte new file mode 100644 index 0000000000000000000000000000000000000000..668a9e6f58db14f7aafda0185a06eba771640c47 --- /dev/null +++ b/src/lib/components/icons/BookOpen.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Bookmark.svelte b/src/lib/components/icons/Bookmark.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9152332b79c60a1ab6af29f1d076cdb6996d3aed --- /dev/null +++ b/src/lib/components/icons/Bookmark.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/BookmarkSlash.svelte b/src/lib/components/icons/BookmarkSlash.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e5ec865b67be823f5c254e0f045c2ec6a1cda148 --- /dev/null +++ b/src/lib/components/icons/BookmarkSlash.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Calendar.svelte b/src/lib/components/icons/Calendar.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c0ee618aa5c945ca6683518390ffc6c319b5779f --- /dev/null +++ b/src/lib/components/icons/Calendar.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/CalendarSolid.svelte b/src/lib/components/icons/CalendarSolid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ebd12bfba458839c0f2b5ee33478a90300cfb9ad --- /dev/null +++ b/src/lib/components/icons/CalendarSolid.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/icons/Camera.svelte b/src/lib/components/icons/Camera.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2210f1a0a6bf73c64a8addf94ab1df17779a36eb --- /dev/null +++ b/src/lib/components/icons/Camera.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/CameraSolid.svelte b/src/lib/components/icons/CameraSolid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3026ef4e3af4abff697a47741bca64f35eb4b263 --- /dev/null +++ b/src/lib/components/icons/CameraSolid.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/icons/ChartBar.svelte b/src/lib/components/icons/ChartBar.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2fa8d6d530ca513476d93109670b276fd95cbf97 --- /dev/null +++ b/src/lib/components/icons/ChartBar.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/icons/ChatBubble.svelte b/src/lib/components/icons/ChatBubble.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fc625daa452b4922e249aea4bbb4b67ff0a295c7 --- /dev/null +++ b/src/lib/components/icons/ChatBubble.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/ChatBubbleDotted.svelte b/src/lib/components/icons/ChatBubbleDotted.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5fe56fa26ce70b0fe826f3fce562025674e64530 --- /dev/null +++ b/src/lib/components/icons/ChatBubbleDotted.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/icons/ChatBubbleDottedChecked.svelte b/src/lib/components/icons/ChatBubbleDottedChecked.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4340afa887910ebddbc9a9c972b4eb97cf58bc86 --- /dev/null +++ b/src/lib/components/icons/ChatBubbleDottedChecked.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/icons/ChatBubbleOval.svelte b/src/lib/components/icons/ChatBubbleOval.svelte new file mode 100644 index 0000000000000000000000000000000000000000..81c8b2a6c14e15201ab4b7dc498fa7b2c2655ac5 --- /dev/null +++ b/src/lib/components/icons/ChatBubbleOval.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/ChatBubbles.svelte b/src/lib/components/icons/ChatBubbles.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7d7aaa881d52d33e6611db4bceda0f256ae739a9 --- /dev/null +++ b/src/lib/components/icons/ChatBubbles.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/ChatCheck.svelte b/src/lib/components/icons/ChatCheck.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7b6099cf55ed641a0689e343614d1a240f1c9f4a --- /dev/null +++ b/src/lib/components/icons/ChatCheck.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/icons/ChatPlus.svelte b/src/lib/components/icons/ChatPlus.svelte new file mode 100644 index 0000000000000000000000000000000000000000..22cabf52288cc9ef54555596edf0fe8f463ebe17 --- /dev/null +++ b/src/lib/components/icons/ChatPlus.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Check.svelte b/src/lib/components/icons/Check.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b173beacd65209f976d0ddbbf0d4ceb3b6744be7 --- /dev/null +++ b/src/lib/components/icons/Check.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/icons/CheckBox.svelte b/src/lib/components/icons/CheckBox.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ecb0033246dc764d79ff615740efc0661c1b0b33 --- /dev/null +++ b/src/lib/components/icons/CheckBox.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/CheckCircle.svelte b/src/lib/components/icons/CheckCircle.svelte new file mode 100644 index 0000000000000000000000000000000000000000..848fe8569b41f919f910ea7ac6b1f39143d807cc --- /dev/null +++ b/src/lib/components/icons/CheckCircle.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/ChevronDown.svelte b/src/lib/components/icons/ChevronDown.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c3d3fd447081bfb260a1560d3f02ebff5fd6e4e8 --- /dev/null +++ b/src/lib/components/icons/ChevronDown.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/icons/ChevronLeft.svelte b/src/lib/components/icons/ChevronLeft.svelte new file mode 100644 index 0000000000000000000000000000000000000000..05836bcd1a1e383d6774974bace59d5da6bceb56 --- /dev/null +++ b/src/lib/components/icons/ChevronLeft.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/icons/ChevronRight.svelte b/src/lib/components/icons/ChevronRight.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a70bcd679592d2c96c765ee3b15135ddd2549766 --- /dev/null +++ b/src/lib/components/icons/ChevronRight.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/icons/ChevronUp.svelte b/src/lib/components/icons/ChevronUp.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7b79e2731e12b3511a943c59b1a712fc0478d507 --- /dev/null +++ b/src/lib/components/icons/ChevronUp.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/icons/ChevronUpDown.svelte b/src/lib/components/icons/ChevronUpDown.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0f3f2a1c489f1e56d3ae589f2d4ae57f875402a8 --- /dev/null +++ b/src/lib/components/icons/ChevronUpDown.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Clip.svelte b/src/lib/components/icons/Clip.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d54f949a9fc77f5a43a61cee259af862060b8b59 --- /dev/null +++ b/src/lib/components/icons/Clip.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/icons/Clipboard.svelte b/src/lib/components/icons/Clipboard.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ad59cb12d6dd5f4841660042b06732f94414ddea --- /dev/null +++ b/src/lib/components/icons/Clipboard.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/ClockRotateRight.svelte b/src/lib/components/icons/ClockRotateRight.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7614a547ff2eb23e6028973d8552ecb51238c093 --- /dev/null +++ b/src/lib/components/icons/ClockRotateRight.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/Cloud.svelte b/src/lib/components/icons/Cloud.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f314c5fe0c46c2d026bab8628fa79254926c8e66 --- /dev/null +++ b/src/lib/components/icons/Cloud.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/icons/CloudArrowUp.svelte b/src/lib/components/icons/CloudArrowUp.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c9bcf53da93d12e23b87b5c4001680f1451ac35c --- /dev/null +++ b/src/lib/components/icons/CloudArrowUp.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Code.svelte b/src/lib/components/icons/Code.svelte new file mode 100644 index 0000000000000000000000000000000000000000..793094e2b4ee017a8ab7fc1ea8c358be10ef8632 --- /dev/null +++ b/src/lib/components/icons/Code.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/CodeBracket.svelte b/src/lib/components/icons/CodeBracket.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2b8afc237ed836520b33024fb18bfb63830a3364 --- /dev/null +++ b/src/lib/components/icons/CodeBracket.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Cog6.svelte b/src/lib/components/icons/Cog6.svelte new file mode 100644 index 0000000000000000000000000000000000000000..00441e01188c4197b57fa5abdbd7e65732bf6b80 --- /dev/null +++ b/src/lib/components/icons/Cog6.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/icons/Cog6Solid.svelte b/src/lib/components/icons/Cog6Solid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..06215a8f6945f9fc06a874d183a0b10035cb1097 --- /dev/null +++ b/src/lib/components/icons/Cog6Solid.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/icons/Collapse.svelte b/src/lib/components/icons/Collapse.svelte new file mode 100644 index 0000000000000000000000000000000000000000..bce46a9347bb293beb37d3e56f128ada4368210b --- /dev/null +++ b/src/lib/components/icons/Collapse.svelte @@ -0,0 +1,41 @@ + + + diff --git a/src/lib/components/icons/CommandLine.svelte b/src/lib/components/icons/CommandLine.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5bbbb715ddd9e7a691eeb04f0c308dfe158d9c10 --- /dev/null +++ b/src/lib/components/icons/CommandLine.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/CommandLineSolid.svelte b/src/lib/components/icons/CommandLineSolid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2cb08ba518ce7f2432bff7477853de4633047f32 --- /dev/null +++ b/src/lib/components/icons/CommandLineSolid.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/icons/Component.svelte b/src/lib/components/icons/Component.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0fe8bd97f3439f9bc6f148dd99a7629d33cabe1a --- /dev/null +++ b/src/lib/components/icons/Component.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/Computer.svelte b/src/lib/components/icons/Computer.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3baf42803fdb0f527ef0e214e8878f1692ce51ea --- /dev/null +++ b/src/lib/components/icons/Computer.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/icons/Cube.svelte b/src/lib/components/icons/Cube.svelte new file mode 100644 index 0000000000000000000000000000000000000000..99b3e89c9f58ed4bf7c109d0f80ee46cd3b3bbda --- /dev/null +++ b/src/lib/components/icons/Cube.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/CursorArrowRays.svelte b/src/lib/components/icons/CursorArrowRays.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7641b10a6612374709b66959ac63a578884bfd61 --- /dev/null +++ b/src/lib/components/icons/CursorArrowRays.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Database.svelte b/src/lib/components/icons/Database.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9f44eab54341bd5b25ad4d5c8f31f347fc750e7c --- /dev/null +++ b/src/lib/components/icons/Database.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/icons/DatabaseSettings.svelte b/src/lib/components/icons/DatabaseSettings.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c6e7ca56c7c2c23c561bdfff375665f50e6e8885 --- /dev/null +++ b/src/lib/components/icons/DatabaseSettings.svelte @@ -0,0 +1,33 @@ + + + diff --git a/src/lib/components/icons/Document.svelte b/src/lib/components/icons/Document.svelte new file mode 100644 index 0000000000000000000000000000000000000000..13745c74db466700fffe011e9f99addeac4797a1 --- /dev/null +++ b/src/lib/components/icons/Document.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/DocumentArrowDown.svelte b/src/lib/components/icons/DocumentArrowDown.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1c2aa17edb144ca44c6e79b9f361f163d83d0ab0 --- /dev/null +++ b/src/lib/components/icons/DocumentArrowDown.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/DocumentArrowUp.svelte b/src/lib/components/icons/DocumentArrowUp.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ee1c424264116833e41371333f7a02889b75976b --- /dev/null +++ b/src/lib/components/icons/DocumentArrowUp.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/DocumentArrowUpSolid.svelte b/src/lib/components/icons/DocumentArrowUpSolid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..94fabc272cb7afbb4632e6f6e576b77261272f1d --- /dev/null +++ b/src/lib/components/icons/DocumentArrowUpSolid.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/DocumentChartBar.svelte b/src/lib/components/icons/DocumentChartBar.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a25367682c92ee0673fdf63a2745cbb3fb555c60 --- /dev/null +++ b/src/lib/components/icons/DocumentChartBar.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/icons/DocumentCheck.svelte b/src/lib/components/icons/DocumentCheck.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5b212251c45ae898ec365a154eae32f2e71fe1a6 --- /dev/null +++ b/src/lib/components/icons/DocumentCheck.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/DocumentDuplicate.svelte b/src/lib/components/icons/DocumentDuplicate.svelte new file mode 100644 index 0000000000000000000000000000000000000000..cea301511929b9cbd0aef51fd784f9b121f7c40b --- /dev/null +++ b/src/lib/components/icons/DocumentDuplicate.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/DocumentPage.svelte b/src/lib/components/icons/DocumentPage.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2cf49de6a807b3c8f2a5aff192ad8ad60269e8c8 --- /dev/null +++ b/src/lib/components/icons/DocumentPage.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/lib/components/icons/Download.svelte b/src/lib/components/icons/Download.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6ae05879dcec0f1b3bc6575d5d5e014bcc515fda --- /dev/null +++ b/src/lib/components/icons/Download.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/icons/EditPencil.svelte b/src/lib/components/icons/EditPencil.svelte new file mode 100644 index 0000000000000000000000000000000000000000..970e2816ac5b94a892122c7515b7730df182fb02 --- /dev/null +++ b/src/lib/components/icons/EditPencil.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/EllipsisHorizontal.svelte b/src/lib/components/icons/EllipsisHorizontal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..93ebb4b7879ddc8641831699b665843e41f6e332 --- /dev/null +++ b/src/lib/components/icons/EllipsisHorizontal.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/EllipsisVertical.svelte b/src/lib/components/icons/EllipsisVertical.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4e223c9c99efee94a36e5406b4f87d95024f6282 --- /dev/null +++ b/src/lib/components/icons/EllipsisVertical.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Expand.svelte b/src/lib/components/icons/Expand.svelte new file mode 100644 index 0000000000000000000000000000000000000000..bedddfbb93962001be0529123b263ed7e60fb6a1 --- /dev/null +++ b/src/lib/components/icons/Expand.svelte @@ -0,0 +1,41 @@ + + + diff --git a/src/lib/components/icons/Eye.svelte b/src/lib/components/icons/Eye.svelte new file mode 100644 index 0000000000000000000000000000000000000000..66c2b2bfaab57e06392d400e225cedc81f1f0d4f --- /dev/null +++ b/src/lib/components/icons/Eye.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/icons/EyeSlash.svelte b/src/lib/components/icons/EyeSlash.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6da89898f8b8b910d9880821ddedf75459204c09 --- /dev/null +++ b/src/lib/components/icons/EyeSlash.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Face.svelte b/src/lib/components/icons/Face.svelte new file mode 100644 index 0000000000000000000000000000000000000000..32d74058c75d7053377110d41bc0f086fc6b17c6 --- /dev/null +++ b/src/lib/components/icons/Face.svelte @@ -0,0 +1,28 @@ + + + diff --git a/src/lib/components/icons/FaceId.svelte b/src/lib/components/icons/FaceId.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c0a9f91d8be8e8002393fd8d38aceae010dbef71 --- /dev/null +++ b/src/lib/components/icons/FaceId.svelte @@ -0,0 +1,36 @@ + + + diff --git a/src/lib/components/icons/FaceSmile.svelte b/src/lib/components/icons/FaceSmile.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9c1b511c427b9392ed3a604054a16f38754c95db --- /dev/null +++ b/src/lib/components/icons/FaceSmile.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/FilePlusAlt.svelte b/src/lib/components/icons/FilePlusAlt.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6a440f200bbcd1c7315468ecf904c7cdfeccef74 --- /dev/null +++ b/src/lib/components/icons/FilePlusAlt.svelte @@ -0,0 +1,28 @@ + + + + + + + diff --git a/src/lib/components/icons/FloppyDisk.svelte b/src/lib/components/icons/FloppyDisk.svelte new file mode 100644 index 0000000000000000000000000000000000000000..bcb481e826541f74ddba12ad333c4dc497ef6312 --- /dev/null +++ b/src/lib/components/icons/FloppyDisk.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Folder.svelte b/src/lib/components/icons/Folder.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9bd279f2264280d417deca16514d008113aa1066 --- /dev/null +++ b/src/lib/components/icons/Folder.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/FolderOpen.svelte b/src/lib/components/icons/FolderOpen.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d680138819490f12e200e94e830c70bc5a518cba --- /dev/null +++ b/src/lib/components/icons/FolderOpen.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/GarbageBin.svelte b/src/lib/components/icons/GarbageBin.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ec1aa9055e632da5809584288cb99f4a81c7212c --- /dev/null +++ b/src/lib/components/icons/GarbageBin.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Github.svelte b/src/lib/components/icons/Github.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6d8c60d6bb6c0173e8fa83633656446b0a2e5e10 --- /dev/null +++ b/src/lib/components/icons/Github.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/icons/Glasses.svelte b/src/lib/components/icons/Glasses.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3d05cf70a266d0ad843c84e9926682ad2f209d80 --- /dev/null +++ b/src/lib/components/icons/Glasses.svelte @@ -0,0 +1,45 @@ + + + diff --git a/src/lib/components/icons/GlobeAlt.svelte b/src/lib/components/icons/GlobeAlt.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ac2871b5db7f852caa84b483f75ec5362285d2dd --- /dev/null +++ b/src/lib/components/icons/GlobeAlt.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/GlobeAltSolid.svelte b/src/lib/components/icons/GlobeAltSolid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7cc7e55171fc4fc62c059b54b6a190876f29dfb3 --- /dev/null +++ b/src/lib/components/icons/GlobeAltSolid.svelte @@ -0,0 +1,15 @@ + + + diff --git a/src/lib/components/icons/Grid.svelte b/src/lib/components/icons/Grid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..60b6db05e1482106d75fa4679f3d0d47ba535dce --- /dev/null +++ b/src/lib/components/icons/Grid.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/H1.svelte b/src/lib/components/icons/H1.svelte new file mode 100644 index 0000000000000000000000000000000000000000..75336f321c3d4ee9f00040867b82df6c36df5033 --- /dev/null +++ b/src/lib/components/icons/H1.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/H2.svelte b/src/lib/components/icons/H2.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b4d341726a5ed05898df277e9dda94f63625fb38 --- /dev/null +++ b/src/lib/components/icons/H2.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/H3.svelte b/src/lib/components/icons/H3.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a2fc0a2475611767ed56459a4ee2d58f2f6feda0 --- /dev/null +++ b/src/lib/components/icons/H3.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Hashtag.svelte b/src/lib/components/icons/Hashtag.svelte new file mode 100644 index 0000000000000000000000000000000000000000..08d229954e7d816ebae871f062219aa9c1ea05ff --- /dev/null +++ b/src/lib/components/icons/Hashtag.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/icons/Headphone.svelte b/src/lib/components/icons/Headphone.svelte new file mode 100644 index 0000000000000000000000000000000000000000..10902a7cced4fb8dfae3cb2e3eecd802529c0b30 --- /dev/null +++ b/src/lib/components/icons/Headphone.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Heart.svelte b/src/lib/components/icons/Heart.svelte new file mode 100644 index 0000000000000000000000000000000000000000..94e95325ece7bf228ad0b31ee77f2dbf355f69cf --- /dev/null +++ b/src/lib/components/icons/Heart.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Home.svelte b/src/lib/components/icons/Home.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ff93fd1ad2b42df94f06151af8c6073f42eb39a8 --- /dev/null +++ b/src/lib/components/icons/Home.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/Info.svelte b/src/lib/components/icons/Info.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c2c756f497b316e176a27081d89452424a8f5cac --- /dev/null +++ b/src/lib/components/icons/Info.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/InfoCircle.svelte b/src/lib/components/icons/InfoCircle.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3d748c9c5c316a8f798b4f2a1d4970243493f77b --- /dev/null +++ b/src/lib/components/icons/InfoCircle.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/Italic.svelte b/src/lib/components/icons/Italic.svelte new file mode 100644 index 0000000000000000000000000000000000000000..194ba1d42c53dfd7db9fb9536ff7cd46db29823c --- /dev/null +++ b/src/lib/components/icons/Italic.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Keyboard.svelte b/src/lib/components/icons/Keyboard.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fdb1d06f454701a2c8dd774fecd629654ad19551 --- /dev/null +++ b/src/lib/components/icons/Keyboard.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/icons/KeyframePlus.svelte b/src/lib/components/icons/KeyframePlus.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ca4e0d62ed90f23ce5c9ceb6edf636b932b8191b --- /dev/null +++ b/src/lib/components/icons/KeyframePlus.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/icons/Keyframes.svelte b/src/lib/components/icons/Keyframes.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b1bb6f54e925afec098543ecf8344a0a9226c8de --- /dev/null +++ b/src/lib/components/icons/Keyframes.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/icons/Knobs.svelte b/src/lib/components/icons/Knobs.svelte new file mode 100644 index 0000000000000000000000000000000000000000..67b29e2688fda36f2e1a668a72baae6b79d74160 --- /dev/null +++ b/src/lib/components/icons/Knobs.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Label.svelte b/src/lib/components/icons/Label.svelte new file mode 100644 index 0000000000000000000000000000000000000000..07f7e26883195335479d17e5c9fa48f76189fa09 --- /dev/null +++ b/src/lib/components/icons/Label.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/icons/Lifebuoy.svelte b/src/lib/components/icons/Lifebuoy.svelte new file mode 100644 index 0000000000000000000000000000000000000000..8129d4757fbdf42f2aa28e569344cf684f4b91c3 --- /dev/null +++ b/src/lib/components/icons/Lifebuoy.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/LightBulb.svelte b/src/lib/components/icons/LightBulb.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6a296065efd240e34cab828071f94316c8135ecf --- /dev/null +++ b/src/lib/components/icons/LightBulb.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/LineSpace.svelte b/src/lib/components/icons/LineSpace.svelte new file mode 100644 index 0000000000000000000000000000000000000000..bc4c5d0c7b2e1fe0540d9c8f6d8764a2b80f5ca7 --- /dev/null +++ b/src/lib/components/icons/LineSpace.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/LineSpaceSmaller.svelte b/src/lib/components/icons/LineSpaceSmaller.svelte new file mode 100644 index 0000000000000000000000000000000000000000..adef3b361ec8a5b038d949b314f97fe2fafb7df2 --- /dev/null +++ b/src/lib/components/icons/LineSpaceSmaller.svelte @@ -0,0 +1,25 @@ + + + diff --git a/src/lib/components/icons/Link.svelte b/src/lib/components/icons/Link.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1809cc897ebcc824873837248137b7a366e7854c --- /dev/null +++ b/src/lib/components/icons/Link.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/LinkSlash.svelte b/src/lib/components/icons/LinkSlash.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f392a997bd3115adf708ff88c5cd40b30138c095 --- /dev/null +++ b/src/lib/components/icons/LinkSlash.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/lib/components/icons/ListBullet.svelte b/src/lib/components/icons/ListBullet.svelte new file mode 100644 index 0000000000000000000000000000000000000000..41fbeb60bee0c987f09711ad8ab5d67fb0ac701e --- /dev/null +++ b/src/lib/components/icons/ListBullet.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Lock.svelte b/src/lib/components/icons/Lock.svelte new file mode 100644 index 0000000000000000000000000000000000000000..bd0be308d100d3822c1722ae4f00d76c39a707d1 --- /dev/null +++ b/src/lib/components/icons/Lock.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/icons/LockClosed.svelte b/src/lib/components/icons/LockClosed.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b79104760579be89aee58a9d725f8688b6565cac --- /dev/null +++ b/src/lib/components/icons/LockClosed.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Map.svelte b/src/lib/components/icons/Map.svelte new file mode 100644 index 0000000000000000000000000000000000000000..79429f9aea78afc78c9f7ffe975ffc739d50fb4a --- /dev/null +++ b/src/lib/components/icons/Map.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/MenuLines.svelte b/src/lib/components/icons/MenuLines.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c1eac44c880a9432ef9205d93252099299dd65d6 --- /dev/null +++ b/src/lib/components/icons/MenuLines.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Merge.svelte b/src/lib/components/icons/Merge.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1746b7c0816c56552edf887dbc5cc36eb3069c5d --- /dev/null +++ b/src/lib/components/icons/Merge.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Mic.svelte b/src/lib/components/icons/Mic.svelte new file mode 100644 index 0000000000000000000000000000000000000000..df3c550451bfe34feee00b4a13d947a83415b567 --- /dev/null +++ b/src/lib/components/icons/Mic.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/MicSolid.svelte b/src/lib/components/icons/MicSolid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..462e64a769dab21754ae7cd21e623cf6ff58e237 --- /dev/null +++ b/src/lib/components/icons/MicSolid.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/icons/Minus.svelte b/src/lib/components/icons/Minus.svelte new file mode 100644 index 0000000000000000000000000000000000000000..710b32fde3427a982ead7b64a824ac15789e550e --- /dev/null +++ b/src/lib/components/icons/Minus.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/icons/NewFolderAlt.svelte b/src/lib/components/icons/NewFolderAlt.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9fabe868d1204906ff42da3707244c40fd889cf8 --- /dev/null +++ b/src/lib/components/icons/NewFolderAlt.svelte @@ -0,0 +1,24 @@ + + + + + + + diff --git a/src/lib/components/icons/Note.svelte b/src/lib/components/icons/Note.svelte new file mode 100644 index 0000000000000000000000000000000000000000..8ee8da705152d3f32362af966a0c21509a8753d0 --- /dev/null +++ b/src/lib/components/icons/Note.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/NumberedList.svelte b/src/lib/components/icons/NumberedList.svelte new file mode 100644 index 0000000000000000000000000000000000000000..8c0c1eb103aef684b7fd9d096214c3b0713f5bdc --- /dev/null +++ b/src/lib/components/icons/NumberedList.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/PageEdit.svelte b/src/lib/components/icons/PageEdit.svelte new file mode 100644 index 0000000000000000000000000000000000000000..cbba2091e3b5576e6174421310f38f1108827d18 --- /dev/null +++ b/src/lib/components/icons/PageEdit.svelte @@ -0,0 +1,28 @@ + + + diff --git a/src/lib/components/icons/PagePlus.svelte b/src/lib/components/icons/PagePlus.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c69816dd8e50ea1c25e9fde79bf2a47d3c792b3a --- /dev/null +++ b/src/lib/components/icons/PagePlus.svelte @@ -0,0 +1,24 @@ + + + diff --git a/src/lib/components/icons/PenAlt.svelte b/src/lib/components/icons/PenAlt.svelte new file mode 100644 index 0000000000000000000000000000000000000000..33798ed65772cf4344d15564626dc98435c148ec --- /dev/null +++ b/src/lib/components/icons/PenAlt.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/src/lib/components/icons/Pencil.svelte b/src/lib/components/icons/Pencil.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b62415e711eaa05156a8d14a75570eb92ae83044 --- /dev/null +++ b/src/lib/components/icons/Pencil.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/PencilSolid.svelte b/src/lib/components/icons/PencilSolid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..711e1cc2042f38b7b51097b9a8840b17e3e80990 --- /dev/null +++ b/src/lib/components/icons/PencilSolid.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/icons/PencilSquare.svelte b/src/lib/components/icons/PencilSquare.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6102fa6ac0df5c5ba612bc29b6409a75b0cebbae --- /dev/null +++ b/src/lib/components/icons/PencilSquare.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/PeopleTag.svelte b/src/lib/components/icons/PeopleTag.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6544591f0a31c84f8b24961d54623bacdb75fcf0 --- /dev/null +++ b/src/lib/components/icons/PeopleTag.svelte @@ -0,0 +1,31 @@ + + + diff --git a/src/lib/components/icons/Photo.svelte b/src/lib/components/icons/Photo.svelte new file mode 100644 index 0000000000000000000000000000000000000000..26296db7c6a65243d75bfed1b1506e9420dc4eb4 --- /dev/null +++ b/src/lib/components/icons/Photo.svelte @@ -0,0 +1,28 @@ + + + diff --git a/src/lib/components/icons/PhotoSolid.svelte b/src/lib/components/icons/PhotoSolid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ba2ef8ef769e0288e8424c56c33e5527ea79aa87 --- /dev/null +++ b/src/lib/components/icons/PhotoSolid.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/icons/Pin.svelte b/src/lib/components/icons/Pin.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d62c9049db87b1f7403a0aa34b74123d5c9aa068 --- /dev/null +++ b/src/lib/components/icons/Pin.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/icons/PinSlash.svelte b/src/lib/components/icons/PinSlash.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6a90da3e87aba760d3cdd8fb7e3944de8b7a7573 --- /dev/null +++ b/src/lib/components/icons/PinSlash.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/icons/Plus.svelte b/src/lib/components/icons/Plus.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f2c5fb443852c8d729ec9cae1321bd9ff1b78ed9 --- /dev/null +++ b/src/lib/components/icons/Plus.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/icons/PlusAlt.svelte b/src/lib/components/icons/PlusAlt.svelte new file mode 100644 index 0000000000000000000000000000000000000000..dfa63644f89bd0d20d24af2fbf8bb661f09222fa --- /dev/null +++ b/src/lib/components/icons/PlusAlt.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/icons/QuestionMarkCircle.svelte b/src/lib/components/icons/QuestionMarkCircle.svelte new file mode 100644 index 0000000000000000000000000000000000000000..abac715c9882d1ecebabc3915d8cc9b783463e24 --- /dev/null +++ b/src/lib/components/icons/QuestionMarkCircle.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/QueueList.svelte b/src/lib/components/icons/QueueList.svelte new file mode 100644 index 0000000000000000000000000000000000000000..26d892aea1a2a434aea50443d4c080672c8ce8db --- /dev/null +++ b/src/lib/components/icons/QueueList.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Refresh.svelte b/src/lib/components/icons/Refresh.svelte new file mode 100644 index 0000000000000000000000000000000000000000..46eb9ac28bf75764946cfb0827a4a77a6c16d24d --- /dev/null +++ b/src/lib/components/icons/Refresh.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/Reset.svelte b/src/lib/components/icons/Reset.svelte new file mode 100644 index 0000000000000000000000000000000000000000..909c468ce969f4c61be4f3457e416d6c3e6ac075 --- /dev/null +++ b/src/lib/components/icons/Reset.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Search.svelte b/src/lib/components/icons/Search.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2953f00990895012d774d7763a9eac04a9d8dd5b --- /dev/null +++ b/src/lib/components/icons/Search.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Settings.svelte b/src/lib/components/icons/Settings.svelte new file mode 100644 index 0000000000000000000000000000000000000000..40fed35080ab16079aa258aed27a16faa6651d05 --- /dev/null +++ b/src/lib/components/icons/Settings.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/icons/SettingsAlt.svelte b/src/lib/components/icons/SettingsAlt.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fc38afc3f79a51dfb45ad938728163804ec75a0f --- /dev/null +++ b/src/lib/components/icons/SettingsAlt.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/Share.svelte b/src/lib/components/icons/Share.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f9b7efc21331fefb1cabcc0d8026462c6b48645f --- /dev/null +++ b/src/lib/components/icons/Share.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/Sidebar.svelte b/src/lib/components/icons/Sidebar.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e158cf3cf43fa200deb96234e809679f0b59dbbe --- /dev/null +++ b/src/lib/components/icons/Sidebar.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/lib/components/icons/SignOut.svelte b/src/lib/components/icons/SignOut.svelte new file mode 100644 index 0000000000000000000000000000000000000000..10a4c77d70af0cbde82b9720d925060ea0129205 --- /dev/null +++ b/src/lib/components/icons/SignOut.svelte @@ -0,0 +1,18 @@ + + + + + + diff --git a/src/lib/components/icons/SoundHigh.svelte b/src/lib/components/icons/SoundHigh.svelte new file mode 100644 index 0000000000000000000000000000000000000000..11cb329e636bd0af46fed643fe2ef58c865b59bf --- /dev/null +++ b/src/lib/components/icons/SoundHigh.svelte @@ -0,0 +1,25 @@ + + + diff --git a/src/lib/components/icons/Sparkles.svelte b/src/lib/components/icons/Sparkles.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5bbdaf8d8909d0a915daa69f4772c1d3fd19b60b --- /dev/null +++ b/src/lib/components/icons/Sparkles.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/SparklesSolid.svelte b/src/lib/components/icons/SparklesSolid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..44949504e097cbb9c0c33a67f312335bc6b13098 --- /dev/null +++ b/src/lib/components/icons/SparklesSolid.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/icons/Star.svelte b/src/lib/components/icons/Star.svelte new file mode 100644 index 0000000000000000000000000000000000000000..33578d1574414cf68efe67b6d281942f55665ee5 --- /dev/null +++ b/src/lib/components/icons/Star.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Strikethrough.svelte b/src/lib/components/icons/Strikethrough.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d01979d407e8b024917f359726f6397812dad345 --- /dev/null +++ b/src/lib/components/icons/Strikethrough.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Tag.svelte b/src/lib/components/icons/Tag.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3fc3446a15bfc06d7e9fd5f74531c8aeaba6d1c7 --- /dev/null +++ b/src/lib/components/icons/Tag.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/TaskList.svelte b/src/lib/components/icons/TaskList.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c944afd7fed812262447d703cd658e94970ab3d0 --- /dev/null +++ b/src/lib/components/icons/TaskList.svelte @@ -0,0 +1,54 @@ + + + + + + + + + + diff --git a/src/lib/components/icons/Terminal.svelte b/src/lib/components/icons/Terminal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..be4c5d02d9e7d35849e05c542b7b15f7001c16cf --- /dev/null +++ b/src/lib/components/icons/Terminal.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/Underline.svelte b/src/lib/components/icons/Underline.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e97f6224514b84dce12293d561bb7d3e088c18d1 --- /dev/null +++ b/src/lib/components/icons/Underline.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/Union.svelte b/src/lib/components/icons/Union.svelte new file mode 100644 index 0000000000000000000000000000000000000000..faede57b9cbd638da7fc3f496f06f99a855881f5 --- /dev/null +++ b/src/lib/components/icons/Union.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/User.svelte b/src/lib/components/icons/User.svelte new file mode 100644 index 0000000000000000000000000000000000000000..410bd4d97a8b47a8fd4324436ff2490f007433f6 --- /dev/null +++ b/src/lib/components/icons/User.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/icons/UserAlt.svelte b/src/lib/components/icons/UserAlt.svelte new file mode 100644 index 0000000000000000000000000000000000000000..865b90452de9bdc9200b36e5397d3fe2a6b764de --- /dev/null +++ b/src/lib/components/icons/UserAlt.svelte @@ -0,0 +1,24 @@ + + + diff --git a/src/lib/components/icons/UserBadgeCheck.svelte b/src/lib/components/icons/UserBadgeCheck.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a704caa10f4271e558ddb952fe3fa8c958313841 --- /dev/null +++ b/src/lib/components/icons/UserBadgeCheck.svelte @@ -0,0 +1,26 @@ + + + diff --git a/src/lib/components/icons/UserCircle.svelte b/src/lib/components/icons/UserCircle.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fdc7594d53b548e42ac29933855dd9828c587bc5 --- /dev/null +++ b/src/lib/components/icons/UserCircle.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/lib/components/icons/UserCircleSolid.svelte b/src/lib/components/icons/UserCircleSolid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..611e87d9e6990b662bfa9ebbf513fff333b3abe8 --- /dev/null +++ b/src/lib/components/icons/UserCircleSolid.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/icons/UserGroup.svelte b/src/lib/components/icons/UserGroup.svelte new file mode 100644 index 0000000000000000000000000000000000000000..497c2f479f6310c698e5437cf3e8b05200cd27e0 --- /dev/null +++ b/src/lib/components/icons/UserGroup.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/UserPlusSolid.svelte b/src/lib/components/icons/UserPlusSolid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0aaddbf8e86ba98047712d791444b992f11c02c3 --- /dev/null +++ b/src/lib/components/icons/UserPlusSolid.svelte @@ -0,0 +1,15 @@ + + + diff --git a/src/lib/components/icons/Users.svelte b/src/lib/components/icons/Users.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b34af4382b71db5f726abd730dd86413d03b66a1 --- /dev/null +++ b/src/lib/components/icons/Users.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/icons/UsersSolid.svelte b/src/lib/components/icons/UsersSolid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0b50357e4de8b34e765ec2fbe0e076c064622bea --- /dev/null +++ b/src/lib/components/icons/UsersSolid.svelte @@ -0,0 +1,15 @@ + + + diff --git a/src/lib/components/icons/Voice.svelte b/src/lib/components/icons/Voice.svelte new file mode 100644 index 0000000000000000000000000000000000000000..43ee47aa5644d5277494b982e0d21b1581ac4151 --- /dev/null +++ b/src/lib/components/icons/Voice.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/Wrench.svelte b/src/lib/components/icons/Wrench.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d761dd9c4be696c473a87834e6b26c69b615598e --- /dev/null +++ b/src/lib/components/icons/Wrench.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/icons/WrenchAlt.svelte b/src/lib/components/icons/WrenchAlt.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5328f600b6f233dc08b8ca8be58f312c5fb914c6 --- /dev/null +++ b/src/lib/components/icons/WrenchAlt.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/WrenchSolid.svelte b/src/lib/components/icons/WrenchSolid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ac1ad3e18db6a0d4bc1af791875c9c68e2590e9d --- /dev/null +++ b/src/lib/components/icons/WrenchSolid.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/icons/XMark.svelte b/src/lib/components/icons/XMark.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1174d674cbd5fd1c709714759fc0f7a4ac9b371e --- /dev/null +++ b/src/lib/components/icons/XMark.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/icons/Youtube.svelte b/src/lib/components/icons/Youtube.svelte new file mode 100644 index 0000000000000000000000000000000000000000..de0948bd5c78745186975815ba63d7bb108b8d6d --- /dev/null +++ b/src/lib/components/icons/Youtube.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/icons/ZoomReset.svelte b/src/lib/components/icons/ZoomReset.svelte new file mode 100644 index 0000000000000000000000000000000000000000..20d946f83fba3d19766223b83bc57759a0128b20 --- /dev/null +++ b/src/lib/components/icons/ZoomReset.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/lib/components/layout/ArchivedChatsModal.svelte b/src/lib/components/layout/ArchivedChatsModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..31e322a10531cebd84dba70bbb9ea8f495209803 --- /dev/null +++ b/src/lib/components/layout/ArchivedChatsModal.svelte @@ -0,0 +1,195 @@ + + + { + unarchiveAllHandler(); + }} +/> + + { + init(); + }} + onDelete={(id) => { + onDelete(id); + }} + loadHandler={loadMoreChats} + {unarchiveHandler} +> +
      +
      + + + +
      +
      +
      diff --git a/src/lib/components/layout/ChatsModal.svelte b/src/lib/components/layout/ChatsModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b56ed780d37506c0941cb5c58efb9ba9c4d1e4a3 --- /dev/null +++ b/src/lib/components/layout/ChatsModal.svelte @@ -0,0 +1,528 @@ + + + { + if (selectedChatId) { + deleteHandler(selectedChatId); + selectedChatId = null; + } + }} +/> + + +
      +
      +
      {title}
      + +
      + +
      + {#if showSearch} +
      +
      +
      + + + +
      + + + {#if query} +
      + +
      + {/if} +
      +
      + {/if} + +
      + {#if chatList} +
      + {#if chatList.length > 0} +
      + {#if showUserInfo} +
      + {$i18n.t('User')} +
      + {/if} + + +
      + {/if} +
      + {#if chatList.length === 0} +
      + {$i18n.t('No results found')} +
      + {/if} + + {#each chatList as chat, idx (chat.id)} + {#if (idx === 0 || (idx > 0 && chat.time_range !== chatList[idx - 1].time_range)) && chat?.time_range} +
      + {$i18n.t(chat.time_range)} + +
      + {/if} + +
      + {#if showUserInfo && chat.user_id} +
      + {chat.user_name + {chat.user_name || 'Unknown'} +
      + {/if} + (show = false)} + > +
      + {chat?.title} +
      +
      + +
      + + + {#if !readOnly} +
      + {#if unarchiveHandler} + + + + {/if} + + {#if unshareHandler && chat.share_id} + + + + {/if} + + + + +
      + {/if} +
      +
      + {/each} + + {#if !allChatsLoaded && loadHandler} + { + if (!chatListLoading) { + loadHandler(); + } + }} + > +
      + +
      {$i18n.t('Loading...')}
      +
      +
      + {/if} +
      + + {#if query === ''} + + {/if} +
      + {:else} +
      + +
      + {/if} + + +
      +
      +
      +
      diff --git a/src/lib/components/layout/FilesModal.svelte b/src/lib/components/layout/FilesModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..499ea48a03e3c84e57d9d2c26035de4739cfd3ea --- /dev/null +++ b/src/lib/components/layout/FilesModal.svelte @@ -0,0 +1,388 @@ + + + { + if (selectedFileId) { + deleteHandler(selectedFileId); + selectedFileId = null; + } + }} +/> + + + + +
      +
      +
      {$i18n.t('Files')}
      + +
      + +
      + +
      +
      +
      + + + +
      + + + {#if query} +
      + +
      + {/if} +
      +
      + + +
      + {#if files !== null} +
      + {#if files.length > 0} +
      + + +
      + {/if} + +
      + {#if files.length === 0} +
      + {$i18n.t('No files found')} +
      + {/if} + + {#each files as file (file.id)} +
      openFileViewer(file)} + > +
      +
      {file.filename}
      +
      + {formatFileSize(file.meta?.size ?? 0)} +
      +
      + +
      + + +
      + + + +
      +
      +
      + {/each} + + {#if !allFilesLoaded} + { + if (!filesLoading) { + loadMoreFiles(); + } + }} + > +
      + +
      {$i18n.t('Loading...')}
      +
      +
      + {/if} +
      +
      + {:else} +
      + +
      + {/if} +
      +
      +
      +
      diff --git a/src/lib/components/layout/Navbar/Menu.svelte b/src/lib/components/layout/Navbar/Menu.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2abc0b0a6e5b7b1cd7341adc29112a9cea7c8b9b --- /dev/null +++ b/src/lib/components/layout/Navbar/Menu.svelte @@ -0,0 +1,457 @@ + + +{#if showFullMessages} + +{/if} + + { + if (state === false) { + onClose(); + } + }} + align="end" + sideOffset={8} +> + + +
      +
      + + + {#if ($artifactContents ?? []).length > 0} + + +
      + {/if} + + {#if !$temporaryChatEnabled && ($user?.role === 'admin' || ($user.permissions?.chat?.share ?? true))} + + {/if} + + + + {#if $user?.role === 'admin' || ($user.permissions?.chat?.export ?? true)} + + {/if} + + + + + + + + {#if !$temporaryChatEnabled && chat?.id} +
      + + {#if $folders.length > 0} + + + {#each $folders.sort((a, b) => b.updated_at - a.updated_at) as folder} + {#if folder?.id} + + {/if} + {/each} + + {/if} + + + +
      + +
      + +
      + {/if} +
      +
      +
      diff --git a/src/lib/components/layout/Overlay/AccountPending.svelte b/src/lib/components/layout/Overlay/AccountPending.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ae3b7e896b90022773ef132fc7d73bb160fbe574 --- /dev/null +++ b/src/lib/components/layout/Overlay/AccountPending.svelte @@ -0,0 +1,81 @@ + + +
      +
      +
      +
      +
      + {#if ($config?.ui?.pending_user_overlay_title ?? '').trim() !== ''} + {$config.ui.pending_user_overlay_title} + {:else} + {$i18n.t('Account Activation Pending')}
      + {$i18n.t('Contact Admin for WebUI Access')} + {/if} +
      + +
      + {#if ($config?.ui?.pending_user_overlay_content ?? '').trim() !== ''} + {@html DOMPurify.sanitize( + marked.parse(($config?.ui?.pending_user_overlay_content ?? '').replace(/\n/g, '
      ')) + )} + {:else} + {$i18n.t('Your account status is currently pending activation.')}{'\n'}{$i18n.t( + 'To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.' + )} + {/if} +
      + + {#if adminDetails} +
      +
      {$i18n.t('Admin')}: {adminDetails.name} ({adminDetails.email})
      +
      + {/if} + +
      + + + +
      +
      +
      +
      +
      diff --git a/src/lib/components/layout/SearchModal.svelte b/src/lib/components/layout/SearchModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5617de7c225bbe6551b115c680794f132aeb9ba9 --- /dev/null +++ b/src/lib/components/layout/SearchModal.svelte @@ -0,0 +1,458 @@ + + + +
      +
      + { + selectedIdx = null; + messages = null; + }} + onKeydown={(e) => { + console.log('e', e); + + if (e.code === 'Enter' && (chatList ?? []).length > 0) { + const item = document.querySelector(`[data-arrow-selected="true"]`); + if (item) { + item?.click(); + } + + show = false; + return; + } else if (e.code === 'ArrowDown') { + selectedIdx = Math.min(selectedIdx + 1, (chatList ?? []).length - 1 + actions.length); + } else if (e.code === 'ArrowUp') { + selectedIdx = Math.max(selectedIdx - 1, 0); + } else { + selectedIdx = 0; + } + + const item = document.querySelector(`[data-arrow-selected="true"]`); + item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' }); + }} + /> +
      + + + +
      +
      +
      + {$i18n.t('Actions')} +
      + + {#each actions as action, idx (action.label)} + + {/each} + + {#if chatList} +
      + + {#if chatList.length === 0} +
      + {$i18n.t('No results found')} +
      + {/if} + + {#each chatList as chat, idx (chat.id)} + {#if idx === 0 || (idx > 0 && chat.time_range !== chatList[idx - 1].time_range)} +
      + {$i18n.t(chat.time_range)} + +
      + {/if} + + { + selectedIdx = idx + actions.length; + }} + on:click={async () => { + await goto(`/c/${chat.id}`); + show = false; + onClose(); + }} + > +
      +
      + {chat?.title} +
      +
      + +
      + {$i18n.t( + dayjs(chat?.updated_at * 1000).calendar(null, { + sameDay: '[Today]', + nextDay: '[Tomorrow]', + nextWeek: 'dddd', + lastDay: '[Yesterday]', + lastWeek: '[Last] dddd', + sameElse: 'L' // use localized format, otherwise dayjs.calendar() defaults to DD/MM/YYYY + }) + )} +
      +
      + {/each} + + {#if !allChatsLoaded} + { + if (!chatListLoading) { + loadMoreChats(); + } + }} + > +
      + +
      {$i18n.t('Loading...')}
      +
      +
      + {/if} + {:else} +
      + +
      + {/if} +
      + +
      +
      +
      diff --git a/src/lib/components/layout/SharedChatsModal.svelte b/src/lib/components/layout/SharedChatsModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..55c5d6cb517be90ad4e02fea0650b480af52ee8b --- /dev/null +++ b/src/lib/components/layout/SharedChatsModal.svelte @@ -0,0 +1,126 @@ + + + { + onUpdate(); + init(); + }} + loadHandler={loadMoreChats} + {unshareHandler} +/> diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fcea714f020242c553f4685ff8187c2d85d7e69d --- /dev/null +++ b/src/lib/components/layout/Sidebar.svelte @@ -0,0 +1,1654 @@ + + + { + await initChatList(); + }} + onDelete={(id) => { + if ($chatId === id) { + goto('/'); + chatId.set(''); + } + }} +/> + + { + let { type, name, is_private, access_grants, group_ids, user_ids } = payload ?? {}; + name = name?.trim(); + + if (type === 'dm') { + if (!user_ids || user_ids.length === 0) { + toast.error($i18n.t('Please select at least one user for Direct Message channel.')); + return; + } + } else { + if (!name) { + toast.error($i18n.t('Channel name cannot be empty.')); + return; + } + } + + const res = await createNewChannel(localStorage.token, { + type: type, + name: name, + is_private: is_private, + access_grants: access_grants, + group_ids: group_ids, + user_ids: user_ids + }).catch((error) => { + toast.error(`${error}`); + return null; + }); + + if (res) { + $socket.emit('join-channels', { auth: { token: $user?.token } }); + await initChannels(); + showCreateChannel = false; + showChannels = true; + goto(`/channels/${res.id}`); + } + }} +/> + + { + await createFolder(folder); + showCreateFolderModal = false; + }} +/> + + + +{#if $showSidebar} +
      { + showSidebar.set(!$showSidebar); + }} + /> +{/if} + + { + if ($mobile) { + showSidebar.set(false); + } + }} +/> + + + +
      + + + + +
      +
      +
      + {#if $user !== undefined && $user !== null} + { + if (e.detail === 'archived-chat') { + showArchivedChats.set(true); + } + }} + > +
      +
      + {$i18n.t('Open + + {#if $config?.features?.enable_user_status} +
      + + + +
      + {/if} +
      +
      +
      + {/if} +
      +
      +
      +
      +{/if} + + + + +{#if $showSidebar} + + + {#if !$mobile} +